Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 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
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
5 changes: 5 additions & 0 deletions 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 Down
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
290 changes: 290 additions & 0 deletions src/checker/taint/python/tornado-taint-checker.ts
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]
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))
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)
}
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 (path && cls && (cls.vtype === 'class' || cls.vtype === 'symbol')) {
this.registerEntryPoints(analyzer, cls, path)
}
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
* @param path
*/
private registerEntryPoints(analyzer: any, cls: any, path: string) {
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

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

Choose a reason for hiding this comment

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

Taint sources scoped globally instead of to specific parameters

High Severity

The source scope entries use scopeFile: 'all', scopeFunc: 'all', locStart: 'all', locEnd: 'all' which causes ANY variable matching the parameter name to be marked as tainted across the entire codebase. According to source-util.ts, when all scope values are 'all', the taint source matches globally by name only. The Django checker correctly uses specific file paths, function names, and line ranges from param.loc and ep.fdef. This bug causes false positives where unrelated variables with common names like id or name are incorrectly marked as user input.

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.

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 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])
}
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

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
Loading