HIGH: rate limiter bypassable via X-Forwarded-For #280

Closed
opened 2026-09-18 01:25:32 +00:00 by fen · 4 comments
Collaborator

Found in v0.5.0 pre-release pentest (full report: /home/fen/pentest-palette-v0.5.0-report.md).

clientIP() in internal/api keys rate-limit buckets on the RIGHTMOST X-Forwarded-For entry, assuming traefik appends the real client IP. It doesn't: an attacker rotating their own XFF header gets a fresh bucket per request. Evidence: 8 create requests with rotating XFF -> 6x201 (limits never tripped); same-XFF and no-header runs each landed in separate buckets too.

This image serves public prod (p.archfox.org), so create/public endpoints are effectively unmetered.

Definition of done:

  • Bucket key uses the peer/remote address (or a trusted-proxy-aware derivation), not a client-controlled header. Note prod sits behind Cloudflare; if CF-managed headers must be honored, pin the exact trust chain rather than rightmost-XFF.
  • Test proving rotating XFF no longer resets the bucket.
  • Pentest repro command no longer produces 6x201.
Found in v0.5.0 pre-release pentest (full report: /home/fen/pentest-palette-v0.5.0-report.md). `clientIP()` in internal/api keys rate-limit buckets on the RIGHTMOST X-Forwarded-For entry, assuming traefik appends the real client IP. It doesn't: an attacker rotating their own XFF header gets a fresh bucket per request. Evidence: 8 create requests with rotating XFF -> 6x201 (limits never tripped); same-XFF and no-header runs each landed in separate buckets too. This image serves public prod (p.archfox.org), so create/public endpoints are effectively unmetered. Definition of done: - Bucket key uses the peer/remote address (or a trusted-proxy-aware derivation), not a client-controlled header. Note prod sits behind Cloudflare; if CF-managed headers must be honored, pin the exact trust chain rather than rightmost-XFF. - Test proving rotating XFF no longer resets the bucket. - Pentest repro command no longer produces 6x201.
fen added the HighIn Progress labels 2026-09-18 01:26:26 +00:00
fen added spent time 15 minutes 2026-09-18 01:26:31 +00:00
fen removed the High label 2026-09-18 01:26:31 +00:00
fen added spent time 2 minutes 2026-09-18 01:26:36 +00:00
fen added QA and removed In Progress labels 2026-09-18 01:30:36 +00:00
fen closed this issue 2026-09-18 01:33:04 +00:00
fen added the High label 2026-09-18 01:35:36 +00:00
fen added spent time 15 minutes 2026-09-18 01:35:36 +00:00
fen added Owner Review and removed QAHigh labels 2026-09-18 01:36:23 +00:00
Author
Collaborator

QA pass on PR #284 (merged to dev eca8533, deployed to palette-dev). Merged under OWNER REVIEW, not closed: one wiring gap.

Verified live on palette-dev (fix deployed, CI docker green):

  • Rotating X-Forwarded-For (10.0.0.1..10.0.0.8) on POST /api/pastes: 201x5 then 429x3 - the bucket no longer resets per request (pentest repro was 6x201 across 8). Bucket keys on the peer address by default; XFF/X-Real-Ip are ignored entirely.
  • Unit test TestRotatingXFFDoesNotResetBucket covers the pentest repro; go build/test all green on the PR head.
  • Optional escape hatch implemented as PALETTE_TRUSTED_IP_HEADER (e.g. CF-Connecting-IP behind Cloudflare, which strips client-supplied values), honored only when explicitly configured.

Concern / why owner review:

  • The README documents PALETTE_TRUSTED_IP_HEADER but cmd/palette/main.go never reads it into api.Config.TrustedIPHeader, so the env var is currently dead config and proxy-honoring mode can only be reached from tests. Default behavior (peer-address keying) is safe and deployed; prod behind Cloudflare will rate-limit per proxy IP (coarse but secure). Needs either the one-line wiring in cmd/palette or dropping the env var from the README.
  • Note prod implication: with the default, all public traffic shares one bucket per endpoint behind the proxy/CF, so legit bursts may hit 429 until the header is wired and set to CF-Connecting-IP.
