Skip to content

feat(backend): implement payload sanitization and strict validation f… - #1686

Merged
emdevelopa merged 1 commit into
emdevelopa:mainfrom
Thosine-01:feature/be-implement-payload-sanitization-and-strict-validation-for-api-gateway-signature-verifier
Oct 24, 2026
Merged

emdevelopa merged 1 commit into
emdevelopa:mainfrom
Thosine-01:feature/be-implement-payload-sanitization-and-strict-validation-for-api-gateway-signature-verifier

Conversation

@Thosine-01

@Thosine-01 Thosine-01 commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #1487

Adds strict input validation and log sanitization to the API Gateway Signature Verifier, and fixes two fail-open bugs that could weaken signature verification.

The signature is an HMAC over the exact request body, so the verifier never rewrites the body. Input that can't be signed unambiguously is rejected, and sanitization is applied to what gets logged.

Security fixes

1. Invalid tolerance config silently disabled the timestamp window (fail-open)

toleranceSeconds = Number(process.env.API_GATEWAY_SIGNATURE_TOLERANCE_SECONDS || 300): any non-numeric value (e.g. a typo such as 300s) produced NaN. Since deltaSeconds > NaN is always false, requests with any timestamp, even years old, passed the window check.

Fix: resolveSignatureToleranceSeconds() falls back to 300 for non-numeric, NaN, zero or negative values and caps values above 3600, logging a warning in both cases. auth.js and the Redis replay reservation use the same resolver. (Before, the reservation would compute EX: NaN and fail.)

2. Unserializable bodies were signed as an empty body (fail-open)

safeJsonStringify returned "" when serialization failed (circular references, BigInt), so such a body was hashed the same as an empty request and shared its signature.

Fix: serialization now throws, and signing and verification fail closed.

Strict validation (new api-gateway-payload-validation.js)

Runs after the circuit-breaker, rate-limit and secret checks, before any hashing.

Input Rule Code
Method Allowlist: GET HEAD POST PUT PATCH DELETE OPTIONS API_GATEWAY_INVALID_REQUEST
Path Starts with /, ≤ 2048 chars, no control characters (blocks CR/LF injection into the newline-delimited canonical payload METHOD\nPATH\nTS\nBODYHASH) API_GATEWAY_INVALID_REQUEST
x-api-timestamp String or number, ≤ 12 chars, checked before regex/parsing API_GATEWAY_INVALID_REQUEST
x-api-signature String, ≤ 128 chars, checked before regex API_GATEWAY_INVALID_REQUEST
Body JSON-safe values only; no __proto__ / constructor / prototype keys; depth ≤ 20; ≤ 10,000 nodes API_GATEWAY_INVALID_PAYLOAD
Secret Must be a string ≥ 16 chars (arrays previously passed the .length check) existing reason

Why non-JSON values are rejected: JSON.stringify rewrites them silently. NaN/Infinity become null and undefined properties are dropped, so { amount: NaN } and { amount: null } would produce the same signature. Body validation is iterative rather than recursive, so a hostile, deeply nested body can't overflow the stack.

Rejections keep the existing 401 { error, code, reason } response shape.

Hardening

  • Log sanitization: sanitizeForLog() is applied to every attacker-controlled value the verifier logs (client IP, method, path, timestamp and signature headers). Control characters become ?, so CR/LF can't forge log lines. Values are truncated to 80 chars, and objects and arrays are never dumped.
  • Key rotation: MAX_KEY_ROTATION_DEPTH (current + previous) was declared but never enforced. Extra keys are now ignored with a warning, and non-string entries are skipped.
  • signApiGatewayRequest applies the same body validation and returns null for invalid bodies.

