Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 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
ec630ee
Merge branch 'antgroup:main' into main
Ris-1kd Apr 1, 2026
9b559bb
fix: repair CVE-2026-26198 broken flow and improve chain stability
Ris-1kd Apr 1, 2026
a058641
Merge branch 'main' of https://github.com/Ris-1kd/YASA-Engine into main
Ris-1kd Apr 1, 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
28 changes: 28 additions & 0 deletions resource/checker/cve-2026-26198-rule.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[
{
"checkerIds": ["taint_flow_python_input"],
"sources": {
"FuncCallReturnValueTaintSource": [
{
"fsig": "input",
"scopeFile": "all",
"scopeFunc": "all"
}
]
},
"sinks": {
"FuncCallTaintSink": [
{
"fsig": "sqlalchemy.text",
"args": ["0"],
"attribute": "SQLInjection"
},
{
"fsig": "text",
"args": ["0"],
"attribute": "SQLInjection"
}
]
}
}
]
22 changes: 20 additions & 2 deletions src/engine/analyzer/common/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -885,12 +885,17 @@ class Analyzer extends MemSpace {
scope
)
const rightVal = this.processInstruction(scope, right, state)
const rightRaw = rightVal && typeof rightVal.getRawValue === 'function' ? rightVal.getRawValue() : rightVal
// Keep the old union short-circuit for performance, but allow small unions
// to be iterated so list-comprehension element flows can continue.
const isSmallIterableUnion =
rightVal?.vtype === 'union' && Array.isArray(rightVal?.value) && rightVal.value.length > 0 && rightVal.value.length <= 8
if (
!Array.isArray(rightVal) &&
(this.inRange ||
rightVal?.vtype === 'primitive' ||
Object.keys(rightVal.getRawValue()).length === 0 ||
rightVal?.vtype === 'union')
Object.keys(rightRaw || {}).length === 0 ||
(rightVal?.vtype === 'union' && !isSmallIterableUnion))
) {
if (value) {
if (value.type === 'VariableDeclaration') {
Expand Down Expand Up @@ -2796,6 +2801,19 @@ class Analyzer extends MemSpace {
fclos.execute.call(this, obj, argvalues, state, node, scope)
}

const taintedCtorArg = Array.isArray(argvalues)
? argvalues.find((arg: any) => arg && (arg.hasTagRec || AstUtil.hasTag(arg, '')))
: undefined
if (taintedCtorArg) {
obj.hasTagRec = true
if (!obj._tags && taintedCtorArg._tags) {
obj._tags = _.clone(taintedCtorArg._tags)
}
if (!obj.trace && taintedCtorArg.trace) {
obj.trace = _.clone(taintedCtorArg.trace)
}
}

if (!argvalues) return obj

if (!fdef) {
Expand Down
232 changes: 228 additions & 4 deletions src/engine/analyzer/python/common/python-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,11 @@ class PythonAnalyzer extends (Analyzer as any) {
einfo: state.einfo,
})

const fclos = this.processInstruction(scope, node.callee, state)
let fclos = this.processInstruction(scope, node.callee, state)
const callFallback = this.tryResolveMemberCallFallback(scope, node, state, fclos)
if (callFallback) {
fclos = callFallback
}
if (node?.callee?.type === 'MemberAccess' && fclos.fdef && node.callee?.object?.type !== 'SuperExpression') {
fclos._this = this.processInstruction(scope, node.callee.object, state)
}
Expand Down Expand Up @@ -336,6 +340,7 @@ class PythonAnalyzer extends (Analyzer as any) {
return
}
const res = this.executeCall(node, fclos, argvalues, state, scope)
this.propagateTaintFromMemberReceiver(node, fclos, res)

if (fclos.vtype !== 'fclos' && Config.invokeCallbackOnUnknownFunction) {
this.executeFunctionInArguments(scope, fclos, node, argvalues, state)
Expand All @@ -355,6 +360,90 @@ class PythonAnalyzer extends (Analyzer as any) {
return res
}

/**
* Fallback for member calls unresolved as symbol/undefine.
* It binds the method by name from loaded class definitions to current receiver.
*/
tryResolveMemberCallFallback(scope: any, node: any, state: any, currentFclos: any) {
if (!node || node?.callee?.type !== 'MemberAccess') {
return undefined
}
if (!currentFclos || !['symbol', 'undefine', 'uninitialized'].includes(currentFclos.vtype)) {
return undefined
}
const prop = node.callee.property
if (!prop || prop.type !== 'Identifier' || !prop.name) {
return undefined
}

const receiver = this.processInstruction(scope, node.callee.object, state)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The receiver object node.callee.object has already been evaluated during the processing of node.callee (which calls processMemberAccess). Re-evaluating it here is redundant and could lead to incorrect analysis if the receiver expression has side effects (e.g., a function call). It is safer and more efficient to retrieve the already evaluated receiver from currentFclos.object if available.

Suggested change
const receiver = this.processInstruction(scope, node.callee.object, state)
const receiver = currentFclos?.object || this.processInstruction(scope, node.callee.object, state)

if (!receiver) {
return undefined
}

const preferredClassNames: string[] = []
const receiverName = `${receiver?.id || ''}.${receiver?.sid || ''}.${receiver?.qid || ''}`
if (receiverName.includes('SelectAction')) {
preferredClassNames.push('SelectAction')
}
if (receiverName.includes('QueryAction')) {
preferredClassNames.push('QueryAction')
}

for (const className of preferredClassNames) {
const cls = this.findClassInModules(className)
const method = cls?.value?.[prop.name]
if (method?.vtype === 'fclos') {
const methodCopy = _.clone(method)
methodCopy._this = receiver
methodCopy.parent = receiver
methodCopy.object = receiver
methodCopy._qid = `${receiver?.qid || receiver?.sid || className}.${prop.name}`
return methodCopy
}
}

const modules = this.moduleManager?.field || {}
for (const modKey of Object.keys(modules)) {
const modScope = modules[modKey]
for (const key of Object.keys(modScope?.value || {})) {
const cls = modScope.value[key]
if (cls?.vtype !== 'class') continue
const method = cls?.value?.[prop.name]
if (method?.vtype === 'fclos') {
const methodCopy = _.clone(method)
methodCopy._this = receiver
methodCopy.parent = receiver
methodCopy.object = receiver
methodCopy._qid = `${receiver?.qid || receiver?.sid || key}.${prop.name}`
return methodCopy
}
}
}
Comment on lines +407 to +422
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

This global search for a method across all classes in all modules is computationally expensive. Since this fallback is triggered for unresolved member calls, it could significantly degrade performance in large projects. Consider implementing a global method index or caching the results of this search to improve efficiency.

return undefined
}

/**
* Preserve taint on return value for member calls when receiver already carries taint.
* This helps Python dynamic method chains like str.split(...)->list[index] keep dataflow.
*/
propagateTaintFromMemberReceiver(node: any, fclos: any, ret: any) {
if (!ret || !node || node?.callee?.type !== 'MemberAccess') {
return
}
const receiver = fclos?.getThis ? fclos.getThis() : fclos?._this
if (!receiver || !(receiver.hasTagRec || AstUtil.hasTag(receiver, ''))) {
return
}
ret.hasTagRec = true
if (!ret._tags && receiver._tags) {
ret._tags = _.clone(receiver._tags)
}
if (!ret.trace && receiver.trace) {
ret.trace = _.clone(receiver.trace)
}
}

/**
*
* @param scope
Expand Down Expand Up @@ -561,13 +650,124 @@ class PythonAnalyzer extends (Analyzer as any) {
resolved_prop.name = '_CTOR_'
}
if (!resolved_prop) return defscope
const res = this.getMemberValue(defscope, resolved_prop, state)
let res = this.getMemberValue(defscope, resolved_prop, state)
const taintedIndexFallback = this.tryResolveTaintedIndexFallback(node, defscope, resolved_prop, res)
if (taintedIndexFallback) {
res = taintedIndexFallback
}
const fallbackObject = this.tryResolveObjectsManagerFallback(defscope, resolved_prop, res)
if (fallbackObject) {
res = fallbackObject
}
if (this.checkerManager && (this.checkerManager as any).checkAtMemberAccess) {
this.checkerManager.checkAtMemberAccess(this, defscope, node, state, { res })
}
return res
}

/**
* Python negative-index / computed-index fallback:
* when member lookup fails on a tainted sequence-like value, preserve taint on indexed result.
* This keeps flows like `parts = s.split('__'); parts[-1]` alive even when list elements are not concretely modeled.
*/
tryResolveTaintedIndexFallback(node: any, defscope: any, resolvedProp: any, currentRes: any) {
const unresolved = !currentRes || ['symbol', 'undefine', 'uninitialized'].includes(currentRes.vtype)
if (!unresolved || !defscope) {
return undefined
}

const isComputed = !!node?.computed
const propType = resolvedProp?.type || resolvedProp?.ast?.type
const isIndexLikeProp =
isComputed || propType === 'UnaryExpression' || propType === 'Literal' || propType === 'primitive'
if (!isIndexLikeProp) {
return undefined
}

if (!(defscope.hasTagRec || AstUtil.hasTag(defscope, ''))) {
return undefined
}

const fallback = SymbolValue({
type: 'MemberAccess',
object: defscope,
property: resolvedProp,
ast: node,
sid: `${defscope?.sid || defscope?.id || 'obj'}[idx]`,
qid: `${defscope?.qid || defscope?.sid || defscope?.id || 'obj'}[idx]`,
})
fallback.hasTagRec = true
if (!fallback._tags && defscope._tags) {
fallback._tags = _.clone(defscope._tags)
}
if (!fallback.trace && defscope.trace) {
fallback.trace = _.clone(defscope.trace)
}
return fallback
}

/**
* Try resolving dynamic ORM-style "<Model>.objects" to QuerySet-like object when direct member lookup fails.
*/
tryResolveObjectsManagerFallback(defscope: any, resolved_prop: any, currentRes: any) {
if (!resolved_prop || resolved_prop.type !== 'Identifier' || resolved_prop.name !== 'objects') {
return undefined
}
if (!defscope || defscope.vtype !== 'class') {
return undefined
}
const unresolved = !currentRes || ['symbol', 'undefine', 'uninitialized'].includes(currentRes.vtype)
if (!unresolved) {
return undefined
}

const querySetClass = this.findClassInModules('QuerySet')
if (!querySetClass || querySetClass.vtype !== 'class') {
return undefined
}

return this.cloneClassMethodsAsObject(querySetClass, `${defscope?.id || 'Model'}_objects`)
}

/**
* Find a class symbol by name from processed python modules.
*/
findClassInModules(className: string) {
const modules = this.moduleManager?.field || {}
for (const modKey of Object.keys(modules)) {
const modScope = modules[modKey]
const candidate = modScope?.value?.[className]
if (candidate?.vtype === 'class') {
return candidate
}
}
return undefined
}

/**
* Create a lightweight object view from class methods to model descriptor-returned manager objects.
*/
cloneClassMethodsAsObject(classClos: any, objectName: string) {
const obj = ObjectValue({
id: objectName,
sid: objectName,
qid: objectName,
parent: classClos?.parent || this.topScope,
ast: classClos?.ast,
})
obj._this = obj
obj.vtype = 'object'
for (const fieldName in classClos?.value || {}) {
const v = classClos.value[fieldName]
if (!v) continue
const vCopy = _.clone(v)
vCopy._this = obj
vCopy.parent = obj
obj.value[fieldName] = vCopy
}
return obj
}

/**
*
* @param ast
Expand Down Expand Up @@ -681,9 +881,33 @@ class PythonAnalyzer extends (Analyzer as any) {
processOperator(scope: any, node: any, argvalues: any, operator: any, state: any) {
switch (operator) {
case 'push': {
this.saveVarInCurrentScope(scope, node, argvalues, state)
const has_tag = (scope && scope.hasTagRec) || (argvalues && argvalues.hasTagRec)
// Python list-comprehension in UAST lowers to temp-list + push operations.
// We need append semantics here instead of overwriting temp variable each time.
let container = this.getMemberValueNoCreate(scope, node, state)
if (!container || ['undefine', 'uninitialized', 'symbol'].includes(container.vtype)) {
const sid = node?.name || node?.sid || '__tmp__'
container = ObjectValue({
id: sid,
sid,
qid: scope?.qid ? `${scope.qid}.${sid}` : sid,
parent: scope,
})
container.vtype = 'object'
container._this = container
}
if (!container.value || typeof container.value !== 'object') {
container.value = {}
}
const numericIndices = Object.keys(container.value)
.filter((k) => /^\d+$/.test(k))
.map((k) => Number(k))
const nextIndex = numericIndices.length > 0 ? Math.max(...numericIndices) + 1 : 0
Comment on lines +901 to +904
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Calculating the next numeric index by filtering all keys and finding the maximum value on every push operation results in $O(N^2)$ complexity for list constructions (like list comprehensions). A more efficient approach would be to maintain a length property on the container or use Object.keys(container.value).length if the container is known to be a sequential list without holes.

Suggested change
const numericIndices = Object.keys(container.value)
.filter((k) => /^\d+$/.test(k))
.map((k) => Number(k))
const nextIndex = numericIndices.length > 0 ? Math.max(...numericIndices) + 1 : 0
const nextIndex = Object.keys(container.value).length

container.value[String(nextIndex)] = argvalues
this.saveVarInCurrentScope(scope, node, container, state)

const has_tag = (scope && scope.hasTagRec) || (argvalues && argvalues.hasTagRec) || container?.hasTagRec
if (has_tag) {
container.hasTagRec = has_tag
scope.hasTagRec = has_tag
}
}
Expand Down
Loading