mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 19:59:26 +00:00
v5 (parameter-fed carried confirmed by the exact flow): pmpro 69, wpstatistics 172, mwwpform 68, vampi 16. Reading the recorded head of each order: every region ahead of a CVE is in its tier on a tied or higher score, and 10 of the 67 ahead of MW WP Form's are test-mail.php. The build's own word on what is product was on the region and not in the vector; it is now, at -2.0 for an unshipped file. Orders only, never promotes. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
190 lines
9.3 KiB
Python
190 lines
9.3 KiB
Python
"""The evidence vector and its tiers, on canned rows (flow-aware-ranking phase 0)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from openultrasast.model.evidence import (
|
|
TIER_CLEANSED,
|
|
TIER_EXCLUDE,
|
|
TIER_NO_SOURCE,
|
|
TIER_OPEN,
|
|
TIER_OPEN_PUBLIC,
|
|
evidence_from_rows,
|
|
)
|
|
|
|
SUMMARY = {"kind": "summary", "sinks": 1, "sourceLocal": False, "sourceNear": False, "familyInRepo": True}
|
|
|
|
|
|
def _sink(code: str, *, literal: bool = False, cleansed: bool = False, arity: int = 1) -> dict[str, object]:
|
|
return {
|
|
"kind": "sink",
|
|
"sink": code,
|
|
"sinkLine": "936",
|
|
"sinkMethod": "f",
|
|
"sinkArity": arity,
|
|
"sinkArg0Literal": literal,
|
|
"cleansedOnCall": cleansed,
|
|
}
|
|
|
|
|
|
def test_a_failed_query_is_not_a_tier() -> None:
|
|
"""No rows at all means the query did not answer. That is a degradation, never tier 0."""
|
|
assert evidence_from_rows(None) is None
|
|
assert evidence_from_rows([]) is None
|
|
|
|
|
|
def test_no_sink_in_scope_is_tier_zero_and_exact() -> None:
|
|
"""The only tier that excludes. A family with no sink in reach cannot produce a flow, in any language."""
|
|
e = evidence_from_rows([{**SUMMARY, "sinks": 0}], entry=True)
|
|
assert e is not None and e.tier == TIER_EXCLUDE
|
|
|
|
|
|
def test_no_sink_anywhere_in_the_repository_is_tier_zero_at_any_depth() -> None:
|
|
"""PMPro never calls `unserialize`: all 487 deserialization requests were asked of nothing."""
|
|
e = evidence_from_rows([{**SUMMARY, "familyInRepo": False}, _sink("x($y)")], entry=True)
|
|
assert e is not None and e.tier == TIER_EXCLUDE
|
|
|
|
|
|
def test_a_sink_with_no_source_is_asked_last() -> None:
|
|
e = evidence_from_rows([SUMMARY, _sink('$wpdb->get_row("... $id")')])
|
|
assert e is not None and e.tier == TIER_NO_SOURCE
|
|
|
|
|
|
def test_the_cve_shape_is_the_top_tier() -> None:
|
|
"""CVE-2023-23488: an entry-point method whose sink concatenates a parameter into SQL, with `esc_sql`
|
|
present in the FUNCTION but wrapping a different statement -- so `cleansedOnCall` is false here."""
|
|
e = evidence_from_rows(
|
|
[SUMMARY, _sink('$wpdb->get_var("... WHERE code = \'" . $code . "\'")', cleansed=False)],
|
|
entry=True,
|
|
)
|
|
assert e is not None and e.tier == TIER_OPEN
|
|
assert len(e.open_sinks) == 1
|
|
|
|
|
|
def test_cleansing_on_the_call_itself_lowers_the_tier() -> None:
|
|
"""The fixed side of that pair wraps the value in `$wpdb->prepare`, which is an ancestor of the argument."""
|
|
e = evidence_from_rows([SUMMARY, _sink('$wpdb->get_var($wpdb->prepare("... %s", $code))', cleansed=True)], entry=True)
|
|
assert e is not None and e.tier == TIER_CLEANSED
|
|
|
|
|
|
def test_a_literal_argument_cannot_carry_taint() -> None:
|
|
e = evidence_from_rows([SUMMARY, _sink('system("ls")', literal=True)], entry=True)
|
|
assert e is not None and e.tier == TIER_CLEANSED
|
|
|
|
|
|
def test_a_bound_shape_is_the_arbiters_own_rule() -> None:
|
|
"""`execute(sql, params)` binds where `execute(sql + x)` interpolates; same rule the taint verdict uses."""
|
|
e = evidence_from_rows([SUMMARY, _sink("cursor.execute(sql, params)", arity=2)], entry=True, bound_names=("execute",))
|
|
assert e is not None and e.tier == TIER_CLEANSED
|
|
|
|
|
|
def test_declared_public_or_a_local_source_asks_very_first() -> None:
|
|
public = evidence_from_rows([SUMMARY, _sink("unlink($f)")], entry=True, access_declared_public=True)
|
|
local = evidence_from_rows([{**SUMMARY, "sourceLocal": True}, _sink("unlink($_GET['f'])")])
|
|
assert public is not None and public.tier == TIER_OPEN_PUBLIC
|
|
assert local is not None and local.tier == TIER_OPEN_PUBLIC
|
|
|
|
|
|
def test_a_carried_field_counts_as_a_source() -> None:
|
|
"""`_delete_files()` has no parameters; its taint arrives as `$this->attachments`. Stage one of the join
|
|
already knows that, and the vector must not demote the region for lacking a direct source."""
|
|
e = evidence_from_rows([SUMMARY, _sink("unlink($file)")], carried=True)
|
|
assert e is not None and e.tier == TIER_OPEN_PUBLIC
|
|
|
|
|
|
def test_a_stringified_boolean_is_read_as_its_value_not_its_truthiness() -> None:
|
|
"""`bool("False")` is True. A serialiser that chose strings would make every open sink read as cleansed."""
|
|
rows = [{**SUMMARY, "sourceLocal": "False", "familyInRepo": "true"}, _sink("unlink($f)")]
|
|
rows[1]["cleansedOnCall"] = "False"
|
|
e = evidence_from_rows(rows, entry=True)
|
|
assert e is not None and e.tier == TIER_OPEN, "a 'False' string must not turn an open sink into a cleansed one"
|
|
|
|
|
|
def test_the_score_orders_within_a_tier_and_never_across_one() -> None:
|
|
"""Phase 2: the published weights rank pairs inside a tier. They cannot lift a pair out of tier 0 --
|
|
a pair with no sink scores nothing whatever the weights say -- and counts are capped so a 346-sink
|
|
file-scope region does not outrank everything by volume alone."""
|
|
from openultrasast.model.evidence import OPEN_SINK_CAP, WEIGHTS, Evidence, SinkEvidence
|
|
|
|
def sink(n: int, cleansed: bool = False) -> SinkEvidence:
|
|
return SinkEvidence(code=f"q({n})", line=n, method="m", arity=1, arg0_literal=False, cleansed_on_call=cleansed)
|
|
|
|
nothing = Evidence(sinks=(), source_local=True)
|
|
assert nothing.tier == 0 and nothing.score == 0.0, "no sink, no score: the weights cannot manufacture a pair"
|
|
|
|
one = Evidence(sinks=(sink(1),), source_near=True)
|
|
local = Evidence(sinks=(sink(1),), source_local=True)
|
|
assert local.tier > one.tier, "a local source is the higher tier, not merely the higher score"
|
|
assert local.score == WEIGHTS["open_sink"] + WEIGHTS["source_local"]
|
|
|
|
many = Evidence(sinks=tuple(sink(i) for i in range(50)), source_near=True)
|
|
assert many.tier == one.tier
|
|
assert many.score == WEIGHTS["open_sink"] * OPEN_SINK_CAP + WEIGHTS["source_near"], "capped: volume stops counting"
|
|
|
|
cleansed = Evidence(sinks=(sink(1), sink(2, cleansed=True)), source_near=True)
|
|
assert cleansed.score < one.score, "a cleansed sink beside an open one counts against the pair"
|
|
assert sorted([many, one, local], key=lambda e: e.order_key, reverse=True) == [local, many, one]
|
|
|
|
|
|
def test_the_query_may_answer_carried_itself() -> None:
|
|
"""Stage one of the two-stage join, reported by the query as `carried`. When present it is the answer;
|
|
when absent the caller's value stands, and absent-and-unknown stays None rather than becoming False."""
|
|
from openultrasast.model.evidence import TIER_OPEN, TIER_OPEN_PUBLIC, evidence_from_rows
|
|
|
|
sink = {
|
|
"kind": "sink",
|
|
"sink": "unlink($f)",
|
|
"sinkLine": "259",
|
|
"sinkMethod": "_delete_files",
|
|
"sinkArity": 1,
|
|
"sinkArg0Literal": False,
|
|
"cleansedOnCall": False,
|
|
}
|
|
said = evidence_from_rows([{"kind": "summary", "sinks": 1, "sourceNear": True, "carried": True, "familyInRepo": True}, sink])
|
|
assert said is not None and said.carried is True and said.tier == TIER_OPEN_PUBLIC
|
|
silent = evidence_from_rows([{"kind": "summary", "sinks": 1, "sourceNear": True, "familyInRepo": True}, sink])
|
|
assert silent is not None and silent.carried is None and silent.tier == TIER_OPEN
|
|
told = evidence_from_rows([{"kind": "summary", "sinks": 1, "sourceNear": True, "familyInRepo": True}, sink], carried=True)
|
|
assert told is not None and told.carried is True
|
|
|
|
|
|
def test_carried_provenance_promotes_only_when_source_fed() -> None:
|
|
"""A field fed from a source (or from a same-file method that reads one) is the strong `carried` and
|
|
promotes to tier 4; a field fed from a parameter of its method is the weak form, orders within the
|
|
tier, and never promotes -- every constructor assigns a field from a parameter."""
|
|
from openultrasast.model.evidence import TIER_OPEN, TIER_OPEN_PUBLIC, WEIGHTS, evidence_from_rows
|
|
|
|
sink = {
|
|
"kind": "sink",
|
|
"sink": "unlink($f)",
|
|
"sinkLine": "259",
|
|
"sinkMethod": "_delete_files",
|
|
"sinkArity": 1,
|
|
"sinkArg0Literal": False,
|
|
"cleansedOnCall": False,
|
|
}
|
|
strong = evidence_from_rows(
|
|
[{"kind": "summary", "sinks": 1, "sourceNear": True, "carried": True, "carriedKind": "source", "familyInRepo": True}, sink]
|
|
)
|
|
weak = evidence_from_rows(
|
|
[{"kind": "summary", "sinks": 1, "sourceNear": True, "carried": True, "carriedKind": "parameter", "familyInRepo": True}, sink]
|
|
)
|
|
none = evidence_from_rows(
|
|
[{"kind": "summary", "sinks": 1, "sourceNear": True, "carried": False, "carriedKind": "", "familyInRepo": True}, sink]
|
|
)
|
|
assert strong is not None and strong.tier == TIER_OPEN_PUBLIC and strong.carried is True
|
|
assert weak is not None and weak.tier == TIER_OPEN and weak.carried is False and weak.carried_weak
|
|
assert none is not None and none.tier == TIER_OPEN and not none.carried_weak
|
|
assert weak.score - none.score == WEIGHTS["carried_weak"]
|
|
|
|
|
|
def test_an_unshipped_file_orders_after_a_shipped_one_in_the_same_tier() -> None:
|
|
"""`shipped` is the build's own word on what is product; a test fixture with the same sink and source
|
|
is not where the bug ships. Orders only: the tier is unchanged."""
|
|
from openultrasast.model.evidence import WEIGHTS, Evidence, SinkEvidence
|
|
|
|
sink = SinkEvidence(code="q(1)", line=1, method="m", arity=1, arg0_literal=False, cleansed_on_call=False)
|
|
product = Evidence(sinks=(sink,), source_local=True, shipped=True)
|
|
fixture = Evidence(sinks=(sink,), source_local=True, shipped=False)
|
|
assert product.tier == fixture.tier
|
|
assert product.score - fixture.score == -WEIGHTS["unshipped"]
|