feat(pagination): PUBLIC_BASE_URL pin to defend Link header against host injection#136
Merged
Merged
Conversation
…ost injection
`buildLinkHeader` used `req.protocol + req.get('host')` to build the
absolute URLs that go into the RFC 5988 `Link` header. The Host
header is client-controlled, so any deployment that doesn't already
filter unknown Host values upstream (raw `docker-compose up`, behind
a tolerant NAT, sidecar-style local exposure) can be fed a malicious
Host and reflect it back into the next/prev/first/last URLs.
Paginating clients following those URLs would then hit the attacker.
Add an opt-in `PUBLIC_BASE_URL` env var. When set, the helper uses
it as the canonical `scheme://host` base; when unset, fall back to
the existing req-derived behavior. Trailing slashes are stripped so
operators who include them don't end up with `//path` in the
emitted links. Whitespace-only values are treated as unset.
Existing callers see no behavior change unless they opt in; the
TLS-fronted compose deployment (Caddy with `TLS_DOMAIN` pinned)
remains the recommended config and is already safe without setting
the env var, since Caddy rejects mismatched Host values upstream.
Three new tests cover: set value pins the host (malicious Host
ignored); trailing-slash normalization; whitespace-only falls back
to default. README env table + .env.example document the new var.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Closes #135.
Summary
buildLinkHeaderusedreq.protocol + req.get('host')to build the absolute URLs in the RFC 5988Linkheader. TheHostheader is client-controlled, so any deployment that doesn't already filter unknownHostvalues upstream — rawdocker-compose upwithout the TLS layer, tolerant-NAT setups, sidecar exposure — can be fedHost: evil.comand have it reflected into next/prev/first/last URLs. A paginating client following those would then walk the rest of its result set against the attacker.Adds an opt-in
PUBLIC_BASE_URLenv var that pins the canonical base. When unset, the existing req-derived behavior is preserved, so the TLS-fronted compose deployment (Caddy withTLS_DOMAINpinned) and any other Host-filtering proxy keep working with no configuration change.Trailing slashes on the value are stripped so operators who include one don't end up with
//pathin the emitted Link. Whitespace-only values are treated as unset.Test plan
npm run lint— cleannpm test— 495 passed (was 492 + 3 new), 15 skippedHostis ignored); trailing-slash normalization; whitespace-only falls back to default.env.exampleProudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/