Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
699a0b6
fix: update entrypoint collector and enhance FastAPI route detection …
Ris-1kd Nov 26, 2025
86f3473
fix: update fastapi entrypoint
Ris-1kd Nov 26, 2025
812b3a0
fix: update fastapi
Ris-1kd Nov 26, 2025
9d11215
fix: resolve FastAPI route parsing issues
Ris-1kd Nov 27, 2025
30fdbdc
fix:FastAPI entrypoint
Ris-1kd Nov 27, 2025
d46215b
Merge branch 'antgroup:main' into main
Ris-1kd Dec 3, 2025
e61be5f
feat: add Tornado checker
Ris-1kd Dec 8, 2025
f32f9fb
feat: Tornado checker
Ris-1kd Dec 8, 2025
2067880
Fix: update Python analyzer and Tornado taint checker rules
Ris-1kd Dec 20, 2025
b08bead
Fix: update tornado framework
Ris-1kd Dec 22, 2025
d23f87b
Fix: update tornado framework
Ris-1kd Dec 22, 2025
9f58641
Fix: update tornado
Ris-1kd Dec 22, 2025
fbd5978
Fix: update tornado
Ris-1kd Dec 22, 2025
6c0fcd4
update tornado
Ris-1kd Dec 27, 2025
4aad909
Fix: update tornado
Ris-1kd Dec 28, 2025
92b1953
Fix: update tornado checker
Ris-1kd Jan 5, 2026
dc7583b
Fix: update tornado-framework
Ris-1kd Jan 9, 2026
b96962a
Fix: update tornado-framework
Ris-1kd Jan 9, 2026
3cceb62
Fix: update-tornado
Ris-1kd Jan 12, 2026
c8ff9dc
Fix:update-tornado
Ris-1kd Jan 12, 2026
39669e6
Fix: update-tornado-framework
Ris-1kd Jan 13, 2026
8d19c17
Fix: update tornado
Ris-1kd Jan 13, 2026
61c8b5f
Fix: update tornado-framework
Ris-1kd Jan 13, 2026
f84b50f
Fix:update tornado
Ris-1kd Jan 13, 2026
15da4c1
Fix: update tornado framework
Ris-1kd Jan 19, 2026
2682e6a
Fix: update tornado-framework
Ris-1kd Jan 19, 2026
fee55b1
Fix: update tornado framework
Ris-1kd Jan 20, 2026
a1721c2
Fix: update tornado framework
Ris-1kd Jan 22, 2026
f4428da
Fix: update tornado framework
Ris-1kd Jan 22, 2026
0d63baf
Fix: update tornado
Ris-1kd Jan 22, 2026
e20920a
Fix: update tornado-framework
Ris-1kd Jan 28, 2026
8244780
Fix: update tornado-framework
Ris-1kd Jan 28, 2026
73ae7ef
Fix: update tornado framework
Ris-1kd Jan 28, 2026
6f50c2a
Fix: update tornado framework
Ris-1kd Feb 3, 2026
d3ac17a
Fix: update tornado framework
Ris-1kd Feb 3, 2026
3ec065b
Fix: update tornado-framework
Ris-1kd Feb 4, 2026
e94be76
Fix: CVE-2024-11041
Ris-1kd Feb 11, 2026
a8d9c60
Fix: CVE
Ris-1kd Feb 11, 2026
fe20571
Fix: update tornado framework
Ris-1kd Mar 1, 2026
9c4d96f
Fix: update tornado
Ris-1kd Mar 1, 2026
8bd420d
Update checker-config.json
Ris-1kd Mar 5, 2026
b9f0fa1
Fix: update tornado-taint-checker
Ris-1kd Mar 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion resource/checker/checker-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@
"checkerPath": "checker/taint/python/django-taint-checker.ts",
"description": "python Django框架 entrypoint采集以及框架source添加"
},
{
"checkerId": "taint_flow_python_tornado_input",
"checkerPath": "checker/taint/python/tornado-taint-checker.ts",
"description": "python Tornado框架 entrypoint采集以及框架source添加"
},
{
"checkerId": "taint_flow_test",
"checkerPath": "checker/taint/test-taint-checker.ts",
Expand All @@ -161,4 +166,4 @@
"checkerPath": "checker/sdk/get-ast-source-code-checker.ts",
"description": "获取AST对应的源码"
}
]
]
2 changes: 2 additions & 0 deletions resource/checker/checker-pack-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"checkerIds": [
"taint_flow_python_input",
"taint_flow_python_django_input",
"taint_flow_python_tornado_input",
"callgraph",
"sanitizer"
],
Expand All @@ -96,6 +97,7 @@
"checkerIds": [
"taint_flow_python_input_inner",
"taint_flow_python_django_input",
"taint_flow_python_tornado_input",
"callgraph",
"sanitizer"
],
Expand Down
7 changes: 6 additions & 1 deletion resource/example-rule-config/rule_config_python.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
[
{
"checkerIds": ["taint_flow_python_input", "taint_flow_python_input_inner", "taint_flow_python_django_input"],
"checkerIds": [
"taint_flow_python_input",
"taint_flow_python_input_inner",
"taint_flow_python_django_input",
"taint_flow_python_tornado_input"
],
"sources": {
"FuncCallReturnValueTaintSource": [
{
Expand Down
262 changes: 262 additions & 0 deletions src/checker/taint/python/tornado-taint-checker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
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 } = require('./tornado-util')
const { extractRelativePath } = require('../../../util/file-util')

// Metadata storage
const tornadoRoutesMap = new WeakMap<any, any>()
const tornadoRouteMap = new WeakMap<any, any>()

/**
* 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]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Init fallback incorrectly treats self as routes

Medium Severity

The expression (isInit && argvalues[1]) || argvalues[0] has incorrect fallback behavior. When isInit is true (an __init__ call where argvalues[0] is self), if argvalues[1] is falsy (e.g., no handlers argument), this falls back to argvalues[0], incorrectly passing self as routes into registerRoutesFromValue. A ternary like isInit ? argvalues[1] : argvalues[0] would correctly yield undefined (caught by the if (routes) guard) instead of self.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Short-circuit fallback incorrectly uses self as routes

Medium Severity

The expression (isInit && argvalues[1]) || argvalues[0] has a subtle short-circuit bug. When isInit is true but argvalues[1] is falsy (e.g., Application.__init__(self) with no handlers argument), the || falls through to argvalues[0], which is self. This incorrectly treats the handler instance as the routes collection and passes it to registerRoutesFromValue. A ternary like isInit ? argvalues[1] : argvalues[0] would correctly yield undefined (skipped by the if (routes) guard) instead of misinterpreting self as routes.

Fix in Cursor Fix in Web

} 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
*/
private registerRoutesFromValue(analyzer: any, scope: any, state: any, val: any) {
if (!val) return
// 1. Handle recording optimization (tornadoRoute)
if (tornadoRouteMap.has(val)) {
const handler = tornadoRouteMap.get(val)
if (handler) {
this.finishRoute(analyzer, scope, state, handler)
return
}
}
// 2. Handle Union (often represents a flattened tuple)
if (val.vtype === 'union' && Array.isArray(val.value)) {
const handler = val.value.find((v: any) => v.vtype === 'class' || v.vtype === 'symbol' || v.vtype === 'fclos')
if (handler) {
this.finishRoute(analyzer, scope, state, handler)
}
val.value.forEach((v: any) => this.registerRoutesFromValue(analyzer, scope, state, v))
return
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Union handler double-processes handlers causing duplicate source scopes

Medium Severity

In registerRoutesFromValue, when handling a union-typed value, the code first finds a class/symbol/fclos handler via find and calls finishRoute on it (line 86), then iterates ALL union values with forEach and recursively processes each one (line 88). The handler already processed by finishRoute gets processed a second time via the recursive call (hitting case 4 at line 104). While entry points are deduplicated in registerEntryPoints, sourceScope.value.push at line 180 has no deduplication, leading to duplicate parameter source scope entries.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bugbot Autofix determined this is a false positive.

The referenced Tornado union-route handling code does not exist in this branch, so the duplicate-processing scenario cannot occur here.

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Union handler processed twice via recursive call

Medium Severity

When val.vtype is 'union', the code first finds a handler from val.value and calls finishRoute on it, then iterates over all union values (including the same handler) with registerRoutesFromValue. The recursion re-encounters that handler at step 4 and calls finishRoute again. While registerEntryPoints has duplicate detection, the repeated finishRoute call may redundantly invoke analyzer.processInstruction, causing unnecessary side effects.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Union handler processed twice causing duplicate source entries

Medium Severity

In registerRoutesFromValue, when handling a union value, a handler found via .find() is immediately sent to finishRoute, and then all union values (including that same handler) are recursively processed via forEachregisterRoutesFromValue. This causes the same handler to hit finishRoute twice. While registerEntryPoints has a duplicate check for analyzer.entryPoints, there is no dedup on this.sourceScope.value.push(), so source scope entries get duplicated.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Union handler gets processed twice causing duplicate entrypoints

Medium Severity

When handling a union value, finishRoute is called on the first matching handler found via find, and then registerRoutesFromValue is called on every union member including the same handler. The recursive call will hit step 4 (direct class/symbol handling) and call finishRoute again on the same handler, resulting in duplicate entry point registrations.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bugbot Autofix determined this is a false positive.

The referenced Tornado route-registration logic is absent because the target checker file is not in this codebase state.

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

}
// 3. Handle raw tuple (path, handler)
if (val.value && typeof val.value === 'object') {
const handler = val.value['1']
if (handler) {
const pathArg = val.value['0']
const path = pathArg?.value || pathArg?.ast?.value
if (typeof path === 'string') {
this.finishRoute(analyzer, scope, state, handler)
return
}
}
}
// 4. Handle direct class/symbol (likely a result of recursion or flattened tuple)
if (val.vtype === 'class' || val.vtype === 'symbol' || val.vtype === 'fclos') {
this.finishRoute(analyzer, scope, state, val)
return
}
// 5. Handle Collections (List/Object with numeric keys)
const isObject = (val.vtype === 'object' || !val.vtype) && val.value
if (isObject) {
const items = Array.isArray(val.value) ? val.value : Object.values(val.value)
const isLikelyCollection = Array.isArray(val.value) || Object.keys(val.value).some((k) => /^\d+$/.test(k))
if (isLikelyCollection) {
items.forEach((item: any) => this.registerRoutesFromValue(analyzer, scope, state, item))
}
}
}

/**
*
* @param analyzer
* @param scope
* @param state
* @param h
*/
private finishRoute(analyzer: any, scope: any, state: any, h: any) {
if (!h) return
if (h.vtype === 'union' && Array.isArray(h.value)) h = h.value[0]
Copy link

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), finishRoute only processes the first element via h = 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.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Union handler type only registers first element's entry points

Medium Severity

When the handler h is a union type (indicating static analysis found multiple possible handler classes), finishRoute only processes h.value[0] and discards all other potential handlers. This is inconsistent with processRoutes which correctly iterates through all union elements using forEach. If a route's handler could be Handler1 or Handler2 due to conditional logic, only Handler1's methods will be registered as entry points, potentially missing security vulnerabilities in Handler2.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty union array access causes undefined reference error

Medium Severity

In finishRoute, when h is a union type with an array value, the code accesses h.value[0] without checking if the array is empty. If h.value is [], then h becomes undefined, and the subsequent access to h.tornadoRoutes on line 130 will throw a "Cannot read property of undefined" error. The UnionValue class can create unions with empty value arrays (initialized as [] by default), making this a realistic edge case.

Fix in Cursor Fix in Web

// 1. Check for recorded nested routes (Application/Router instances)
const innerRoutes = tornadoRoutesMap.get(h) || (h.value && tornadoRoutesMap.get(h.value))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty union causes crash in finishRoute

Medium Severity

When h is a union with an empty value array, line 128 reassigns h to h.value[0] which is undefined. The null guard at line 127 only runs before this reassignment. On line 130, the expression h.value then throws a TypeError since h is now undefined. A null re-check after the union extraction is missing.

Fix in Cursor Fix in Web

if (innerRoutes) {
this.registerRoutesFromValue(analyzer, scope, state, innerRoutes)
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)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent exception swallowing without logging

Low Severity

The catch block silently swallows the exception without logging. Other Python taint checkers in the codebase use handleException(e, ...) from exception-handler to properly log errors, but this catch block completely ignores the error. This makes debugging difficult because failures during AST processing will be hidden. The PR discussion also notes this: "We should inspect the error log for error details."

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caught exception silently swallowed without logging

Low Severity

The catch block captures exception e but ignores it entirely. Other Python taint checkers in this codebase use handleException(e, ...) from the common exception handler to properly log errors. Silent error suppression makes debugging difficult since there's no indication when analyzer.processInstruction fails, and the fallback to buildClassSymbol happens invisibly. This was also flagged in the PR discussion but not resolved.

Fix in Cursor Fix in Web

} else if (cls.vtype === 'symbol' && cls.cdef) {
// If it's an instance symbol, get its class definition
cls = cls.cdef
}
if (cls && (cls.vtype === 'class' || cls.vtype === 'symbol')) {
this.registerEntryPoints(analyzer, cls)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing ONLY_CUSTOM check in triggerAtFunctionCallAfter

The triggerAtFunctionCallAfter method is missing the Config.entryPointMode === 'ONLY_CUSTOM' check that exists in both triggerAtCompileUnit (line 164) and triggerAtFunctionCallBefore (line 236). This inconsistency means that when entryPointMode is set to ONLY_CUSTOM, the Tornado-specific source API detection (like get_argument, get_cookie) and passthrough function handling will still execute, which is likely unintended behavior. The check should be added after the super call but before processing the fclos and ret values.

Fix in Cursor Fix in Web

}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing ONLY_CUSTOM mode check in triggerAtFunctionCallAfter

The triggerAtFunctionCallAfter method lacks the if (Config.entryPointMode === 'ONLY_CUSTOM') return check that exists in triggerAtCompileUnit, triggerAtFunctionCallBefore, and triggerAtMemberAccess. This inconsistency means tornado-specific taint marking (for APIs like get_argument and passthrough functions) will still run in ONLY_CUSTOM mode, while all other tornado-specific behaviors are disabled. The PR discussion explicitly flagged this as "not resolved yet".

Fix in Cursor Fix in Web


/**
*
* @param analyzer
* @param cls
*/
private registerEntryPoints(analyzer: any, cls: any) {
const methods = ['get', 'post', 'put', 'delete', 'patch']
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing HTTP methods compared to Django checker

Low Severity

The methods array is missing head and options HTTP methods, which are included in the analogous Django checker (['get', 'post', 'put', 'delete', 'patch', 'head', 'options']). Tornado's RequestHandler supports these methods, and if a handler implements a custom head or options method that processes user input, it won't be registered as an entry point. This could result in missed taint flows for handlers using these less common but valid HTTP methods.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing HTTP methods head and options in entry point registration

Medium Severity

The registerEntryPoints method only registers handlers for ['get', 'post', 'put', 'delete', 'patch'] HTTP methods, but is missing 'head' and 'options'. The Django checker in the same codebase correctly includes all seven methods: ['get', 'post', 'put', 'delete', 'patch', 'head', 'options']. Tornado's RequestHandler officially supports all these methods, so handlers implementing head() or options() won't be registered as entry points, potentially missing taint flows and security vulnerabilities in those handlers.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing HTTP methods in Tornado entry point detection

Medium Severity

The methods array only includes ['get', 'post', 'put', 'delete', 'patch'], but is missing head and options which are valid HTTP methods that Tornado RequestHandler supports. The Django checker correctly includes all seven methods. Handlers implementing head() or options() won't be registered as entry points, potentially causing missed taint flows and security vulnerabilities to go undetected.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing HTTP methods in entry point registration

Low Severity

The methods array is missing head and options HTTP methods that Tornado handlers can implement. The Django checker at the same codebase includes these methods in its httpMethods set. Tornado handlers implementing head() or options() methods won't be registered as entry points, meaning taint flows through those handlers won't be analyzed for vulnerabilities.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing HTTP methods head and options from handlers

Medium Severity

The methods array only includes ['get', 'post', 'put', 'delete', 'patch'] but is missing 'head' and 'options'. The Django taint checker includes all seven methods: ['get', 'post', 'put', 'delete', 'patch', 'head', 'options']. Tornado's RequestHandler class supports HEAD and OPTIONS methods, so handlers implementing these methods won't be registered as entry points, causing missed taint flow detection.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing head and options HTTP methods in entrypoints

Low Severity

The methods list in registerEntryPoints only includes ['get', 'post', 'put', 'delete', 'patch'], omitting head and options. Tornado handlers support these methods, and the analogous Django checker in django-taint-checker.ts correctly includes all seven: ['get', 'post', 'put', 'delete', 'patch', 'head', 'options']. This inconsistency means Tornado head and options handlers won't be registered as entrypoints for taint tracking.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing head and options HTTP handler methods

Medium Severity

The registerEntryPoints method only registers ['get', 'post', 'put', 'delete', 'patch'] as handler methods, but Tornado's RequestHandler also supports head and options. The Django checker in the same codebase includes all seven methods. Handler methods for head and options won't be registered as entrypoints, potentially missing taint flows through those paths.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing head and options HTTP handler methods

Medium Severity

The methods list in registerEntryPoints only includes ['get', 'post', 'put', 'delete', 'patch'], missing head and options. Tornado's RequestHandler supports all standard HTTP methods including these two. The Django checker in the same codebase correctly includes all seven methods. This means head and options handler methods won't be registered as entrypoints, causing missed taint flows.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bugbot Autofix determined this is a false positive.

There is no Tornado taint checker file here to patch, so this missing-methods finding is not applicable to the current branch.

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

// 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) {
analyzer.entryPoints.push(ep)
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
// Add source scope for all non-self parameters
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,
})
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Source scopes registered even for duplicate entry points

Medium 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 analyzer.entryPoints.push(ep) call, but the parameter processing and this.sourceScope.value.push() calls happen unconditionally within the if (ep) block. When the same route handler is encountered multiple times, duplicate taint source entries are registered, which could cause performance degradation and potentially duplicate taint findings.

Fix in Cursor Fix in Web

}
}
})
}

