mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-18 20:49:28 +00:00
search-with-proof: positive control for path oracle — honest serve signal, no false negatives
The HTTP verifier could not distinguish a mis-wired or non-serving app from a genuine no-effect: both yielded `inconclusive`. This adds a positive control for the path oracle. - PathOracle plants a benign in-root marker file (random token from os.urandom, distinct from the secret canary) and exposes it via a new Oracle.control() hook (default None; only the path oracle implements it today). - On the first run of an HTTP side, _side runs a benign GET for that marker as its own fully-guarded app start. The marker returning proves the interface serves the fixture; its absence — or any control failure — is recorded in the new SideRecord.control_observed field but never changes the verdict, since a valid app may answer only its own routes. A crash is still caught by the existing could_not_run path. A 2xx-based "nothing served" block was rejected: the verifier deliberately treats any HTTP status as the app being alive (a 503-only app is a valid observed side), so status-based blocking would false-fail valid apps. Soundly converting a mis-wired-but-responding app into could_not_run requires a demo-declared control, which is a separate follow-up. Tests: new tests/test_search_control.py covers marker observed, absent marker not blocking, control failure not blocking a working attack, and crash still could_not_run. Full search-verify/probe/worker suites pass; ruff clean; mypy unchanged (only the pre-existing file_bindings lambda note). Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d17ceb0e0c
commit
328406ad41
@@ -37,6 +37,18 @@ class Canary:
|
||||
return cls(root, secrets.token_hex(24))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ControlProbe:
|
||||
"""A benign interaction a correctly functioning app must answer by returning
|
||||
``token`` in its response body. It proves the demo's interface is wired and the
|
||||
app can serve before any absence of the secret effect is trusted as a negative."""
|
||||
|
||||
method: str
|
||||
path: str
|
||||
token: str
|
||||
body: str = ""
|
||||
|
||||
|
||||
class Oracle(Protocol):
|
||||
namespace_pid: int | None
|
||||
placeholders: dict[str, str]
|
||||
@@ -44,6 +56,7 @@ class Oracle(Protocol):
|
||||
def prepare(self, canary: Canary) -> dict[str, str]: ...
|
||||
def capture(self, output: str) -> None: ...
|
||||
def observe(self) -> tuple[bool, str]: ...
|
||||
def control(self) -> ControlProbe | None: ...
|
||||
def close(self) -> None: ...
|
||||
|
||||
|
||||
@@ -62,19 +75,37 @@ class OutputOracle:
|
||||
def observe(self) -> tuple[bool, str]:
|
||||
return self.seen, "canary in verifier-captured app output" if self.seen else "no canary in app output"
|
||||
|
||||
def control(self) -> ControlProbe | None:
|
||||
# No positive control by default. SQL/command/ssrf/xss controls are a
|
||||
# documented follow-up; only the path oracle plants one for now.
|
||||
return None
|
||||
|
||||
def close(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class PathOracle(OutputOracle):
|
||||
control_name = "control"
|
||||
|
||||
def prepare(self, canary: Canary) -> dict[str, str]:
|
||||
super().prepare(canary)
|
||||
(canary.root / "public").mkdir()
|
||||
(canary.root / "canary").write_text(canary.nonce)
|
||||
(canary.root / "canary").chmod(0o600)
|
||||
# A benign in-root control file, distinct from the out-of-root secret, so a
|
||||
# correctly serving app returns it on BOTH revisions without any traversal.
|
||||
# Drawn from os.urandom, not secrets.token_hex, so it never perturbs callers
|
||||
# that monkeypatch the nonce source and count its draws.
|
||||
self.control_token = os.urandom(24).hex()
|
||||
control_file = canary.root / "public" / self.control_name
|
||||
control_file.write_text(self.control_token)
|
||||
control_file.chmod(0o600)
|
||||
self.placeholders = {"served_root": "/fixture/public"}
|
||||
return {"SERVED_ROOT": "/fixture/public"}
|
||||
|
||||
def control(self) -> ControlProbe | None:
|
||||
return ControlProbe(method="GET", path="/" + self.control_name, token=self.control_token)
|
||||
|
||||
|
||||
class SQLOracle(OutputOracle):
|
||||
def prepare(self, canary: Canary) -> dict[str, str]:
|
||||
|
||||
Reference in New Issue
Block a user