Files

  • backend/src/lib/api-gateway-payload-validation.js: new, the validation, tolerance and log-sanitization helpers
  • backend/src/lib/api-gateway-payload-validation.test.js: new, 54 tests
  • backend/src/lib/api-gateway-signature.js: wires in validation, fail-closed serialization, sanitized logging, rotation depth
  • backend/src/lib/api-gateway-signature.test.js: 13 new integration tests
  • backend/src/lib/auth.js: uses the shared tolerance resolver
  • backend/docs/API_GATEWAY_SIGNATURE_VALIDATION.md: new, documents rules, error codes and config

Test output

$ npx vitest run src/lib/api-gateway-payload-validation.test.js src/lib/api-gateway-signature.test.js src/lib/auth.test.js
 Test Files  3 passed (3)
      Tests  112 passed (112)

$ npx vitest run tests/e2e/api-gateway-security.e2e.test.js
 Test Files  1 passed (1)
      Tests  15 passed (15)

Regression check: every test file that imports the changed modules (via vitest related) was also run against both main and this branch. 9 route and e2e files fail identically on both (65 failed / 3 passed), and no test fails only on this branch. Those are existing failures on main, unrelated to this change. The full backend suite also has existing failures in other modules (fraud detection, Horizon poller, idempotency, load tests).

Security notes

  • Behaviour change for misconfigured deployments: if API_GATEWAY_SIGNATURE_TOLERANCE_SECONDS is currently set to a non-numeric value, the timestamp window is effectively disabled today. After this PR it becomes 300s, so stale requests that were accepted will now be rejected. A configured value above 3600s is capped.
  • Behaviour change for programmatic callers: bodies containing NaN, undefined, Date, Buffer and similar can no longer be signed or verified. Bodies parsed from JSON over HTTP never contain these, so normal API clients are unaffected.
  • Not changed, flagged for follow-up: client-caused failures (bad signatures, and now validation rejections) count toward the global circuit breaker (50 failures in total open it for everyone). An unauthenticated client could therefore trip the breaker and block signature verification for all merchants for 60s. This is existing behaviour and is out of scope here, but it's worth a separate issue: count only unexpected errors, or make the breaker per-client.
  • Prototype-pollution keys are rejected at the verifier, which is defence in depth. Route-level sanitizers such as merchant-payload-validation.js still apply.

🤖 Generated with Claude Code

…or API Gateway Signature Verifier

- Add api-gateway-payload-validation module:
  - validateSignatureRequestParts: allowlisted HTTP methods, path must start
    with "/", be <= 2048 chars and contain no control characters (blocks
    CR/LF injection into the newline-delimited canonical payload), and
    length-bounded timestamp/signature headers
  - validateSignatureBody: JSON-safe values only (rejects NaN/Infinity,
    undefined, functions, BigInt, Date, Buffer...), prototype-pollution keys,
    depth <= 20 and <= 10k nodes; iterative so hostile bodies cannot
    overflow the stack
  - resolveSignatureToleranceSeconds: invalid/non-positive values fall back
    to 300s and values above 3600s are capped
  - sanitizeForLog: strips control chars and truncates attacker-controlled
    values before logging
- Verifier: run strict validation before hashing, require string secrets,
  sanitize all logged request values, enforce MAX_KEY_ROTATION_DEPTH and
  skip non-string rotation keys
- Fix: a non-numeric API_GATEWAY_SIGNATURE_TOLERANCE_SECONDS produced NaN,
  which silently disabled the timestamp window check
- Fix: unserializable bodies were hashed as "" and so matched the signature
  of an empty request; signing/verification now fail closed
- auth.js uses the shared tolerance resolver for replay reservation
- Add tests and docs/API_GATEWAY_SIGNATURE_VALIDATION.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 22, 2026

Copy link
Copy Markdown

@Thosine-01 is attempting to deploy a commit to the Emmanuel's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Oct 22, 2026

Copy link
Copy Markdown

@Thosine-01 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@emdevelopa
emdevelopa merged commit d80b16f into emdevelopa:main Oct 24, 2026
1 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Backend] Implement payload sanitization and strict validation for API Gateway Signature Verifier

2 participants