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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions