AI002 · STOA-LLM02-OUTPUT-EXEC
Model output observed reaching a dangerous execution or injection sink.
- OWASP: LLM02 Insecure Output Handling (v1.1); LLM05 (2025).
- Severity: critical (exec/sql/deserialize), high (markup/request).
- Gates: yes — exec class at high confidence only. This is the one AI
rule that can fail a build, because a static flow from model output into an execution sink can essentially be proven.
- Kind: data-flow (needs the AST layer).
Detection
A taint chain from a model-output source (response.choices[0].message.content, .output_text, llm.invoke(...), generateText(...), streaming deltas, json.loads(<model var>)) to a sink, classified by variant:
| variant | sinks | severity | gate |
|---|---|---|---|
| exec | eval, exec, os.system, subprocess.*, new Function, child_process.*, vm.runInContext | critical | yes (high conf.) |
| sql | cursor.execute(f"…"), .raw(), .query() (supersedes SEC003) | critical | no |
| deserialize | pickle.loads, yaml.load, marshal.loads | critical | no |
| markup | innerHTML =, dangerouslySetInnerHTML, document.write, Markup, mark_safe | high | no |
| request | tainted URL into requests.*/fetch (SSRF-shaped) | high | no |
No sanitizer breaks the exec chain; only dispatch through a static allowlist (using model output solely as a dict/map key) does.
Vulnerable → remediated
# VULNERABLE — model output executed as a shell command
reply = response.choices[0].message.content
subprocess.run(reply.strip(), shell=True)
# REMEDIATED — model selects from a static allowlist; code never runs free text
ACTIONS = {"restart_worker": restart_worker, "clear_cache": clear_cache}
choice = response.choices[0].message.content.strip()
if choice in ACTIONS:
ACTIONS[choice]()
Finding message
The value from model output reaches a
{class}-class sink with no interposed allowlist observed on the flow. Model output is attacker-influenceable whenever any untrusted content reaches the model (OWASP LLM02). (exec/high: this finding is gate-eligible.)Suppress: # stoa: ignore[AI002] reason