mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 11:49:28 +00:00
feat(facts): PHP semantic facts, validated against a real PHP CPG
contributor-scan 5.1, the first step toward the v0.1 WordPress target. Written against a PHP CPG rather than against PHP source, because the two do not look alike. Verified with joern-parse 4.0.623 / php2cpg on a probe: $_GET["name"] -> call `<operator>.indexAccess`, CODE $_GET["name"] mysqli_query($conn, $sql) -> call NAMED mysqli_query echo $_GET["msg"] -> call NAMED echo So sources match on code and sinks match on name, and every entry the probe did not exercise is marked unverified in the file -- `include` and `require` among them, which is PHP's most characteristic path bug and the first thing to check on a real checkout. Five families derive (injection, output_encoding, path, deserialization, untrusted_destination) with no sink that is its own sanitizer -- asserted by a test, because that bug class has now appeared three times in three matchers. `php` is added to the region model's languages, without which the facts would have reached nothing. The arbiter decides 4 of 4 on the probe: the SQL concat entails, its `mysqli_real_escape_string` twin drops to corroborated, `echo` XSS entails and `system` command injection entails. **A whole scan finds 2 of those 3, and the gap is the finding.** PHP files yield FILE-LEVEL regions because no PHP entry points are mapped, and the taint arbiter returns one verdict per region per family -- so a file-level region reports at most one injection finding however many it holds. `system` won on flow length and the SQL injection went unreported. That is the mirror image of task 2.9: that one is many regions collapsing to one site, this is many sites collapsing to one verdict, and it bites hardest exactly where PHP needs it not to, since a WordPress file is large and has no decorator for the mapper to find. Recorded as task 5.4 with WordPress's registration model (add_action, add_filter, admin-ajax, REST routes), which the mapper already handles in principle for other frameworks. A 13-line probe is not a WordPress result. What is established is that the seam works end to end for PHP; everything about scale and coverage is unmeasured, and there are still zero PHP pairs in a corpus of 263 python, 351 c_cpp, 47 javascript and 49 java. Gates byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
co-authored by
Claude Opus 5
parent
b5d3cd6ab6
commit
61811512a8
@@ -241,3 +241,46 @@ def test_a_sink_is_matched_as_a_word_not_a_substring() -> None:
|
||||
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
|
||||
|
||||
|
||||
def test_php_facts_derive_the_families_a_web_application_needs() -> None:
|
||||
"""contributor-scan 5.1, toward the WordPress target.
|
||||
|
||||
Written against a real PHP CPG rather than against PHP source, because the two do not look alike: a
|
||||
superglobal is an `<operator>.indexAccess` whose CODE reads `$_GET["name"]`, while a dangerous operation
|
||||
is a plain call named `mysqli_query` or `echo`. Sources therefore match on code and sinks on name.
|
||||
"""
|
||||
from openultrasast.model.specs import taint_specs
|
||||
|
||||
families = taint_specs(language="php")
|
||||
|
||||
assert {"injection", "output_encoding", "path", "deserialization", "untrusted_destination"} <= set(families)
|
||||
injection = families["injection"]
|
||||
assert "mysqli_query" in injection.sinks, "SQL"
|
||||
assert "system" in injection.sinks, "command execution"
|
||||
assert "$_GET" in injection.sources, "a superglobal is matched on the code of an index access"
|
||||
assert "echo" in families["output_encoding"].sinks, "PHP's XSS sink is a language construct php2cpg emits as a call"
|
||||
|
||||
|
||||
def test_no_php_sink_is_its_own_sanitizer() -> None:
|
||||
"""The bug class this codebase has hit three times, asserted for the newest fact table before it can."""
|
||||
from openultrasast.model.specs import taint_specs
|
||||
|
||||
for family, spec in taint_specs(language="php").items():
|
||||
assert not set(spec.sinks) & set(spec.sanitizers), f"{family}: an operation that discharges itself can never be reported"
|
||||
|
||||
|
||||
def test_a_php_file_can_become_a_region() -> None:
|
||||
"""Facts alone reach nothing: `_LANGUAGES` decides whether a file is offered to the model layer at all."""
|
||||
from openultrasast.model.regions import regions_for
|
||||
|
||||
class _Target:
|
||||
path = "wp-admin/admin-ajax.php"
|
||||
language = "php"
|
||||
loc = 100
|
||||
tags: tuple[str, ...] = ()
|
||||
|
||||
regions = regions_for([], [_Target()])
|
||||
|
||||
assert regions, "a php file with no entry point should still yield a file-level region"
|
||||
assert regions[0].language == "php"
|
||||
|
||||
Reference in New Issue
Block a user