From 5262033ec5ef128f017679196724f94eb200af9c Mon Sep 17 00:00:00 2001 From: fen Date: Thu, 10 Sep 2026 11:12:34 -0500 Subject: [PATCH] security: document rate limiter IP-keying, anti-spoofing verification, shared-bucket tradeoff (#144) --- security.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 security.md diff --git a/security.md b/security.md new file mode 100644 index 0000000..f3e0327 --- /dev/null +++ b/security.md @@ -0,0 +1,21 @@ +# Security Notes + +Notes from the exploratory security pass on palette-dev (2026-09-09). No issues remain open from these findings; this page records the verified behavior and known tradeoffs. + +## Rate limiting + +### Bucket key and header spoofing (verified, #144) + +The create rate limiter is keyed on `clientIP`, which takes the **rightmost** entry in `X-Forwarded-For` (the address of the immediate peer as seen by the trusted proxy layer). This means: + +- Spoofed `X-Forwarded-For`, `X-Real-Ip`, and `True-Client-Ip` headers **cannot bypass or poison the bucket**. +- Verified on palette-dev: six rapid `POST /api/pastes` from one IP gave 201 x5 then 429; repeating while sending `X-Forwarded-For: 1.2.3.4`, `X-Real-Ip: 9.9.9.9`, and `True-Client-Ip: 3.3.3.3` still returned 429 for every request (no bypass). + +### Create budget + +- Budget per IP: **burst of 5** creates, refilled at **1 request/second**. +- Sixth rapid create from the same IP gets `429 Too Many Requests`. + +### Known inherent limitation: shared egress IP + +The limiter is keyed solely by IP, so clients behind a single egress address — an office NAT, a VPN exit node, a CGNAT carrier — share **one** create bucket. One heavy user can consume the burst-5 budget and cause `429` responses for everyone behind that IP. This is an inherent tradeoff of IP-keyed limiting, accepted for now; alternatives (auth-scoped or cookie-scoped buckets) would be a design change and have not been scheduled.