What this is. A sanitized excerpt of an internal self-audit — we ran our own code through our attack-first review method ("dogfooding"). Target identifiers are redacted; the methodology, findings, severities and data-flow traces are verbatim. This is a representative sample of a client deliverable, not a marketing mock-up.
| Target | internal loopback API service (name redacted) — a local async task-gateway |
| Scan mode | READ-ONLY — exploit tests were written but not executed; each finding carries a static source→sink trace with concrete payloads. No source file was modified. |
| Method | Recon → Parallel Hunt → Adversarial Falsification → Capability Filter → independent read-only Verify (maker ≠ grader) |
| Findings | 0 Critical, 0 High, 1 Medium, 7 Low — all Confirmed (2 further candidates dropped in falsification) |
Most scanners optimise for recall — they surface everything that pattern-matches a weakness and leave you to sort signal from noise. We optimise for precision under an adversary: every candidate must survive a step whose only job is to disprove it.
Recon ─▶ Parallel Hunt ─▶ Adversarial Falsify ─▶ Capability Filter ─▶ Independent Verify
map many hypotheses "prove this is NOT "is it reachable read-only grader,
sinks in parallel real" (default: by a real actor?" different context
& inputs refuted) (maker ≠ grader)
On this run the falsification step dropped 2 of 10 candidates as false positives (see Falsification drops below). A verified-only report is worth more than a long one: you spend remediation budget on real defects, not on triage.
The service binds 127.0.0.1 only (gateway.py:475) and authenticates every route
except /health with a single shared install token X-Api-Token
(secrets.token_urlsafe(24), constant-time compare :273). There is exactly one
principal — any local process holding the token — so no cross-tenant boundary
exists. This caps every finding: "read/write any task" is the authenticated baseline,
and blast radius is one loopback host. The highest-value finding (F-001) is
principal-independent (attribution spoof + destructive), hence Medium; everything
else is Low. Honest scoping beats severity inflation.
| ID | Title | CWE | Severity | Status |
|---|---|---|---|---|
| F-001 | Answer spoofing + forced deletion of any pending task | CWE-290 / CWE-639 | Medium | Confirmed |
| F-002 | next_id() TOCTOU race → duplicate IDs / context overwrite |
CWE-367 / CWE-362 | Low | Confirmed |
| F-003 | Unbounded request-body read (/submit, /upload) |
CWE-400 | Low | Confirmed |
| F-004 | Unbounded base64 attachment decode + disk write (/upload) |
CWE-400 | Low | Confirmed |
| F-005 | Unbounded request-body read (/answer) |
CWE-400 | Low | Confirmed |
| F-006 | Global (non-per-caller) rate-limit shared-budget DoS | CWE-770 / CWE-307 | Low | Confirmed |
| F-007 | Unauthenticated /health triggers filesystem scrub |
CWE-306 | Low | Confirmed |
| F-008 | Auth token in cleartext file + stdout; chmod no-op on Windows |
CWE-312 | Low | Confirmed |
Each finding shipped with a static PoC and an exploit test (written, not run).
| Field | Value |
|---|---|
| CWE | CWE-290 (Auth Bypass by Spoofing) / CWE-639 (Authorization Bypass Through User-Controlled Key) |
| Severity | Medium |
| Entry point | POST /answer |
| Location | gateway.py:448-451 (write); poll read :302-308; scrub delete :186-191 |
| Data flow | attacker sets reply_to = any task id → the handler appends {author:"worker", …} with no check the task exists / is still open (:448-451) → (a) the polling UI receives the forged answer as genuine worker output (:304-307); (b) the "answered" cleanup deletes the target task's full-context transient before the real worker reads it (:186-191). |
| Fix | In the answer handler, verify the target task exists and is open before appending; gate the answered-deletion on a validated answer; enforce answer provenance on read. |
| Root cause | No existence/open-state binding on the user-controlled reply_to key; hardcoded author field. |
| Why Medium (not Low) | Principal-independent: spoof + destroy in-flight tasks is beyond the authenticated "post answers" baseline. |
The fix — shipped & re-tested the same day (identifiers redacted, logic verbatim):
# POST /answer handler
- # any caller could answer ANY id — existence unchecked, author hardcoded
- append_reply(author="worker", reply_to=req["reply_to"], text=req["text"])
+ # a reply must answer a real, still-OPEN request — everything else is refused
+ if read_request(reply_to) is None: # no such open request -> 404
+ return 404
+ if already_answered(reply_to): # can't double-answer -> 409
+ return 409
+ append_reply(author="worker", reply_to=reply_to, text=text)
Re-test after the fix: the spoof path now returns 404 and appends nothing; a pending request survives the cleanup sweep (it can no longer be forced into the "answered" set by an attacker). Both prongs — attribution spoof and forced deletion — close at the same guard. Time from confirmation to verified fix: same day.
next_id() TOCTOU race| Field | Value |
|---|---|
| CWE | CWE-367 (TOCTOU) / CWE-362 (Race Condition) |
| Severity | Low |
| Entry point | POST /submit, POST /upload (concurrency) |
| Location | gateway.py:136-138 (next_id), callers :336, :390 |
| Data flow | next_id() derives the id from len(read_events()) (:137) → under the threading HTTP server (:475) two concurrent requests read the same count before either appends → both allocate the same id → duplicate ledger entry and one task's context transient overwritten. The rate-limit lock guards only the hit counter, not id allocation. |
| Fix | Serialize next_id()→append→write under one dedicated lock, or derive ids from a monotonic counter / UUID instead of ledger length. |
/submit, /upload)| Field | Value |
|---|---|
| CWE | CWE-400 (Uncontrolled Resource Consumption) |
| Severity | Low |
| Entry point | POST /submit, POST /upload |
| Location | gateway.py:323, :376 |
| Data flow | attacker sets a large Content-Length → rfile.read(n) with no MAX_BODY cap → single unbounded allocation. The rate limiter caps request count, not size. |
| Fix | Add a module-level MAX_BODY; reject over-limit / absent / negative Content-Length with 413/400 in one shared _read_json_body() helper used by every POST route. |
/upload)| Field | Value |
|---|---|
| CWE | CWE-400 |
| Severity | Low |
| Entry point | POST /upload |
| Location | gateway.py:392-403 |
| Data flow | attacker sends a large base64 attachment → b64decode (:394) → open(path,"wb").write(raw) (:402-403) with no length check → attacker-controlled bytes written to disk; the TTL sweep is slower than the write rate. |
| Fix | Enforce MAX_ATTACHMENT_BYTES on the decoded length before the disk write; reject oversize. |
/answer)| Field | Value |
|---|---|
| CWE | CWE-400 |
| Severity | Low |
| Entry point | POST /answer |
| Location | gateway.py:445 |
| Data flow | same pattern as F-003 at the third handler: int(Content-Length) → rfile.read(n) with no cap. |
| Fix | Same shared _read_json_body() cap as F-003 — fixes all three routes at the root. |
| Field | Value |
|---|---|
| CWE | CWE-770 (Allocation Without Limits) / CWE-307 (Improper Restriction of Excessive Attempts) |
| Severity | Low |
| Entry point | all routes (gate runs pre-auth) |
| Location | gateway.py:261-269, checked :289, :312 |
| Data flow | the limiter keeps one class-global hit list with no per-caller key, checked before auth → an unauthenticated local caller exceeding the window forces 429 for the legitimate operator (shared budget). |
| Fix | Key the limiter per caller (client address and/or token) with an isolated per-key window. |
/health triggers filesystem scrub| Field | Value |
|---|---|
| CWE | CWE-306 (Missing Authentication for Critical Function) |
| Severity | Low |
| Entry point | GET /health |
| Location | gateway.py:290-292 → scrub :161-192 |
| Data flow | the /health branch runs the filesystem scrub before the auth check (:293) → any local unauth caller triggers deletion of already-eligible (answered / TTL-stale) transients. Opportunistic only, but real — and it chains with F-001 (unauth deletion of attacker-marked tasks). |
| Fix | Make /health a pure read — drop the scrub call; rely on the authenticated sweep and the startup wipe. |
| Field | Value |
|---|---|
| CWE | CWE-312 (Cleartext Storage of Sensitive Information) |
| Severity | Low |
| Location | gateway.py:66-70 (file), :468 (stdout) |
| Data flow | token_urlsafe(24) written cleartext to .api-token; os.chmod(...,0o600) is a no-op on Windows (:70) → the file may be world-readable; the full token is also printed to stdout (:468) → a local non-operator user reading the file/console obtains the full token (which unlocks F-001/006/007). |
| Fix | Encrypt at rest (OS keyring / DPAPI); set a real Windows ACL instead of relying on POSIX chmod; mask the stdout print (fingerprint / last-4 only). |
A finding list is only trustworthy if it also proves what was checked and cleared. Every externally-controlled input was traced to a disposition (redacted field names):
| Input | Disposition |
|---|---|
Origin header |
SAFE — governs only CORS reflection, not access; prefix quirk not leverageable (token still required). |
X-Api-Token |
SAFE (auth) — constant-time compare. Token storage is CWE-312 → F-008. |
Content-Length |
CANDIDATE → F-003 (/submit,/upload), F-005 (/answer). |
primary payload |
SAFE — redacted/secret-scrubbed before store; no injection sink. |
metadata object (assorted string fields) |
SAFE — wrapped/redacted context; no sink. |
classifier url field |
SAFE — classification only (allow-by-default noted as a code smell → see DiD). |
option selector |
SAFE — not a sink. |
base64 attachment |
CANDIDATE → F-004. |
reply_to key |
CANDIDATE → F-001. |
answer text body |
SAFE — abuse is via the reply_to binding, not the text content. |
path id {id} (GET routes) |
SAFE — sanitizer re.sub(r"[^a-z0-9\-]","",id) blocks traversal; cross-principal IDOR not expressible (single shared token). |
| second-order ledger / store reads | SAFE — own writes; torn-line self-heals. |
| CLI flags | DESIGN-INTENT — local operator only; not a data sink. |
All inputs resolved. Findings not tied to a single input (concurrency, global
rate-limit, zero-input /health) are captured as F-002 / F-006 / F-007.
eval / exec / subprocess / pickle / yaml.load / os.system in production paths./health route enforces a constant-time token compare; no
skip-auth branch exists.| Root cause | Instances found | Mitigated (=finding) | Remaining |
|---|---|---|---|
Unbounded rfile.read(Content-Length) |
3 | 3 (F-003 ×2, F-005) | 0 |
Unbounded b64decode → disk |
1 | 1 (F-004) | 0 |
Lock-free next_id() TOCTOU |
3 | 3 (F-002) | 0 |
| Global rate-limit budget | 1 | 1 (F-006) | 0 |
| Pre-auth state-changing endpoint | 6 routes | 5 auth-gated + 1 (F-007) | 0 |
| Secret at rest / stdout | 3 | 1 (F-008) + 1 dev-only excluded | 0 |
| Injection sinks | 0 | 0 | 0 |
| Path traversal | 2 | 2 (sanitized) | 0 |
Every root cause maps 1:1 onto the findings — no orphaned instances.
| Pattern | Why it's not a finding today | Risk if conditions change | Recommendation |
|---|---|---|---|
| Content classifier uses an allow-by-default keyword blocklist | no attacker-controlled sink reached — a policy completeness gap, not an injection | a sensitive site whose URL lacks a listed keyword gets its body egressed | move to allowlist / content-based classification; anchor consistently |
Origin check via startswith(...) prefix match |
only reflects CORS header; token still gates access | if reflection were ever used as an access decision, a look-alike origin would match | use exact-origin equality / parse the host |
| External redaction module fails open (returns text unchanged on error) | out of scan scope; not attacker-controlled | if the module is missing/broken, redaction silently degrades to passthrough | fail-closed (withhold) when it can't load; log the degradation |
Two hypotheses reached the adversarial step and were refuted — reported here on purpose, because what a scanner discards is as diagnostic as what it keeps:
A tightly-scoped local service with a single real design defect (F-001) and a cluster of routine hardening gaps — 1 Medium, 7 Low, zero inflation, two false positives caught before they reached the report. That is what a verified deliverable looks like.
Redacted sample · attack-first review · read-only mode · maker ≠ grader.
The report above is a snapshot of the run on 2026-07-19 and is not rewritten. This is the addendum: what was changed, when, and how the change was checked. Verification was done by a pass written against each fix — maker ≠ grader applies to the repair too.
| ID | Closed | What changed | How it was re-tested |
|---|---|---|---|
| F-001 | 2026-07-19 | Bound the reply key to an existing, still-open request before appending; double-answers refused. | Re-tested the same day: the spoof path returns 404 and appends nothing; a pending request survives the cleanup sweep. |
| F-002 | 2026-08-28 | Id allocation serialised under a lock with an in-process high-water mark. | 40 concurrent allocations produced 40 distinct ids. |
| F-003 | 2026-08-28 | One shared capped body reader for every POST route — absent, negative and oversize Content-Length all refused before allocation. | 99 MB declared → 413; negative → 400; header absent → 400. |
| F-004 | 2026-08-28 | Cap enforced on the DECODED attachment length, before the disk write. | Oversize image → 413 with zero files written to the store. |
| F-005 | 2026-08-28 | Same shared reader — the fix went in at the root, not three times at three handlers. | 99 MB declared on the third route → 413. |
| F-006 | 2026-08-28 | Rate limit keyed per caller class instead of one process-global list. | An unauthenticated flood no longer starves the authenticated caller (200); restoring the single global bucket reproduces the 429. |
| F-007 | 2026-08-28 | The liveness route is a pure read; the sweep runs at startup and on the authenticated route only. | A scrub-eligible transient survives an unauthenticated liveness call, and is still removed by an explicit sweep. |
| F-008 | 2026-08-28 | Credential masked in console output (printing it is now opt-in); a real Windows ACL is set instead of a POSIX permission call that does nothing there. | Source check plus the ACL call on the token file. |
One line per finding from the Summary table — ID|CWE|Severity|Status, exactly as printed. One line per row of Remediation — ID|fixed|date. One line run|2026-07-19. All lines sorted alphabetically together, joined with a newline (no trailing newline), SHA-256. Every input is printed on this page.
Supersedes 414329d8ff0b6e68a173a6d4d1383fba… — the earlier seal over the finding set alone, before the remediation addendum existed. A document that changes should say so with a chain, not quietly.
What this seal does not do. It does not attest the prose, the fixes, or when this page was published, and it is not a timestamp authority. It fixes the finding set — identifiers, CWEs, severities and dispositions — so a later version of this page cannot quietly gain a finding, lose one, or downgrade a severity without the digest changing. That is the whole claim.