Skip to content

fix(security): Host-header allowlist + restore dropped security headers#178

Open
aaronjmars wants to merge 1 commit into
reconurge:mainfrom
aaronjmars:security/host-header-allowlist-dns-rebinding
Open

fix(security): Host-header allowlist + restore dropped security headers#178
aaronjmars wants to merge 1 commit into
reconurge:mainfrom
aaronjmars:security/host-header-allowlist-dns-rebinding

Conversation

@aaronjmars

Copy link
Copy Markdown

Summary

Adds a Host-header allowlist to flowsint-app/nginx.conf so the frontend rejects requests carrying an unexpected Host header before they reach /api/. Without this check, browser CORS does not protect the API from DNS rebinding: after the attacker's DNS flip the browser treats the rebound request as same-origin and skips the CORS check, leaving POST /api/auth/register and any other unauthenticated route reachable from an attacker page.

Also re-declares the four add_header security headers in the two location blocks (location ~* \.(js|css|...)$ and location = /index.html) that were silently dropping them. nginx's add_header inheritance is replace-all, not merge — a location block that sets any header drops every header inherited from server. The main HTML page was shipping without X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, or Referrer-Policy.

Impact

DNS rebinding (primary). Attacker JS on evil.com makes evil.com resolve briefly to the attacker's IP, then rebinds to a victim's flowsint instance (e.g. 127.0.0.1). Browser sends Host: evil.com to flowsint; the existing server_name _; accepts any Host, and proxy_set_header Host $host; forwards the attacker-controlled value to the FastAPI upstream. The CORS middleware in flowsint-api/app/main.py does not help, because from the browser's perspective the request is same-origin (the JS and the rebound request both use evil.com).

Concrete reachable surface from an attacker page, today, with the default deploy:

  • POST /api/auth/register — open registration. Attacker can plant accounts in the victim's instance. The first registration on a fresh deploy becomes the de-facto admin.
  • POST /api/auth/token — login. Attacker can mount distributed credential-stuffing against the victim's instance from the victim's own browser.
  • GET /health — info leak (low).

JWT bearer tokens are localStorage-scoped to the legitimate origin, so authenticated endpoints don't directly leak — but that's not the boundary the deployment claims. The README's "Everything is stored on your machine" privacy posture assumes the local instance isn't drivable from arbitrary origins; this fix restores that.

Dropped security headers (secondary). With the prior config, hitting / (the SPA) returns /index.html from the location = /index.html block, which sets add_header Cache-Control "no-store, ..." and thereby drops every server-level security header. So the main HTML page goes out with no X-Frame-Options (clickjacking), no Referrer-Policy (cross-site referrer leak), and no X-Content-Type-Options (MIME confusion). Same loss applied to the static-assets location for .js/.css.

Location

  • flowsint-app/nginx.confmap $http_host $is_flowsint_host allowlist + if ($is_flowsint_host = 0) gate at the top of the server block; security headers re-declared in the static-assets and /index.html locations.
  • README.md — one paragraph under "Deploy on a network (team / server)" naming the new operator step for LAN deploys.

Fix

  • New map $http_host $is_flowsint_host block at http level. Default 0; allowlist entries for localhost, 127.0.0.1, [::1] (any port, case-insensitive), matching the documented single-user install path (http://localhost:5173/register). Two commented-out template lines show LAN operators how to add their server hostname/IP.
  • New if ($is_flowsint_host = 0) { return 403; } at the top of the server block, before any header or location processing.
  • In location ~* \.(js|css|...)$ and location = /index.html, re-declare the four add_header security headers that nginx's replace-all inheritance was dropping.
  • README: one paragraph under "Deploy on a network" telling operators to add their hostname/IP to the allowlist alongside the existing .env secret rotation step.

Verification

  • nginx -t -c flowsint-app/nginx.conf -p <prefix>/syntax is ok / test is successful.
  • 26/26 Host-header regression cases pass (allowed: localhost, localhost:5173, 127.0.0.1, LocalHost:5173, [::1], [::1]:5173, etc.; blocked: evil.com, evil.com:5173, localhost.evil.com, 127.0.0.1.attacker.com, 127-0-0-1.attacker.com, 127.0.0.1evil, empty host, 192.168.1.42 (operator must opt-in), AWS-metadata IP, etc.).
  • LAN-deploy regression: 192.168.1.42:5173 is correctly blocked under the default config — the README change tells operators to extend the allowlist (matches existing .env-rotation pattern).
  • Static assets (.js/.css) and /index.html confirmed to keep the four add_header security headers in the patched location blocks.

Detected by

Aeon + semgrep

  • Severity: medium
  • CWE-350 (Reliance on Reverse DNS Resolution for a Security-Critical Action) / CWE-352 (CSRF, the rebinding variant) / CWE-1021 (Improper Restriction of Rendered UI Layers — for the missing X-Frame-Options on /index.html).
  • semgrep rule: generic.nginx.security.request-host-used at flowsint-app/nginx.conf:69 flagged the unvalidated Host propagation; generic.nginx.security.header-redefinition at :84/:90/:102 flagged the silently dropped headers.

Filed by Aeon.

Detected by Aeon + semgrep.
Severity: medium
CWE-350 (reverse-DNS trust) / CWE-352 (CSRF via DNS rebinding) / CWE-1021 (UI layering / dropped X-Frame-Options).

- Add `map $http_host $is_flowsint_host` allowlist + default-deny `if` at
  the top of the server block. Default permits localhost / 127.0.0.1 /
  ::1 (any port, case-insensitive); two commented-out template lines show
  LAN operators how to add their server hostname/IP.
- Re-declare the four `add_header` security headers in the static-assets
  and `/index.html` location blocks, which previously dropped them due to
  nginx`s replace-all add_header inheritance.
- README: one paragraph under "Deploy on a network" naming the new
  operator step alongside the existing `.env` rotation.
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.

1 participant