fix(cpg): a sink is matched as a word, not as a substring

`fgets` contains `gets`. The sink matcher's last clause was an unbounded
`methodFullName.contains(n)`, so every bounded read in libpng matched the
sink for the unbounded one. All 26 entailed findings on that repository
were the same false positive:

  *argv -> fgets(pnmline, 256, wpng_info.infile)   (line 335, 8 steps)

which is the safe API being reported as the dangerous one -- and `fgets`
is in the fact tables as a SOURCE, never a sink. Verified against the CPG:
methodFullName is exactly "fgets", and .contains("gets") is true.

Third instance of one bug class in this codebase, after the sanitizer that
matched `resolve` inside `resolveUrl` and the discharger that matched
`user` inside `users`. The sink matcher had never been given the same
treatment. All four of its clauses now run through the shared bounded
matcher, and that matcher is the punctuation-tolerant version written for
task 2.6 -- so `input(`, which an unconditional lookahead had made
unmatchable in `input(x)`, works for the first time as well.

  libpng entailed   26 -> 0

Zero is the right answer for a PNG library whose only known CVE is out of
family. Gates byte-identical, so no pair outcome depended on the substring
match.

What remains on that scan is recorded, not claimed: 113 `c-unsafe-*` rows
assert an API is present rather than misused, and 50 overlay-coverage rows
are annotations rather than findings. 82 of the 113 are under `contrib/`.
Task 2.11 covers whether that band is worth putting in front of a
contributor at all; 2.10 already covers not scanning example code.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
Marius Ciepluch
2026-09-09 11:43:00 +02:00
co-authored by Claude Opus 5
parent 68b4e551f3
commit 67497c4d68
3 changed files with 52 additions and 9 deletions
+20
View File
@@ -221,3 +221,23 @@ def test_a_guard_discharges_only_the_obligation_it_is_a_guard_of() -> None:
assert "token_validator" not in identity, "authentication is not an object-level check"
assert "token_validator" in set(spec.dischargers_by_kind["path_guard"])
assert "['sub']" in identity or '["sub"]' in identity
def test_a_sink_is_matched_as_a_word_not_a_substring() -> None:
"""contributor-scan: `fgets` contains `gets`, and the safe API is not the dangerous one.
The sink matcher's last clause was an unbounded `methodFullName.contains(n)`, so every bounded read in
libpng matched the sink for the unbounded one: all 26 entailed findings on that repository were the same
false positive, `*argv -> fgets(buf, 256, f)`. Third instance of this bug class here, after the sanitizer
that matched `resolve` inside `resolveUrl` and the discharger that matched `user` inside `users`.
The query is Scala, so this asserts the fact tables still carry the pair that made it visible; the
boundary itself is exercised against a real CPG in the repository measurements.
"""
from openultrasast.model.specs import taint_specs
memory = taint_specs(language="c")["memory"]
assert "gets" in memory.sinks, "the unbounded read is a sink"
assert "fgets" in memory.sources, "the bounded read is a SOURCE of untrusted input, never a sink"
assert "fgets" not in memory.sinks