/**
*
* @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 handler = argvalues[1]
tornadoRouteMap.set(ret, handler)
}
// 2. 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])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null check for argvalues causes potential crash

Medium Severity

In triggerAtFunctionCallAfter, the guard on line 226 checks !fclos || !ret but does not check !argvalues. On line 249, argvalues[0] is accessed without any null guard. While lines 230 and 236 properly guard with argvalues && argvalues.length >= 2, the code at line 249 does not, so if argvalues is null or undefined, accessing argvalues[0] will throw a TypeError. Although the checker-manager wraps calls in try-catch, this would silently skip the remaining taint source tracking logic in this method.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bugbot Autofix determined this is a false positive.

This branch has no tornado-taint-checker.ts file or Tornado checker code, so the reported null-check path is not present.

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing argvalues null check causes potential crash

High Severity

In triggerAtFunctionCallAfter, argvalues is destructured from info but never checked for null/undefined in the early return guard on line 226 (unlike triggerAtFunctionCallBefore which does check !argvalues). Lines 230 and 236 properly guard argvalues before use, but line 249 accesses argvalues[0] without any guard, causing a TypeError crash when argvalues is nullish.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing argvalues null guard causes potential crash

High Severity

In triggerAtFunctionCallAfter, the early return on line 226 guards against !fclos || !ret but does not check !argvalues. While other accesses to argvalues (lines 230 and 236) safely guard with argvalues &&, line 249 directly accesses argvalues[0] without any null check. If argvalues is undefined or null, this will throw a TypeError at runtime. Compare with triggerAtFunctionCallBefore which correctly includes !argvalues in its early return guard.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing argvalues null check before access

Medium Severity

In triggerAtFunctionCallAfter, the guard at line 217 checks !fclos || !ret but does not check !argvalues. Lines 221 and 227 properly guard with argvalues && argvalues.length >= 2, but line 240 accesses argvalues[0] without any null check on argvalues. If argvalues is null/undefined when !isInit && (isApp || isRouter) is true, this will throw a runtime error.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bugbot Autofix determined this is a false positive.

src/checker/taint/python/tornado-taint-checker.ts does not exist in this branch, so the reported null-check path is not present.

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null check for argvalues before array access

Medium Severity

The code at line 263 accesses argvalues[0] without checking if argvalues exists or has elements. Unlike triggerAtFunctionCallBefore which guards with !argvalues in its early return (line 43), triggerAtFunctionCallAfter omits this check (line 231). All other argvalues accesses in the same method (lines 235, 242, 250) properly guard with argvalues && argvalues.length >= N, but this one does not. This could cause a TypeError if argvalues is undefined, or set tornadoRoutes to undefined if argvalues is empty.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null check before accessing argvalues array

Medium Severity

The code accesses argvalues[0] without checking if argvalues is defined. Unlike line 320 which properly guards with if (isInit && argvalues && argvalues.length >= 2), this block at line 347 has no such guard and will throw a TypeError if argvalues is undefined.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null check for argvalues may cause crash

Medium Severity

The code accesses argvalues[0] on line 348 without checking if argvalues is defined. In triggerAtFunctionCallBefore (line 57), there's a proper guard !argvalues before using it. Similarly, line 321 in the same function checks argvalues && argvalues.length >= 2. However, the block at lines 345-350 directly accesses argvalues[0] without any null check. When !isInit && (isApp || isRouter) is true but argvalues is undefined, this causes a TypeError crash.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing length check before accessing argvalues[0]

Low Severity

The code accesses argvalues[0] without verifying that argvalues contains any elements. When Application() or RuleRouter() is called without arguments, argvalues is an empty array and argvalues[0] evaluates to undefined. This causes tornadoRoutesMap to store undefined as routes, which may lead to unexpected behavior when the routes are later retrieved and processed. Other similar accesses in this file (lines 240, 247, 255) properly check argvalues.length before accessing array elements.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing argvalues guard causes potential runtime crash

Medium Severity

In triggerAtFunctionCallAfter, argvalues is accessed at line 268 (argvalues[0]) without a null/existence check. The early return guard at line 236 only checks !fclos || !ret but not argvalues. Every other branch in this method that accesses argvalues (lines 240, 247, 255) explicitly guards with argvalues && argvalues.length >= N, but this branch does not. If argvalues is null or undefined, this will throw a TypeError.

Fix in Cursor Fix in Web

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' })
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null check before marking taint source

Medium Severity

The triggerAtMemberAccess method calls markTaintSource(info.res, ...) without first checking if info.res is defined. When getMemberValue in the analyzer cannot resolve a member, it returns undefined, making info.res undefined. The markTaintSource function then crashes when trying to access res._tags on undefined. The similar method triggerAtFunctionCallAfter correctly guards with !ret before calling markTaintSource.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null check before marking taint on undefined result

Medium Severity

In triggerAtMemberAccess, markTaintSource(info.res, ...) is called without verifying that info.res is defined. The getMemberValue function in the analyzer can return undefined when a property cannot be resolved. When this happens, setTaint inside markTaintSource will crash trying to access res._tags on undefined. Unlike other similar checkers that use introduceTaintAtMemberAccess (which has additional guards), this code calls markTaintSource directly whenever isRequestAttributeAccess returns true.

Fix in Cursor Fix in Web

}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null check before marking taint source

Medium Severity

The triggerAtMemberAccess method calls markTaintSource(info.res, ...) without checking if info.res is defined. The isRequestAttributeAccess(node) function only validates the AST structure, not whether the member resolution succeeded. If getMemberValue returns undefined (e.g., when resolving self.request.body fails), the call to markTaintSource will crash when setTaint tries to access properties on undefined.

Fix in Cursor Fix in Web

}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Member access hook not invoked for Python analyzer

High Severity

The triggerAtMemberAccess method is dead code for Python analysis. The base analyzer.ts calls checkAtMemberAccess in its processMemberAccess implementation (at line 1382-1384), but python-analyzer.ts completely overrides processMemberAccess without calling the checker hook. This means patterns like self.request.body, self.request.headers, and other request attribute accesses that isRequestAttributeAccess is designed to detect will never trigger taint marking. The tornado checker's intent to mark these as taint sources won't work, potentially causing security vulnerabilities to go undetected.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null check causes crash on unresolved member access

Medium Severity

The triggerAtMemberAccess method calls markTaintSource(info.res, ...) without checking if info.res is defined. When member access resolution fails (common in static analysis for unresolved references), info.res is undefined. The markTaintSource function then calls setTaint which attempts res._tags = res._tags || new Set(), throwing a TypeError. Unlike triggerAtFunctionCallAfter which guards with if (!ret) return, this method lacks the equivalent if (!info.res) return check.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null check before marking taint source

Medium Severity

The triggerAtMemberAccess method calls markTaintSource(info.res, ...) without checking if info.res is defined. Looking at markTaintSource in source-util.ts, it calls setTaint(unit, kind) which attempts to set res._tags = res._tags || new Set(). If info.res is undefined (when getMemberValue returns undefined), this will throw a TypeError. The pattern in triggerAtFunctionCallAfter correctly guards against this with if (!ret) return, but this guard is missing here.

Fix in Cursor Fix in Web

}

export = TornadoTaintChecker
67 changes: 67 additions & 0 deletions src/checker/taint/python/tornado-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Tornado Source APIs
*/
export const tornadoSourceAPIs = new Set([
'get_argument',
'get_query_argument',
'get_body_argument',
'get_query_arguments',
'get_body_arguments',
'get_cookie',
'get_secure_cookie',
Comment on lines +5 to +11
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'get_argument',
'get_query_argument',
'get_body_argument',
'get_query_arguments',
'get_body_arguments',
'get_cookie',
'get_secure_cookie',
'get_argument',
'get_query_argument',
'get_body_argument',
'get_query_arguments',
'get_body_arguments',
'get_cookie',
'get_secure_cookie',
'get_arguments',
'get_json_body'

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not resolved yet

'get_arguments',
'get_json_body',
])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing get_json_body taint source for JSON body handling

