Skip to content

Security: harden /verdict/:tx/payload auth (replay, domain binding, header format) #9

Description

@A1igator

Context

app.get(\"/verdict/:transaction/payload\", ...) in x402r-ai-garbage-detector/src/arbiter.ts:237-271 gates the stored response body behind a payer signature. The scheme as written has three defensible but non-production-grade weaknesses. Threat surface is narrow today (the payer already received the content at payment time, so replay mostly buys "re-fetching what you could have cached"), but the scheme wouldn't pass a review as-is, and the pattern is likely to get copy-pasted into arbiters that gate more sensitive content.

Issues

1. No replay protection / no freshness

Signed message is literally x402r:payload:{txHash}. That signature is valid forever. Any leak of the Authorization header (browser devtools, server logs, HTTP proxy, exfiltrated backup) grants indefinite re-request access. No nonce, no timestamp, no rotation path.

2. No domain binding

The message excludes chain ID, arbiter address, and endpoint path. A signature made for arbiter A's endpoint would verify against arbiter B's if B copies the same format. A rogue arbiter could passively observe a payer signature in logs and then use it against its own copy of the same code.

3. Non-standard Authorization header

Bare hex, no scheme prefix — no Bearer, no Sig. HTTP RFC 7235 expects Authorization: <scheme> <credentials>. A caller who sends Authorization: Bearer 0xabc... (the RFC-conformant form) will have the Bearer prefix included in signature, verifyMessage throws, the catch returns 403 \"Invalid signature\" with no guidance. A caller who follows the README sends bare hex, which works but isn't standards-compliant.

Severity

Not broken-exploitable today. Becomes real when:

  • (a) VERDICTS_DIR persists across more than the current in-memory lifetime (e.g., mounted volume),
  • (b) this auth pattern is reused for content more sensitive than a weather blurb,
  • (c) log pipelines retain Authorization headers (common default in many observability stacks).

Proposed fix (~2-3 hours + tests)

Message format

Replace x402r:payload:{txHash} with a structured, versioned, domain-bound message:

x402r:payload:v1
chain: <chainId>
arbiter: <arbiter-address>
tx: <txHash>
created: <unix-seconds>

Server-side checks

  • Enforce abs(now - created) < 300 seconds (±5 min skew tolerance; reject if outside window).
  • Verify chain matches a supported chain, arbiter matches clients.account.address, tx matches req.params.transaction.
  • Return granular error codes so callers can distinguish the failure mode:
    • 401 no-auth — missing Authorization header
    • 401 malformed — wrong scheme prefix, bad hex, unparseable message
    • 401 stalecreated outside freshness window
    • 401 wrong-domain — chain/arbiter/tx mismatch in message
    • 403 wrong-signer — signature valid but recovered address ≠ stored.payer

Header format

Switch to an RFC-conformant custom scheme:

Authorization: X402rSig <hex>

Server parses Authorization: X402rSig <hex>, strips prefix before verifyMessage.

Acceptance criteria

  • Message format includes chain, arbiter, tx, and a created timestamp
  • Freshness window enforced server-side
  • Auth header uses a named scheme prefix
  • All four failure modes return distinct, actionable error responses
  • README comment in arbiter.ts updated to reflect the new scheme
  • Blog post x402-frontend/apps/web/content/blog/open-sourcing-error-detector-arbiter.md:187 updated to document the new format
  • Tests covering: missing header, malformed header, stale created, wrong chain, wrong arbiter, wrong tx, wrong signer, happy path

Out of scope

  • Rate-limiting on /verify (separate concern — inference cost exposure)
  • CORS posture (app.use(cors()) is wide-open by design for a public arbiter)
  • Moving to EIP-712 typed data (cleaner UX in wallets but a bigger client-side change; reconsider if this pattern gets used for high-value content)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions