Fix endpoint XSS & rate-limit spoofing, add PLATFORM config#32
Merged
Conversation
P1: The endpoint page rendered {{.EndpointURL}} into an Alpine @click
expression. html/template only HTML-escapes @click (not a JS context),
so a quote supplied via %27 in the path broke out of the copy('...')
string and Alpine evaluated the rest via Function() — permitted by the
CSP's 'unsafe-eval'. The copy button now reads the URL from the DOM
(x-ref) instead, and validEndpointID() rejects any :endpoint param that
isn't haikunator-shaped (lowercase words/digits, hyphen-joined, <=64)
with a 404 across all endpoint routes.
P3: Behind Cloudflare, a client-supplied X-Forwarded-For is appended to,
not stripped, so resolveClientIP and the limiter's c.IP() key both read
an attacker-controlled value — rotating XFF defeated the rate limiter
and poisoned the stored client IP. resolveClientIP now prefers the
unforgeable CF-Connecting-IP header, and the limiter buckets on the
resolved IP via an explicit KeyGenerator.
…ping httphq runs behind different providers depending on where it's self-hosted, each exposing the real client IP under its own header. The PLATFORM env var lets the operator declare their provider (cloudflare, fly, heroku, render, proxy, or direct) and httphq resolves the right client-IP strategy from it — replacing the Cloudflare-hardcoded chain. Declaring the platform also strips that provider's vendor headers (e.g. Cloudflare's Cf-*) from captured requests, so users inspect their own traffic without infrastructure noise. Cdn-Loop (RFC 8586, not vendor specific) joins the generic omittedHeaders list. TrustProxy is now conditional on a platform being set, so a directly exposed deployment doesn't trust forwarded headers. An unknown PLATFORM fails safe to direct. README documents PLATFORM, APPLICATION_ENV and LOG_LEVEL.
No functional changes — whitespace and wrapping only.
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.
Summary
Security fixes from an app-side review, plus a
PLATFORMenv var that makes httphq portable across hosting providers. Final commit is a repo-wide Prettier pass (formatting only).P1 — Reflected XSS via the endpoint path param (high)
endpoint.htmlrendered{{.EndpointURL}}into an Alpine@clickexpression. Go'shtml/templateonly treatson*attributes as JS contexts, so@clickwas merely HTML-escaped — a'supplied as%27in the path survived, broke out of thecopy('...')string, and Alpine evaluated the rest via theFunctionconstructor (permitted by the CSP's'unsafe-eval'). Any single-segment path renders the endpoint page, so the attack was a crafted link, e.g./x%27%29%3Balert(1)%3B%28%27.x-ref="endpointUrl") instead of interpolating a template value into a JS expression.validEndpointID()(regex^[a-z0-9]+(-[a-z0-9]+)*$, max 64 chars) rejects any non-haikunator:endpointparam with a404, applied acrossGET /:endpoint,Use /to/:endpoint,GET /ws/:endpoint, and the/api/endpoints/:endpoint/...routes. The charset excludes' " < > ( ) ; %, independently killing the XSS class and keeping junk out of the DB/logs.P3 — Rate-limit bypass + spoofed client IP (medium)
With
TrustProxy: true, bothresolveClientIPand the limiter's key read a client-suppliedX-Forwarded-For(Cloudflare appends to it rather than stripping it). Rotating XFF handed out a fresh rate-limit bucket per request and poisoned the stored/logged client IP. The limiter now keys on the resolved client IP;resolveClientIPis driven by thePLATFORMconfig below.PLATFORMenv varhttphq runs behind different providers depending on where it's self-hosted, each exposing the real client IP under its own header.
PLATFORMlets the operator declare their provider; httphq resolves the client-IP strategy from it.PLATFORMdirectcloudflareCF-Connecting-IPflyFly-Client-IPherokuX-Forwarded-For(leftmost)renderX-Forwarded-For(leftmost)proxyX-Forwarded-For(leftmost) — generic proxyCf-*) from captured requests, so users inspect their own traffic without infrastructure noise.Cdn-Loop(a standard CDN header) joins the genericomittedHeaderslist.TrustProxyis now conditional on a platform being set — a directly exposed deployment no longer trusts forwarded headers.PLATFORMfails safe todirect. README documentsPLATFORM,APPLICATION_ENV, andLOG_LEVEL.Trust model
Setting
PLATFORMtrusts that platform's client-IP header unconditionally — the operator must ensure inbound traffic can't bypass the platform, or a client could spoof its IP. Documented in the README.Testing
go build ./src,go vet ./src,go test ./...pass.validEndpointID,resolvePlatform, andomitHeadercovered by focused tests during development (XSS payload, platform mapping, header stripping); the repo keeps nomain-package tests so they were not committed.curl/ Playwright checks were not run locally due to a port-8080 conflict with an unrelated container; e2e uses haikunator-format IDs so endpoint validation won't reject it.