Skip to content

fix(security): sanitize unmapped errors + stop example stream leaks#124

Open
gmoon wants to merge 1 commit into
masterfrom
fix/sanitize-errors-and-stream-leaks
Open

fix(security): sanitize unmapped errors + stop example stream leaks#124
gmoon wants to merge 1 commit into
masterfrom
fix/sanitize-errors-and-stream-leaks

Conversation

@gmoon

@gmoon gmoon commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Two hardening fixes from a security review of the S3-fronting proxy. Scoped to the two highest-value findings; validated locally (lint + type-check + build + 49/49 unit tests green).

1. Error-detail leakage (MEDIUM) — src/s3-gateway.ts

mapError wrapped only NoSuchKey/NoSuchBucket→404, AccessDenied→403, InvalidRange→416 and rethrew everything else unchanged. Unmapped S3ServiceExceptions carry sensitive detail:

  • PermanentRedirect → discloses the bucket's real region/endpoint
  • SlowDown/InternalError → carry $metadata.requestId and often the bucket name

…and the examples render err.message straight into the response body, so a client can force these conditions and harvest AWS internals.

Fix: unmapped errors are wrapped in a generic S3ProxyError(500); the original is preserved on cause for server-side logging only. Applies to both fetch() and healthCheck() — the latter is client-reachable via the examples' /health route, so narrowing to fetch() alone would leave /health leaking.

Important

Behavior change: send()/init()/fetch() now reject with a sanitized S3ProxyError(500) for previously-unclassified errors instead of the raw error. Inspect err.cause for the original. The known mappings (404/403/416) are unchanged. Unit tests updated to assert the new contract (sanitized message + preserved cause).

2. Stream/socket leak in examples (MEDIUM) — examples/{http,express-basic,fastify-basic}.ts

.pipe(res) does not destroy the S3 source stream when the client disconnects mid-transfer, leaking the underlying S3 socket under abusive clients (open a range request, disconnect, repeat). Switched to stream.pipeline(), which destroys the source on client abort or write error. (hono-basic.ts already handles this via Readable.toWeb cancellation.)

Deliberately not included (tracked separately)

Test plan

  • npm run lint / type-check / build / test:unit (49/49) green locally
  • CI green
  • Confirm the error-contract change is acceptable for downstream consumers

…ples

Two hardening fixes from a security review of the S3-fronting proxy.

1) Error-detail leakage (src/s3-gateway.ts). mapError only wrapped
   NoSuchKey/NoSuchBucket -> 404, AccessDenied -> 403, InvalidRange -> 416
   and rethrew everything else unchanged. Unmapped S3ServiceExceptions carry
   sensitive detail — PermanentRedirect discloses the bucket's region/endpoint,
   SlowDown/InternalError carry $metadata.requestId and often the bucket name —
   and the examples render err.message straight into the response body, so a
   client could probe to harvest AWS internals. Unmapped errors are now wrapped
   in a generic S3ProxyError(500); the original is preserved on `cause` for
   server-side logging only. This applies to both fetch() and healthCheck()
   (the latter is client-reachable via the examples' /health route).

   BEHAVIOR CHANGE: send()/init()/fetch() now reject with a sanitized
   S3ProxyError(500) for previously-unclassified errors instead of the raw
   error. Inspect `err.cause` for the original. Tests updated to assert the
   new contract (sanitized message + preserved cause).

2) Stream/socket leak in examples (examples/http.ts, express-basic.ts,
   fastify-basic.ts). `.pipe(res)` doesn't destroy the S3 source stream when
   the client disconnects mid-transfer, leaking the underlying S3 socket under
   abusive clients. Switched to stream.pipeline(), which destroys the source on
   client abort or write error.

Not included (tracked separately): optional keyPrefix confinement, nosniff
header control, 403/404 enumeration hardening, init() double-signaling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant