mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
fix(taint): a filter returns its value argument; a source is the access, not a call around it
The apply_filters class from the second PMPro adjudication (2 of 3
remaining false positives): `apply_filters('pmpro_random_code', $code,
$this)` returned whatever $this carried, and a report's `$type` reached SQL
only as an extra filter argument. Two causes, both fixed generally:
- The dispatch fact now says what an apply call RETURNS: `returns` names
the calls whose result is the dispatched value and `value_arg` its
position (WordPress: apply_filters/apply_filters_ref_array return
argument 2; do_action returns nothing). A path through such a call must
ENTER by that argument -- the first of the run of the call's arguments
before it, because the engine hops between sibling arguments
($_GET["c"] -> md5("x") -> apply_filters). Loaded and validated as data;
the query knows no framework.
- A call was a source when the pattern appeared anywhere in its text, so
`apply_filters(..., $_GET["c"])` and `sanitize_text_field($_POST[..])`
were sources themselves -- the "a token matches the thing it names"
invariant. A call is now a source when the pattern names its callee,
receiver or the access it is; the inner access stays a source.
Controls: dispatch facts load/validate; a real-Joern case with request
data as filter context (quiet) and as the filtered value (found). 1,453
host and 18 query tests pass.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f1e43d3646
commit
7b8ff3cf44
@@ -0,0 +1,36 @@
|
||||
"""A registry read returns its VALUE argument, not its context: a fact, loaded and checked."""
|
||||
|
||||
import pytest
|
||||
|
||||
from openultrasast.model.specs import taint_specs
|
||||
from openultrasast.semantic.facts import FactLoadError, load_facts
|
||||
|
||||
|
||||
def _write(tmp_path, dispatch: str) -> None:
|
||||
(tmp_path / "php.toml").write_text('language = "php"\n[[source]]\nid = "g"\npatterns = ["$_GET"]\n' + dispatch)
|
||||
|
||||
|
||||
def test_the_shipped_facts_say_which_argument_a_filter_returns() -> None:
|
||||
spec = taint_specs(language="php")["injection"]
|
||||
assert "apply_filters:2" in spec.dispatch_value and "do_action:0" in spec.dispatch_value
|
||||
|
||||
|
||||
def test_a_dispatch_row_declares_returns_with_a_position(tmp_path) -> None:
|
||||
row = '[[dispatch]]\nid = "hooks"\nregister = ["add_filter"]\napply = ["apply_filters", "do_action"]\n'
|
||||
_write(tmp_path, row + 'returns = ["apply_filters"]\nvalue_arg = 2\n')
|
||||
(fact,) = load_facts(tmp_path).dispatches
|
||||
assert fact.returns == ("apply_filters",) and fact.value_arg == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"row",
|
||||
[
|
||||
'returns = ["apply_filters"]\n', # a returning call without the value's position
|
||||
'returns = ["not_an_apply_call"]\nvalue_arg = 2\n',
|
||||
"value_arg = -1\n",
|
||||
],
|
||||
)
|
||||
def test_a_malformed_dispatch_row_is_refused(tmp_path, row: str) -> None:
|
||||
_write(tmp_path, '[[dispatch]]\nid = "hooks"\nregister = ["add_filter"]\napply = ["apply_filters"]\n' + row)
|
||||
with pytest.raises(FactLoadError):
|
||||
load_facts(tmp_path)
|
||||
Reference in New Issue
Block a user