utas: first real corpus, and two sanitiser gaps the audit caught

24 transactions across 11 connections from a retail session: login,
hub, one pack open, two squad saves, a quick-sell, with before/after
state manifests. Raw .ofcap stays gitignored at 0600; the sanitized
corpus is committed as adapter fixtures.

TWO GAPS FOUND BY AUDITING THE OUTPUT, NOT BY TRUSTING THE SANITISER.

1. `POST /ut/auth` carries `macAddress` and `deviceId`. Session tokens
   were being redacted correctly and these were not. A committed fixture
   is a published fixture.

2. Then, with those fixed, the audit fired AGAIN on the file about to be
   committed: `GET .../phishing/trusteddevice?deviceId=...` puts the id in
   the QUERY STRING. Three input surfaces carry identifiers -- headers,
   JSON bodies, and query strings -- and the sanitiser knew about two.

Both fixed in the tool rather than by editing the file, with a
regression test and a mutation for the query path.

AND A THIRD ARTEFACT MIX-UP, in the mutation harness itself. It reported
the query-redaction mutation as SURVIVED while a hand-run of the same
mutation killed it. Cause: the harness pointed at a stale scratchpad copy
of the test that pre-dated the query assertion, so it was faithfully
testing the mutated tool against a test that could not detect the
mutation. That is the same class as the build guard checking the wrong
binary and cargo reusing a binary compiled from mutated source -- the
third instance today of measuring the wrong artifact. The harness now
resolves ROOT from its own location and runs the COMMITTED test; the
stale copy is deleted.

Harness committed as scripts/mutate-utas-observe.py so this is repeatable
rather than a thing that happened once in a scratch directory. 6/6 killed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-08-11 18:33:59 +00:00
parent cdea85e214
commit 0b66662525
6 changed files with 405 additions and 50 deletions
+12 -2
View File
@@ -50,7 +50,7 @@ class H(http.server.BaseHTTPRequestHandler):
return self.rfile.read(n) if n else b""
def do_GET(self):
if self.path == "/chunked":
if self.path.split("?")[0] == "/chunked":
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Transfer-Encoding", "chunked")
@@ -126,7 +126,7 @@ def main():
b"GET /ut/game/fifa17/userMassInfo HTTP/1.1\r\nHost: t\r\n\r\n",
b"POST /ut/game/fifa17/purchased/items HTTP/1.1\r\nHost: t\r\n"
b"Content-Type: application/json\r\nContent-Length: %d\r\n\r\n" % len(body) + body,
b"GET /chunked HTTP/1.1\r\nHost: t\r\n\r\n",
b"GET /chunked?deviceId=DEADBEEFCAFE&keep=yes HTTP/1.1\r\nHost: t\r\n\r\n",
b"GET /ut/game/fifa17/hub HTTP/1.1\r\nHost: t\r\nConnection: close\r\n\r\n",
]
@@ -199,6 +199,16 @@ def main():
check("secret JSON value redacted", echoed.get("sid") == "<REDACTED>")
check("non-secret payload preserved exactly", echoed.get("echo_len") == len(body))
check("redaction is recorded, not silent", "redacted" in txs[1])
# Regression: the sanitiser handled headers and JSON bodies but not the
# QUERY STRING, and was one audit away from publishing a device id from
# `?deviceId=...`. Three surfaces carry identifiers, not two.
qtx = next((t for t in txs if t["request"]["query"]), None)
check("a secret query parameter is redacted",
qtx is not None and "deviceId=<REDACTED>" in qtx["request"]["query"],
qtx["request"]["query"] if qtx else "no query captured")
check("a non-secret query parameter is preserved",
qtx is not None and "keep=yes" in qtx["request"]["query"],
qtx["request"]["query"] if qtx else "")
srv.shutdown()
print()