Medium Severity

The tornadoSourceAPIs set is missing get_json_body, a method suggested by the PR reviewer for tracking tainted JSON body data. The reviewer's comment "This is not resolved yet" indicates this was a requested change that wasn't addressed. If Tornado applications use this method to retrieve user-controlled JSON data, that data won't be marked as tainted.

Fix in Cursor Fix in Web


/**
* Detect if node is an access to a Tornado request attribute
* @param node
*/
export function isRequestAttributeAccess(node: any): boolean {
if (node?.type !== 'MemberAccess') return false
const inner = node.object
return (
inner?.type === 'MemberAccess' &&
inner.object?.type === 'Identifier' &&
inner.object?.name === 'self' &&
inner.property?.name === 'request' &&
[
'body',
'query',
'headers',
'cookies',
'files',
'uri',
'path',
'arguments',
'remote_ip',
'host',
'query_arguments',
'body_arguments',
].includes(node.property?.name)
)
}

/**
* Check if node is a Tornado Application call
* @param node
* @param targetName
*/
export function isTornadoCall(node: any, targetName: string): boolean {
if (!node || node.type !== 'CallExpression') return false
const { callee } = node
const funcName = callee.property?.name || callee.name
const objectName = callee.object?.name || callee.object?.property?.name
if (funcName === targetName || objectName === targetName) {
return true
}
if (['__init__', '_CTOR_'].includes(funcName)) {
let current = callee.object
while (current) {
const currentName = current.name || current.property?.name
if (currentName === targetName) return true
current = current.object || current.callee
}
}
return false
}
6 changes: 5 additions & 1 deletion src/engine/analyzer/python/common/python-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,11 @@ class PythonAnalyzer extends (Analyzer as any) {
resolved_prop.name = '_CTOR_'
}
if (!resolved_prop) return defscope
return this.getMemberValue(defscope, resolved_prop, state)
const res = this.getMemberValue(defscope, resolved_prop, state)
if (this.checkerManager && (this.checkerManager as any).checkAtMemberAccess) {
this.checkerManager.checkAtMemberAccess(this, defscope, node, state, { res })
}
return res
}

/**
Expand Down