harden(console): SSRF/CSRF/headers/supply-chain hardening for the credential-holding web UI (part of #369)#607
Merged
Merged
Conversation
…limit, headers)
The console is a credential-holding monitoring UI: it proxies a metrics
backend (/api/timeseries) and reaches internal cache nodes, the exact
pattern behind real-world dashboard SSRF and account-takeover CVEs. This
audits the four web surfaces and closes the gaps the audit found.
SSRF / outbound fetch (audited; boundary confirmed, then extended):
* Every outbound target (seed nodes, node admin URL, metrics base URL)
comes only from server config; no request input reaches an outbound
fetch (/api/nodes/{addr} is a topology lookup, and discovery-derived
endpoints are displayed, never dialed). The timeseries route stays an
allowlist (bare ironcache_* metric names, clamped range/step, no raw
query passthrough), and the outbound client already refuses redirects,
non-http schemes, and link-local/metadata IPs with bounded time and
response size.
* New: the outbound URLs and every seed host:port are validated at BOOT
(config::validate + httpclient::validate_url), failing fast instead of
erroring on the first query or dial.
CSRF / session / auth:
* New same-origin mutation gate: any state-changing /api/* request a
browser marks as cross-site (Sec-Fetch-Site, else Origin vs Host) is
refused 403 before the auth gate, in every posture including the
loopback dev mode, which serves mutations token-free and was otherwise
reachable by a hostile page (DNS rebinding / cross-site form POST).
Non-browser clients are unaffected; reads are not gated (no CORS
header is ever emitted, so a cross-site reader cannot read anything).
* New failed-auth fixed-window rate limit (10 failures / 60s): invalid
Bearer presentations are counted and a tripped window answers 429
before the compare; token-free requests are never throttled. The token
compare was already constant time; the session model stays cookie-free
(Bearer header + tab-scoped sessionStorage), so there is no ambient
credential for a cross-site request to ride.
Headers / browser surface:
* /api/* responses now carry a deny-all CSP (default-src 'none';
frame-ancestors 'none'), X-Frame-Options: DENY, and Referrer-Policy:
no-referrer on top of the existing nosniff + Cache-Control: no-store.
The UI's strict self-only CSP (no unsafe-inline) was already in place
and is asserted end to end.
Supply chain / docs:
* Verified the frontend is fully embedded (no CDN/external script,
style, font, image, or fetch) and locked that in with a test; no new
dependencies anywhere.
* SECURITY.md gains a web-surface hardening section: the gates above,
the egress allowlist, the cookie-free session model, and the general
deployment posture (private network, VPN-restricted ingress, TLS at
the edge, least-privilege monitor credential on the nodes).
Tests: full header set on / and /api/* (unit + over TCP), allowlist
rejection of request-supplied URLs/hosts/metrics, redirect refusal
(existing live-socket test), cross-site-rejected / same-origin-accepted
mutations over TCP, rate-limit 401-then-429 over TCP, plus unit coverage
for the gate rules, the limiter window, and the boot-time validators.
Part of #369.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFWCZR47TDvsFPJ1waX85X
Signed-off-by: Zeke <ezequiel.lares@outlook.com>
perf-gate (A5)Same-runner ratchet of HEAD against the merge-base (both rebuilt and measured in this job).
Overall: WARN
|
This was referenced Jul 10, 2026
ELares
added a commit
that referenced
this pull request
Jul 10, 2026
…TOR, ACL) (#610) A doc-audit found stale claims. Each correction below uses only measured numbers (c7g, 2026-07-10): - README.md (perf/competitive): reframed from "Dragonfly leads GET / close the remaining gap" to the corrected thread-per-core re-bench. IronCache now LEADS GET by about 19% single-endpoint (2.43M vs Dragonfly 2.04M ops/sec) and by roughly 2x cluster-aware (4.32M vs 2.19M) once #517 zero-hop routing removes the cross-shard hop; the old "leads both" was a benchmark CONFIG artifact (shards oversubscribed relative to cores). Kept honest nuance: baseline p99.9 TIES Dragonfly (~15ms), the durable-save tail (291ms) is competitive not category-leading, and the decisive edges are GET throughput, memory, and determinism. Fixed the two-vCPU section's stale "Dragonfly pulls ahead at real core count" reading. - docs/PRODUCTION_READINESS.md: goal-completion verdict is now MET on GET (#507 CLOSED, #517 hop-elimination paid off) instead of "unproven pending a re-bench"; moved console web-surface hardening (#369, PR #607) from the "remain" list to "landed"; removed the stale within-3%/re-bench finish-line items; fixed the false "README.md:143 claims MONITOR redaction" citation in items 27 (README documents SLOWLOG/INFO/logs redaction, and MONITOR is intentionally unimplemented, so there is no doc mismatch). - docs/bench/TAIL_LATENCY.md: corrected the plain-GET "can trail Dragonfly" framing to "now LEADS Dragonfly under a proper thread-per-core config"; the durable-load tail nuance (already honest) is unchanged. - docs/design/ACL.md: documented per-subcommand ACL grants (Redis 7 `+cmd|sub`), specifically `+slowlog|get` for the least-privilege monitor user (read-only SLOWLOG without the `SLOWLOG RESET` wipe), and clarified that the CLUSTER_CONTRACT.md "no per-subcommand carve-out" note is narrow. - docs/research/dragonfly.md: reworded one phrase to remove a stale-deficit-grep false positive. - CHANGELOG.md: annotated the historical 2026-07-03 benchmark entry with the corrective re-bench note (record preserved). Not changed (already current): docs/design/CONFIG.md and docs/design/PERSISTENCE.md already point the save-throttle (#577) warning at the per-slot Arc-COW (#588) fix that cut the snapshot tail 3.5s -> 291ms (11.5x). Docs currency sweep (post competitive re-bench + console completion). Signed-off-by: Zeke <ezequiel.lares@outlook.com> Co-authored-by: Claude Fable 5 <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.
What
Hardens the
ironcache-consoleweb surface. The console is a monitoring UI that holds a privileged cache credential, proxies a metrics backend, and reaches internal nodes -- the exact pattern behind real monitoring-UI SSRF + account-takeover CVEs. Audited four surfaces; fixed the genuine gaps (much was already safe, verified line-by-line).1. SSRF / outbound fetch. Already safe (outbound URLs are server-config/discovery only; the
/api/timeseriesproxy allowlists the metric to a bareironcache_*name, rejects PromQL/URL smuggling, builds+encodes the query itself and clamps range/step/points; the client refuses redirects + non-http schemes + link-local/metadata IPs incl. IPv4-mapped-IPv6, with size+time caps). FIXED: outbound URLs + seedhost:portare now validated at BOOT (fail-fast) rather than first-use.2. CSRF / session / auth. Session is cookie-free (per-request bearer in tab-scoped storage). FIXED: a same-origin mutation gate (
Sec-Fetch-Sitefirst, elseOriginauthority must equalHost) runs before auth on EVERY non-GET route, closing the loopback-dev token-free mutation vector (DNS-rebinding / cross-site form POST); non-browser clients unaffected, reads not gated. FIXED: a fail-closed login rate limiter (10 failures / 60s) answering 429 before any compare. Constant-time compare + token zeroization/redaction were already present.3. Security headers.
/api/*responses (every outcome incl. 401/403/405/429) now carryCSP default-src 'none'; frame-ancestors 'none',X-Frame-Options: DENY,Referrer-Policy: no-referrer,nosniff,Cache-Control: no-store. The UI already carried a strict self-only CSP.4. Supply chain. Verified fully-embedded frontend (zero external loads), locked in by a
ui_loads_no_external_resourcestest. Zero new dependencies. Extended the console SECURITY.md with a "Web-surface hardening" section (general deployment posture: private network, VPN-restricted ingress, TLS at the edge, least-privilege monitor ACL user).Review + verification
Adversarially security-reviewed: NO holes found across all five threat classes (SSRF structurally unbypassable, CSRF gate airtight before auth, constant-time + rate-limited auth with no bypass route, universal headers with no reflected-input injection, no request-reachable panic). Two documented tradeoffs (global-not-per-IP rate limiter = operator-lockout-under-attack, mitigated by VPN posture; prefix-not-enumerated metric allowlist, sound against injection) are called out in SECURITY.md, not holes.
Tests: security-headers (unit + over-TCP), timeseries allowlist rejection of request-supplied url/host/metric, outbound redirect refusal, CSRF cross-site-403/same-origin-accept, login rate-limit 401-then-429, embedded-assets no-external-load; 311 console tests pass; clippy
-D warnings+ prior-art + fmt clean; no dashes; no company identifiers.Part of #369.
🤖 Generated with Claude Code