-
Notifications
You must be signed in to change notification settings - Fork 23
feat: add Tornado checker #92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
699a0b6
86f3473
812b3a0
9d11215
30fdbdc
d46215b
e61be5f
f32f9fb
2067880
b08bead
d23f87b
9f58641
fbd5978
6c0fcd4
4aad909
92b1953
dc7583b
b96962a
3cceb62
c8ff9dc
39669e6
8d19c17
61c8b5f
f84b50f
15da4c1
2682e6a
fee55b1
a1721c2
f4428da
0d63baf
e20920a
8244780
73ae7ef
6f50c2a
d3ac17a
3ec065b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,290 @@ | ||
| const PythonTaintAbstractChecker = require('./python-taint-abstract-checker') | ||
| const Config = require('../../../config') | ||
| const completeEntryPoint = require('../common-kit/entry-points-util') | ||
| const { markTaintSource } = require('../common-kit/source-util') | ||
| const { isTornadoCall, tornadoSourceAPIs, isRequestAttributeAccess, extractTornadoParams } = require('./tornado-util') | ||
| const { extractRelativePath } = require('../../../util/file-util') | ||
|
|
||
| // Metadata storage | ||
| const tornadoRoutesMap = new WeakMap<any, any>() | ||
| const tornadoRouteMap = new WeakMap<any, { path: string; handler: any }>() | ||
| const tornadoPathMap = new WeakMap<any, string>() | ||
|
|
||
| /** | ||
| * Tornado Taint Checker - Simplified | ||
| */ | ||
| class TornadoTaintChecker extends PythonTaintAbstractChecker { | ||
| /** | ||
| * | ||
| * @param resultManager | ||
| */ | ||
| constructor(resultManager: any) { | ||
| super(resultManager, 'taint_flow_python_tornado_input') | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * @param analyzer | ||
| * @param scope | ||
| * @param node | ||
| * @param state | ||
| * @param info | ||
| */ | ||
| triggerAtStartOfAnalyze(analyzer: any, scope: any, node: any, state: any, info: any): void { | ||
| this.addSourceTagForcheckerRuleConfigContent('PYTHON_INPUT', this.checkerRuleConfigContent) | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * @param analyzer | ||
| * @param scope | ||
| * @param node | ||
| * @param state | ||
| * @param info | ||
| */ | ||
| triggerAtFunctionCallBefore(analyzer: any, scope: any, node: any, state: any, info: any): void { | ||
| super.triggerAtFunctionCallBefore(analyzer, scope, node, state, info) | ||
| const { fclos, argvalues } = info | ||
| if (Config.entryPointMode === 'ONLY_CUSTOM' || !fclos || !argvalues) return | ||
| const isApp = isTornadoCall(node, 'Application') | ||
| const isRouter = isTornadoCall(node, 'RuleRouter') | ||
| const isAdd = isTornadoCall(node, 'add_handlers') | ||
| if (isApp || isRouter || isAdd) { | ||
| let routes: any = null | ||
| if (isApp || isRouter) { | ||
| const isInit = ['__init__', '_CTOR_'].includes(node.callee?.property?.name || node.callee?.name) | ||
| routes = (isInit && argvalues[1]) || argvalues[0] | ||
| } else { | ||
| routes = argvalues[1] // isAdd case | ||
| } | ||
| if (routes) { | ||
| this.registerRoutesFromValue(analyzer, scope, state, routes) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Register routes from a collection value (List/Dict/Union/Single Symbol) | ||
| * @param analyzer | ||
| * @param scope | ||
| * @param state | ||
| * @param val | ||
| * @param prefix | ||
| */ | ||
| private registerRoutesFromValue(analyzer: any, scope: any, state: any, val: any, prefix = '') { | ||
| if (!val) return | ||
| // 1. Handle recording optimization (tornadoRoute) | ||
| if (tornadoRouteMap.has(val)) { | ||
| const { path, handler } = tornadoRouteMap.get(val)! | ||
| if (path && handler) { | ||
| this.finishRoute(analyzer, scope, state, handler, prefix + path) | ||
| return | ||
| } | ||
| } | ||
| // 2. Handle Union | ||
| if (val.vtype === 'union' && Array.isArray(val.value)) { | ||
| // Small optimization: if this union contains exactly a string and something else, it might be a flattened tuple | ||
| const pathVal = val.value.find( | ||
| (v: any) => tornadoPathMap.has(v) || typeof v.value === 'string' || typeof v.ast?.value === 'string' | ||
| ) | ||
| const hVal = val.value.find((v: any) => v.vtype === 'class' || v.vtype === 'symbol' || v.vtype === 'object') | ||
| if (pathVal && hVal) { | ||
| const path = tornadoPathMap.get(pathVal) || pathVal.value || pathVal.ast?.value | ||
| if (typeof path === 'string') { | ||
| this.finishRoute(analyzer, scope, state, hVal, prefix + path) | ||
| return | ||
| } | ||
| } | ||
| val.value.forEach((v: any) => this.registerRoutesFromValue(analyzer, scope, state, v, prefix)) | ||
| return | ||
| } | ||
| // 3. Handle raw tuple (path, handler) | ||
| if (val.value && typeof val.value === 'object') { | ||
| const pathArg = val.value['0'] | ||
| const handler = val.value['1'] | ||
| const path = (pathArg && tornadoPathMap.get(pathArg)) || pathArg?.value || pathArg?.ast?.value | ||
| if (typeof path === 'string' && handler) { | ||
| this.finishRoute(analyzer, scope, state, handler, prefix + path) | ||
| return | ||
| } | ||
| } | ||
| // 4. Handle Collections (List/Object with numeric keys) | ||
| const isObject = val.vtype === 'object' && val.value | ||
| if (isObject) { | ||
| const isCollection = Array.isArray(val.value) || Object.keys(val.value).some((k) => /^\d+$/.test(k)) | ||
| if (isCollection) { | ||
| const items = Array.isArray(val.value) ? val.value : Object.values(val.value) | ||
| items.forEach((item: any) => this.registerRoutesFromValue(analyzer, scope, state, item, prefix)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * @param analyzer | ||
| * @param scope | ||
| * @param state | ||
| * @param h | ||
| * @param path | ||
| */ | ||
| private finishRoute(analyzer: any, scope: any, state: any, h: any, path: string) { | ||
| if (!h) return | ||
| if (h.vtype === 'union' && Array.isArray(h.value)) h = h.value[0] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Union handler type only registers first element's entry pointsMedium Severity When the handler There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Empty union array access causes undefined reference errorMedium Severity In |
||
| // 1. Check for recorded nested routes (Application/Router instances) | ||
| const innerRoutes = tornadoRoutesMap.get(h) || (h.value && tornadoRoutesMap.get(h.value)) | ||
| if (innerRoutes) { | ||
| this.registerRoutesFromValue(analyzer, scope, state, innerRoutes, path) | ||
| return | ||
| } | ||
| // 2. Handle Class Definition (Handler classes) | ||
| let cls = h | ||
| if (cls.vtype !== 'class' && cls.ast?.type === 'ClassDefinition') { | ||
| try { | ||
| cls = analyzer.processInstruction(scope, cls.ast, state) || this.buildClassSymbol(cls.ast) | ||
| } catch (e) { | ||
| cls = this.buildClassSymbol(cls.ast) | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Silent exception swallowing without loggingLow Severity The catch block silently swallows the exception without logging. Other Python taint checkers in the codebase use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Caught exception silently swallowed without loggingLow Severity The catch block captures exception |
||
| } else if (cls.vtype === 'symbol' && cls.cdef) { | ||
| // If it's an instance symbol, get its class definition | ||
| cls = cls.cdef | ||
| } | ||
| if (path && cls && (cls.vtype === 'class' || cls.vtype === 'symbol')) { | ||
| this.registerEntryPoints(analyzer, cls, path) | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing ONLY_CUSTOM check in triggerAtFunctionCallAfterThe |
||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing ONLY_CUSTOM mode check in triggerAtFunctionCallAfterThe |
||
|
|
||
| /** | ||
| * | ||
| * @param analyzer | ||
| * @param cls | ||
| * @param path | ||
| */ | ||
| private registerEntryPoints(analyzer: any, cls: any, path: string) { | ||
| const methods = ['get', 'post', 'put', 'delete', 'patch'] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing HTTP methods compared to Django checkerLow Severity The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing HTTP methods head and options in entry point registrationMedium Severity The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing HTTP methods in Tornado entry point detectionMedium Severity The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing HTTP methods in entry point registrationLow Severity The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing HTTP methods head and options from handlersMedium Severity The |
||
| // Look for methods in cls.value, cls.field, or cls.value.field (Python specificity) | ||
| const classValue = cls.value?.field || cls.field || cls.value || {} | ||
| Object.entries(classValue).forEach(([name, fclos]: [string, any]) => { | ||
| if (methods.includes(name)) { | ||
| const ep = completeEntryPoint(fclos) | ||
| if (ep) { | ||
| ep.funcReceiverType = cls.ast?.id?.name || cls.sid || 'Unknown' | ||
| const isDuplicate = analyzer.entryPoints.some( | ||
| (existing: any) => | ||
| existing.functionName === ep.functionName && | ||
| existing.filePath === ep.filePath && | ||
| existing.funcReceiverType === ep.funcReceiverType | ||
| ) | ||
| if (!isDuplicate) { | ||
| analyzer.entryPoints.push(ep) | ||
| } | ||
| const info = extractTornadoParams(path) | ||
| let paramIdx = 0 | ||
| const actualParams = (fclos.fdef?.parameters || fclos.ast?.parameters || []) as any[] | ||
| actualParams.forEach((p: any) => { | ||
| const pName = p.id?.name || p.name | ||
| if (pName === 'self') return | ||
| paramIdx++ | ||
| // Add source scope for parameters based on URL pattern | ||
| if (info.named.includes(pName) || (info.named.length === 0 && paramIdx <= info.positionalCount)) { | ||
| this.sourceScope.value.push({ | ||
| path: pName, | ||
| kind: 'PYTHON_INPUT', | ||
| scopeFile: extractRelativePath(fclos?.ast?.loc?.sourcefile || ep.filePath, Config.maindir), | ||
| scopeFunc: ep.functionName, | ||
| locStart: p.loc?.start?.line, | ||
| locEnd: p.loc?.end?.line, | ||
| }) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Taint sources scoped globally instead of to specific parametersHigh Severity The source scope entries use |
||
| } | ||
| }) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Source scopes registered even for duplicate entry pointsMedium Severity The code that registers source scopes (lines 180-198) executes regardless of whether the entry point is a duplicate. The duplicate check at lines 171-178 only guards the |
||
| } | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * @param node | ||
| */ | ||
| private buildClassSymbol(node: any) { | ||
| const value: any = {} | ||
| node.body?.forEach((m: any) => { | ||
| if (m.type === 'FunctionDefinition') { | ||
| const name = m.id?.name || m.name?.name | ||
| if (name) { | ||
| value[name] = { | ||
| vtype: 'fclos', | ||
| fdef: m, | ||
| ast: m, | ||
| } | ||
| } | ||
| } | ||
| }) | ||
| return { vtype: 'class', value, ast: node } | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * @param analyzer | ||
| * @param scope | ||
| * @param node | ||
| * @param state | ||
| * @param info | ||
| */ | ||
| triggerAtFunctionCallAfter(analyzer: any, scope: any, node: any, state: any, info: any): void { | ||
| super.triggerAtFunctionCallAfter(analyzer, scope, node, state, info) | ||
| const { fclos, ret, argvalues } = info | ||
| if (Config.entryPointMode === 'ONLY_CUSTOM' || !fclos || !ret) return | ||
| const name = node.callee?.property?.name || node.callee?.name | ||
| // 1. Record route info for Rule, URLSpec, url (Recording phase) | ||
| const isRuleCall = isTornadoCall(node, 'Rule') || isTornadoCall(node, 'URLSpec') || name === 'url' | ||
| if (isRuleCall && argvalues && argvalues.length >= 2) { | ||
| const pArg = argvalues[0] | ||
| const path = (pArg && tornadoPathMap.get(pArg)) || pArg?.value | ||
| const handler = argvalues[1] | ||
| tornadoRouteMap.set(ret, { path, handler }) | ||
| } | ||
| // 2. Record path for PathMatches | ||
| if (isTornadoCall(node, 'PathMatches') && argvalues && argvalues.length >= 1) { | ||
| const path = argvalues[0]?.value | ||
| if (typeof path === 'string') { | ||
| tornadoPathMap.set(ret, path) | ||
| } | ||
| } | ||
| // 3. Record internal routes for Application/RuleRouter instances | ||
| const isInit = ['__init__', '_CTOR_'].includes(name) | ||
| if (isInit && argvalues && argvalues.length >= 2) { | ||
| const self = argvalues[0] | ||
| const routes = argvalues[1] | ||
| // Heuristic: if routes looks like a list/tuple of routes | ||
| const isRouteList = | ||
| routes && (routes.vtype === 'object' || routes.vtype === 'symbol' || Array.isArray(routes.value)) | ||
| if (isRouteList && self) { | ||
| tornadoRoutesMap.set(self, routes) | ||
| } | ||
| } | ||
| const isApp = isTornadoCall(node, 'Application') | ||
| const isRouter = isTornadoCall(node, 'RuleRouter') | ||
| if (!isInit && (isApp || isRouter)) { | ||
| tornadoRoutesMap.set(ret, argvalues[0]) | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing null check for
|
||
| if (tornadoSourceAPIs.has(name)) { | ||
| markTaintSource(ret, { path: node, kind: 'PYTHON_INPUT' }) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * @param analyzer | ||
| * @param scope | ||
| * @param node | ||
| * @param state | ||
| * @param info | ||
| */ | ||
| triggerAtMemberAccess(analyzer: any, scope: any, node: any, state: any, info: any): void { | ||
| if (Config.entryPointMode !== 'ONLY_CUSTOM' && isRequestAttributeAccess(node)) { | ||
| markTaintSource(info.res, { path: node, kind: 'PYTHON_INPUT' }) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing null check before marking taint sourceMedium Severity The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing null check before marking taint on undefined resultMedium Severity In |
||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing null check before marking taint sourceMedium Severity The |
||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Member access hook not invoked for Python analyzerHigh Severity The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing null check causes crash on unresolved member accessMedium Severity The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing null check before marking taint sourceMedium Severity The |
||
| } | ||
|
|
||
| export = TornadoTaintChecker | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Union handler only processes first element, missing others
Medium Severity
When a handler is a union type (e.g., from conditional assignment like
handler = A if cond else B),finishRouteonly processes the first element viah = h.value[0], discarding all other potential handlers. This causes two problems: entry points from other handlers in the union are never registered, and if union element ordering varies between analysis runs, different handlers get processed, leading to non-deterministic results where different bugs are discovered on subsequent runs.