Skip to content

feat(pagination): PUBLIC_BASE_URL pin to defend Link header against host injection#136

Merged
CryptoJones merged 1 commit into
masterfrom
feat/pagination-public-base-url
May 19, 2026
Merged

feat(pagination): PUBLIC_BASE_URL pin to defend Link header against host injection#136
CryptoJones merged 1 commit into
masterfrom
feat/pagination-public-base-url

Conversation

@CryptoJones
Copy link
Copy Markdown
Owner

Closes #135.

Summary

buildLinkHeader used req.protocol + req.get('host') to build the absolute URLs in 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 without the TLS layer, tolerant-NAT setups, sidecar exposure — can be fed Host: evil.com and 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_URL env var that pins the canonical base. When unset, the existing req-derived behavior is preserved, so the TLS-fronted compose deployment (Caddy with TLS_DOMAIN pinned) 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 //path in the emitted Link. Whitespace-only values are treated as unset.

Test plan

  • npm run lint — clean
  • npm test — 495 passed (was 492 + 3 new), 15 skipped
  • Three new tests cover: set value pins the host (malicious Host is ignored); trailing-slash normalization; whitespace-only falls back to default
  • Documented in README env table and .env.example

Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/

…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>
@CryptoJones CryptoJones merged commit 077ab3c into master May 19, 2026
3 checks passed
@CryptoJones CryptoJones deleted the feat/pagination-public-base-url branch May 19, 2026 06:21
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.

security(pagination): Host header is reflected into Link header next/prev/first/last URLs

1 participant