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:
Marius Ciepluch
2026-10-06 17:07:24 +02:00
co-authored by Claude Opus 4.8
parent d17ceb0e0c
commit 328406ad41
3 changed files with 226 additions and 1 deletions
+31
View File
@@ -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]:
+42 -1
View File
@@ -67,6 +67,7 @@ class SideRecord:
exit_code: int | None = None
stderr: str = ""
stdout: str = ""
control_observed: bool | None = None
@dataclass(frozen=True)
@@ -285,6 +286,7 @@ def _side(side: Side, demo: Path, family: str, timeout: int, browser: BrowserExe
private_values: list[str] = []
disk_peak = 0
control_observed: bool | None = None
root: Path | None = None
def timed(stage: str, operation: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
@@ -315,13 +317,14 @@ def _side(side: Side, demo: Path, family: str, timeout: int, browser: BrowserExe
result.stderr if result is not None else app_diagnostics.get("stderr", ""), maximum=1500, private=tuple(private_values)
),
stdout=diagnostic(getattr(result, "stdout", app_diagnostics.get("stdout", "")), maximum=1500, private=tuple(private_values)),
control_observed=control_observed,
)
try:
schema = load_demo(demo)
originals = (side.checkout, *side.dependencies, demo, *((side.products,) if side.products else ()))
initial = tuple(_tree(p) for p in originals)
for _ in range(1 if task_boundary else 3):
for run_index in range(1 if task_boundary else 3):
tick = time.monotonic()
with tempfile.TemporaryDirectory(prefix="ousast-proof-") as temporary:
root = Path(temporary)
@@ -475,6 +478,44 @@ def _side(side: Side, demo: Path, family: str, timeout: int, browser: BrowserExe
captures: dict[str, str] = {}
if schema["start"]["mode"] == "http":
# Positive control: on the first run, a benign in-root request
# for a verifier-planted marker, run as its OWN app start and
# fully guarded so it can only annotate, never change the
# verdict. Its marker returning proves the interface serves the
# fixture; its absence (or any control failure) is recorded, not
# trusted, since a valid app may answer only its own routes.
probe = oracle.control()
if run_index == 0 and probe is not None:
control_schema = {
**schema,
"steps": [
{
"type": "http",
"method": probe.method,
"path": probe.path,
**({"body": probe.body} if probe.body else {}),
}
],
}
try:
control_outputs = timed(
"run",
_http_steps,
control_schema,
command,
job,
app_scratch,
app_mounts,
app_env,
oracle.namespace_pid,
canary.nonce,
runner,
http_ready_check,
{},
)
control_observed = bool(control_outputs) and probe.token in control_outputs[0]
except (CommandFailure, OSError, ValueError, AssertionError):
control_observed = False
outputs = timed(
"run",
_http_steps,