fix(llm-security): AST-taint clears taint on reassignment (#29) + sink test coverage (#28)

py-ast-taint.py Assign handler never removed a name from the tainted set on reassignment, so a source-then-constant/sanitizer rebind (g=os.getenv(); g='safe'; os.system(g), or x=shlex.quote(x)) kept stale taint and fired false AST-CMD-EXEC findings. Assign now clears taint for target names when the RHS is not a source. No cross-expression propagation added — the f-string/concat/alias recall gap (#27) stays deferred to v8.

#28: added fixtures+assertions locking the tainted subprocess/os.system AST-CMD-EXEC sink and the open(...,'w') AST-FILE-WRITE sink. Suite 1931/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
This commit is contained in:
Kjell Tore Guttormsen 2026-07-18 10:14:51 +02:00
commit 76f190cf78
4 changed files with 69 additions and 0 deletions

17
tests/fixtures/ast-scan/reassign.py vendored Normal file
View file

@ -0,0 +1,17 @@
import os
import shlex
# Taint must CLEAR when a name is rebound to a non-source value (#29).
# Neither function below may produce a finding.
def reassigned_constant():
g = os.getenv("G") # tainted source
g = "safe-constant" # rebound to a literal -> taint must clear
os.system(g) # must NOT be flagged
def sanitized_reassignment():
x = input("path> ") # tainted source
x = shlex.quote(x) # rebound to a non-source call -> taint must clear
os.system(x) # must NOT be flagged

20
tests/fixtures/ast-scan/sinks.py vendored Normal file
View file

@ -0,0 +1,20 @@
import os
import subprocess
# Locks the subprocess/os.system command sinks and the file-write sink (#28).
def run_user_command():
cmd = input("cmd> ") # tainted source
subprocess.run(cmd, shell=True) # sink: subprocess.* -> AST-CMD-EXEC
def shell_from_env():
target = os.getenv("TARGET") # tainted source
os.system(target) # sink: os.system -> AST-CMD-EXEC
def leak_to_file():
payload = os.environ["DATA"] # tainted source
log = open("out.txt", "w") # write handle
log.write(payload) # sink: file.write -> AST-FILE-WRITE (high)