Skip to content

fix: count a guard only when it examines what the sink receives - #136

Merged
SizzleUnrlsd merged 3 commits into
mainfrom
feat/guard-examines-flow
Sep 24, 2026
Merged

SizzleUnrlsd merged 3 commits into
mainfrom
feat/guard-examines-flow

Conversation

@SizzleUnrlsd

Copy link
Copy Markdown
Contributor

Closes #135. This is the rule the #98 measurement led to. It comes after the model fixes #129, #131 and #133, so the verdicts it changes are all real vulnerabilities.

Rule

A dominating guard used to make a flow a hotspot as soon as it read any value the flow's origin fed. if request.method == 'POST': therefore lowered os.system(request.POST['cmd']) to a hotspot (MEDIUM confidence), and almost every Django or Flask view starts with such a check.

A guard now makes a flow a hotspot only when it examines what the sink receives. The engine walks from the condition down its operands, and does not descend into an object when the guard only reads another attribute or item of it than the flow does. The guard still counts, as before, when it:

  • tests the value itself (if cmd:, len(cmd) < 10, cmd in allowed());
  • reads the same attribute or item again (if req.data: before req.data);
  • calls a method on the object (form.is_valid(), allowed(q.lower()));
  • reads an item under a key unknown on either side;
  • reads any part of an object the flow passes whole, to the sink or to a function, since then any attribute may reach it.

Proofs and authorization guards are unchanged.

Commits

  1. test: record the verdict of taint findings in the regression snapshots changes no behaviour. Snapshots used to record file, line, rule and function only, so a verdict change never showed up in review. All 19 are re-recorded: 28 vulnerabilities and 28 hotspots.
  2. test: … adds 8 refutation cases:
    • it fails on method, user and other-key checks, which must be vulnerabilities;
    • it passes on reread, same key, method call, transformed value and whole object, which must stay hotspots.
  3. fix: … holds the rule, the usage guide, the CHANGELOG, and the snapshot diff: exactly the 10 verdicts below.

The 10 findings raised to HIGH, each reviewed

Finding Guard that hid it Code
DjangoGoat authentication/views.py:57 sql-injection request.method == 'POST' User.objects.raw(query) with query = "... '%s' ..." % (username, password) from request.POST
dvpwa sqli/views.py:57 sql-injection request.method == 'POST' Student.create(conn, data['name']), which runs "VALUES ('%(name)s')" % ...
pygoat mitre.py:242 command-injection request.method == "POST" command_out("nmap " + request.POST.get('ip')), run with shell=True
pygoat views.py:430 command-injection request.method=="POST" subprocess.Popen("dig {}".format(domain), shell=True)
pygoat views.py:214 insecure-deserialization request.user.is_authenticated pickle.loads(base64.b64decode(request.COOKIES['token']))
pygoat views.py:560 insecure-deserialization + exploitable-vulnerability request.user.is_authenticated yaml.load(request.FILES["file"], yaml.Loader) on PyYAML 5.1
pygoat views.py:584 exploitable-vulnerability request.method == "POST" Image.open(request.FILES["file"]) on a vulnerable Pillow
pygoat views.py:927 path-traversal request.user.is_authenticated open(os.path.join(dirname, request.POST["blog"]))
pygoat views.py:963 ssrf request.method == "POST" requests.get(request.POST["url"])

The seven model false positives that an earlier version of this rule would have raised were fixed first: #129, #131 and #133. Two healthchecks login flows (hc/accounts/views.py:170 and 281) stay hotspots. They pass request whole into _check_2fa, whose reverse sanitizer is not applied through the summary (#134).

Verification

  • pytest: 1065 passed. ruff check src tests and mypy are clean.
  • pytest -m regression under Python 3.12: 19 passed, before and after the rule.
  • Mutation checks. Each of these changes fails at least one test: never pruning, dropping the method-call exception, ignoring what the flow reads, and ignoring objects taken whole.

@SizzleUnrlsd SizzleUnrlsd self-assigned this Sep 24, 2026
@SizzleUnrlsd
SizzleUnrlsd merged commit 8db4501 into main Sep 24, 2026
10 checks passed
@SizzleUnrlsd
SizzleUnrlsd deleted the feat/guard-examines-flow branch September 24, 2026 22:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Precision: a guard counts only when it examines a value the sink depends on

1 participant