feat(model): an access-control finding names a line, and what the PDG does not give us

contributor-scan 2.7. Taint and config verdicts have always carried a
`path:line:function` location; dominance never did, so every
access-control finding reached a contributor as
`api_views/users.py:?:update_password` -- the right file by accident,
because the region was asked about that file, and no line at all. The
query now reports the operation's file and the verdict carries the
location, with an engine that cannot report one getting no location rather
than an invented one.

Two questions answered by measurement while doing it.

**Does this factor in the PDG?** The data half, centrally: `reachableByFlows`
is the entire taint arbiter and `joern-parse` applies the `dataflowOss`
overlay. The control half, not at all -- and it is not available to us.
Measured: 0 of 13,184 calls in the libpng CPG and 0 of 1,152 in the vampi
CPG have a controlling control structure, so `controlledBy` returns nothing
in either. Everything guard-shaped therefore runs on CFG dominance or on
syntactic conditions, which is exactly why 2.12's first attempt failed:
libpng's bound reaches its sink through an `error` flag, and that is a
control-dependence fact and nothing else. Recorded as 2.14, including the
possibility that it cannot be enabled -- which would bound what guard
reasoning this engine can ever do.

**Would checking out a vulnerable libpng find a known CVE?** No, and not
because of the version. Every one of the twelve files using
`strcpy`/`strcat`/`sprintf`/`gets` is under `contrib/`; the library itself
uses none of them. libpng overflows through array and pointer arithmetic
and `memcpy` sizing, and five of its buffer-overflow fixes touch no
modelled sink at all. Pinning an older commit would produce the same
silence for the same reason.

That leaves two routes of very different size, recorded as 2.15: pin a C
project whose CVE genuinely runs through a string function, which is cheap
and would give the memory family its first true positive; or model how C
actually overflows, which is size arithmetic rather than sink matching and
should not be started by accident.

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 13:09:47 +02:00
co-authored by Claude Opus 5
parent 9221ba8e40
commit c1191b3a6b
4 changed files with 104 additions and 4 deletions
+4 -1
View File
@@ -28,7 +28,7 @@
// the kinds that would discharge it and `dischargersByKind` maps a kind to its tokens; when both are absent
// the flat list is used, which is the committed single-request behaviour.
//
// Output: a fenced JSON array of {operation, opLine, opMethod, dominatingGuards}.
// Output: a fenced JSON array of {operation, opLine, opMethod, opFile, dominatingGuards}.
// BATCHED, like taint.sc: `requests` is {id: {operations, dischargers, function}} and the output is
// {id: [row, ...]}. One JVM start answers a whole scan. The single-request form is kept so the committed
@@ -151,6 +151,9 @@
"operation" -> op.code.take(200),
"opLine" -> op.lineNumber.getOrElse(-1).toString,
"opMethod" -> method.name,
// Where the operation is. Without it an access-control finding reaches a contributor as
// `api_views/users.py:?:update_password` -- a location no editor can open.
"opFile" -> method.filename,
"dominatingGuards" -> ujson.Arr(guards.map(ujson.Str(_)): _*)
)
}
+17 -2
View File
@@ -77,10 +77,24 @@ def verdict(cpg: CpgResult, spec: DominanceSpec, *, function: str = "", file: st
f"while {len(guarded_methods)} sibling handler(s) are guarded "
f"({', '.join(guarded_methods[:3])})"
)
return Verdict(rung=Rung.ENTAILED, family=spec.family, witness=witness)
return Verdict(rung=Rung.ENTAILED, family=spec.family, witness=witness, location=_location(target))
witness = f"{target['opMethod']} line {target['opLine']}: obligated operation with no discharging guard, and no guarded sibling"
return Verdict(rung=Rung.CORROBORATED, family=spec.family, witness=witness)
return Verdict(rung=Rung.CORROBORATED, family=spec.family, witness=witness, location=_location(target))
def _location(row: Mapping[str, object]) -> str:
"""``path:line:function`` of the obligated operation, when the engine reported a file.
Taint and config verdicts have always carried this; dominance did not, so every access-control finding
reached a contributor as `api_views/users.py:?:update_password` -- the right file by accident, because
the region was asked about that file, and no line at all.
"""
where = str(row.get("opFile") or "")
line = str(row.get("opLine") or "")
if not where or not line.lstrip("-").isdigit() or int(line) < 0:
return ""
return f"{where}:{line}:{row.get('opMethod') or '?'}"
def _operations(rows: object) -> list[dict[str, object]]:
@@ -97,6 +111,7 @@ def _operations(rows: object) -> list[dict[str, object]]:
"operation": str(row.get("operation", "")),
"opLine": str(row.get("opLine", "")),
"opMethod": str(row.get("opMethod", "")),
"opFile": str(row.get("opFile", "")),
"guards": [str(g) for g in guards] if isinstance(guards, Sequence) and not isinstance(guards, str) else [],
}
)