Repository navigation
fix(security)!: sanitize unmapped S3 errors and stop mid-stream socket leaks - #124
Merged
Merged
Conversation
gmoon
force-pushed
the
fix/sanitize-errors-and-stream-leaks
branch
from
October 31, 2026 19:55
8e53ef7 to
2f0f7f6
Compare
Owner
Author
|
Rebased onto current master (was 40 commits behind) and finished:
Local: |
gmoon
force-pushed
the
fix/sanitize-errors-and-stream-leaks
branch
from
October 31, 2026 20:01
2f0f7f6 to
6c29d61
Compare
…t leaks Two hardening fixes from a security review of the S3-fronting proxy. 1. Error-detail leakage (src/s3-gateway.ts): mapError() mapped only NoSuchKey/NoSuchBucket->404, AccessDenied->403, InvalidRange->416 and rethrew everything else unchanged. Unmapped S3ServiceExceptions leak sensitive detail (PermanentRedirect -> bucket region/endpoint; SlowDown/InternalError -> requestId and often bucket name), which the examples render straight into the response body. Unmapped errors are now wrapped in a generic S3ProxyError(500); the original is preserved on `cause` for server-side logging only. 2. Mid-stream socket leak (src/index.ts writeBody, examples/http.ts, examples/fastify-basic.ts): plain stream.pipe(res) does not destroy the S3 source stream when the client disconnects mid-transfer, leaking the underlying S3 socket. Switched to stream.pipeline(), which destroys the source on client abort or write error. Rebased onto master: the leak fix moved from examples/express-basic.ts (now the v4.1 middleware() one-liner) into the shared writeBody() path, so pipe()/middleware()/static-site are all covered at the core. BREAKING CHANGE: fetch(), send(), and init() now reject with a sanitized S3ProxyError(500) for previously-unclassified errors instead of the raw S3ServiceException/AWS error. Inspect `err.cause` for the original. The known mappings (S3NotFound/404, S3Forbidden/403, S3InvalidRange/416) are unchanged. Additionally, a mid-stream S3 failure (after headers are sent) now terminates (destroys) the response rather than end()-ing it, so a truncated body surfaces as a broken connection instead of a silent short success. Consumers that branch on raw error types/messages or rely on the old end()-on-mid-stream-error behavior must update. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
gmoon
force-pushed
the
fix/sanitize-errors-and-stream-leaks
branch
from
October 31, 2026 20:10
6c29d61 to
5dad0c8
Compare
Merged
gmoon
added a commit
that referenced
this pull request
Oct 31, 2026
Major release. Breaking security hardening (#124): - fetch()/send()/init() reject with a sanitized S3ProxyError(500) for previously-unclassified errors; original on err.cause. Known 404/403/416 mappings unchanged. - Mid-stream S3 failures now terminate (destroy) the response instead of end()-ing it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.tsmapErrorwrapped only NoSuchKey/NoSuchBucket→404, AccessDenied→403, InvalidRange→416 and rethrew everything else unchanged. UnmappedS3ServiceExceptions carry sensitive detail:PermanentRedirect→ discloses the bucket's real region/endpointSlowDown/InternalError→ carry$metadata.requestIdand often the bucket name…and the examples render
err.messagestraight 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 oncausefor server-side logging only. Applies to bothfetch()andhealthCheck()— the latter is client-reachable via the examples'/healthroute, so narrowing tofetch()alone would leave/healthleaking.Important
Behavior change:
send()/init()/fetch()now reject with a sanitizedS3ProxyError(500)for previously-unclassified errors instead of the raw error. Inspecterr.causefor the original. The known mappings (404/403/416) are unchanged. Unit tests updated to assert the new contract (sanitized message + preservedcause).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 tostream.pipeline(), which destroys the source on client abort or write error. (hono-basic.tsalready handles this viaReadable.toWebcancellation.)Deliberately not included (tracked separately)
nosniff/content-type control — stored-XSS surface if the bucket accepts untrusted uploadsinit()double-signaling (emit + throw)Test plan
npm run lint/type-check/build/test:unit(49/49) green locally