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.