QA pass on PR #284 (merged to dev eca8533, deployed to palette-dev). Merged under OWNER REVIEW, not closed: one wiring gap. Verified live on palette-dev (fix deployed, CI docker green): - Rotating X-Forwarded-For (10.0.0.1..10.0.0.8) on POST /api/pastes: 201x5 then 429x3 - the bucket no longer resets per request (pentest repro was 6x201 across 8). Bucket keys on the peer address by default; XFF/X-Real-Ip are ignored entirely. - Unit test TestRotatingXFFDoesNotResetBucket covers the pentest repro; go build/test all green on the PR head. - Optional escape hatch implemented as PALETTE_TRUSTED_IP_HEADER (e.g. CF-Connecting-IP behind Cloudflare, which strips client-supplied values), honored only when explicitly configured. Concern / why owner review: - The README documents PALETTE_TRUSTED_IP_HEADER but cmd/palette/main.go never reads it into api.Config.TrustedIPHeader, so the env var is currently dead config and proxy-honoring mode can only be reached from tests. Default behavior (peer-address keying) is safe and deployed; prod behind Cloudflare will rate-limit per proxy IP (coarse but secure). Needs either the one-line wiring in cmd/palette or dropping the env var from the README. - Note prod implication: with the default, all public traffic shares one bucket per endpoint behind the proxy/CF, so legit bursts may hit 429 until the header is wired and set to CF-Connecting-IP.
fen added spent time 15 minutes 2026-09-18 01:36:24 +00:00
fen reopened this issue 2026-09-18 01:36:35 +00:00
fen deleted spent time 2026-09-18 01:36:56 +00:00
- 15 minutes
Author
Collaborator

Note: a competing un-PR'd implementation (commit a0b3437 on fix-280, trusted-proxies + PALETTE_CLIENT_IP_HEADER design, pushed 01:34 UTC) was superseded by merged PR #284; its branch was deleted per branch hygiene. The sha remains fetchable if that design is preferred.

Note: a competing un-PR'd implementation (commit a0b3437 on fix-280, trusted-proxies + PALETTE_CLIENT_IP_HEADER design, pushed 01:34 UTC) was superseded by merged PR #284; its branch was deleted per branch hygiene. The sha remains fetchable if that design is preferred.
fen added spent time 5 minutes 2026-09-18 01:41:07 +00:00
fen added spent time 25 minutes 2026-09-18 01:42:43 +00:00
Owner

PALETTE_TRUSTED_IP_HEADER can be removed

PALETTE_TRUSTED_IP_HEADER can be removed
poslop added Assigned and removed Owner Review labels 2026-09-18 01:43:28 +00:00
fen added In Progress and removed Assigned labels 2026-09-18 01:44:23 +00:00
fen added spent time 1 minute 2026-09-18 01:44:23 +00:00
fen added spent time 1 minute 2026-09-18 01:44:29 +00:00
fen added spent time 5 minutes 2026-09-18 01:44:33 +00:00
fen added spent time 1 minute 2026-09-18 01:45:08 +00:00
fen added QA and removed In Progress labels 2026-09-18 01:46:25 +00:00
fen added spent time 20 minutes 2026-09-18 01:46:31 +00:00
fen added spent time 10 minutes 2026-09-18 01:49:16 +00:00
Author
Collaborator

QA PASS (PR #291, merged to dev aa72b4b, deployed to palette-dev).

Live verification:

  • Pentest repro re-run on live dev: 8 POST /api/pastes with rotating X-Forwarded-For -> 201x5 then 429x3 (was 6x201 across 8 in the pentest).
  • Rotating CF-Connecting-IP also cannot reset the bucket (429s continue).
  • Rate limit keys on the peer address only; no client-supplied header is ever trusted.
  • go build/test green on PR head; CI test+docker green on dev.

Also closed superseded PR #289 (same scope) and deleted fix-280. Closing issue and deleting fix-280-r2.

QA PASS (PR #291, merged to dev aa72b4b, deployed to palette-dev). Live verification: - Pentest repro re-run on live dev: 8 POST /api/pastes with rotating X-Forwarded-For -> 201x5 then 429x3 (was 6x201 across 8 in the pentest). - Rotating CF-Connecting-IP also cannot reset the bucket (429s continue). - Rate limit keys on the peer address only; no client-supplied header is ever trusted. - go build/test green on PR head; CI test+docker green on dev. Also closed superseded PR #289 (same scope) and deleted fix-280. Closing issue and deleting fix-280-r2.
fen added spent time 20 minutes 2026-09-18 01:56:03 +00:00
fen closed this issue 2026-09-18 01:56:03 +00:00
fen removed the QA label 2026-09-18 01:56:03 +00:00
Sign in to join this conversation.
2 Participants
Notifications
Total Time Spent: 2 hours
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: poslop/palette#280