Untrusted data reaches a code / injection sink (flow-confirmed)
ID |
untrusted-data-reaches-code-sink |
Severity |
critical |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
OWASP LLM |
LLM05:2025 — Improper Output Handling (secondary LLM03:2025 — Supply Chain) |
OWASP ASI |
ASI05:2026 (secondary) |
Family |
LLM05 — Improper Output Handling |
Red-team vectors |
Sandbox Escape / RCE |
Tags |
ai_security |
Description
An untrusted, external / user-controlled value reaches a dangerous sink — an unsafe deserializer
(pickle / cPickle / yaml.load), a code / command executor (eval, exec,
subprocess(…, shell=True), os.system), or an unparameterized SQL cursor.execute — along a
confirmed data-flow path. The flow may cross function and file boundaries.
This is the flow-confirmed counterpart of the sink-present detectors (unsafe-deserializer-on-remote-artifact, llm-output-to-shell-or-eval, template-injection), which flag a sink in isolation. By proving the source → sink connection it removes two error classes at once: the false positive (untrusted data detected but never used) and the false negative (the chain is real but split across functions/files so no single line looks dangerous).
Mechanism. Reuses the sankxy tainting engine — backward data-flow propagation plus interprocedural
cross-file resolution (the callee’s declaring file is located via the project’s software model and
parsed on demand) — for both Python and JavaScript / TypeScript. A declared sanitizer on the path
(parameterized query, html.escape, a safe parser) breaks the flow and suppresses the finding.
Scope vs the SAST tainting rules. Only AI-specific sources (an untrusted dataset row, a
remote-model artifact — the sources list) produce a new finding here. Generic sources
(input(), an HTTP response — the confirm_sources list) are analyzed too, but solely to confirm
(upgrade to high-confidence / flow-confirmed) a sink-present finding this scan already produced:
a generic chain like input() → eval() is already reported by the shipped SAST tainting rules
(python.code_injection and siblings), and duplicating it in the AI report would double-count the
same flaw across two reports.
Examples
Cross-file chain — source in one module, sink in another:
# untrusted_sources.py
def load_poison_row():
return load_dataset("attacker/poison-ds")["train"][0]["blob"] # source
# pickle_sink.py
from untrusted_sources import load_poison_row
blob = load_poison_row()
obj = pickle.loads(blob) (1)
| 1 | Untrusted data reaches pickle.loads across files — flagged critical, with the source → sink
path in the finding’s taint_path. |
A parameterized query (cursor.execute("… = %s", (name,))) or a sanitizer on the path produces
no finding.
Mitigation / Fix
-
Break the flow: validate / sanitize the untrusted value before the sink (allowlist, safe parser, parameterized query), or do not route external data into the operation.
-
Prefer safe APIs at the sink (
yaml.safe_load,torch.load(…, weights_only=True), parameterized SQL) so a tainted argument cannot execute.