mirror of
https://github.com/reconurge/flowsint.git
synced 2026-06-10 00:30:17 -05:00
[PR #178] fix(security): Host-header allowlist + restore dropped security headers #2640
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/reconurge/flowsint/pull/178
Author: @aaronjmars
Created: 6/5/2026
Status: 🔄 Open
Base:
main← Head:security/host-header-allowlist-dns-rebinding📝 Commits (1)
0a3567afix(security): Host-header allowlist + restore dropped security headers📊 Changes
2 files changed (+42 additions, -0 deletions)
View changed files
📝
README.md(+2 -0)📝
flowsint-app/nginx.conf(+40 -0)📄 Description
Summary
Adds a Host-header allowlist to
flowsint-app/nginx.confso the frontend rejects requests carrying an unexpectedHostheader 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, leavingPOST /api/auth/registerand any other unauthenticated route reachable from an attacker page.Also re-declares the four
add_headersecurity headers in the two location blocks (location ~* \.(js|css|...)$andlocation = /index.html) that were silently dropping them. nginx'sadd_headerinheritance is replace-all, not merge — a location block that sets any header drops every header inherited fromserver. The main HTML page was shipping withoutX-Frame-Options,X-Content-Type-Options,X-XSS-Protection, orReferrer-Policy.Impact
DNS rebinding (primary). Attacker JS on
evil.commakesevil.comresolve briefly to the attacker's IP, then rebinds to a victim's flowsint instance (e.g.127.0.0.1). Browser sendsHost: evil.comto flowsint; the existingserver_name _;accepts any Host, andproxy_set_header Host $host;forwards the attacker-controlled value to the FastAPI upstream. The CORS middleware inflowsint-api/app/main.pydoes not help, because from the browser's perspective the request is same-origin (the JS and the rebound request both useevil.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.htmlfrom thelocation = /index.htmlblock, which setsadd_header Cache-Control "no-store, ..."and thereby drops every server-level security header. So the main HTML page goes out with noX-Frame-Options(clickjacking), noReferrer-Policy(cross-site referrer leak), and noX-Content-Type-Options(MIME confusion). Same loss applied to the static-assets location for.js/.css.Location
flowsint-app/nginx.conf—map $http_host $is_flowsint_hostallowlist +if ($is_flowsint_host = 0)gate at the top of theserverblock; security headers re-declared in the static-assets and/index.htmllocations.README.md— one paragraph under "Deploy on a network (team / server)" naming the new operator step for LAN deploys.Fix
map $http_host $is_flowsint_hostblock athttplevel. Default0; allowlist entries forlocalhost,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.if ($is_flowsint_host = 0) { return 403; }at the top of theserverblock, before any header or location processing.location ~* \.(js|css|...)$andlocation = /index.html, re-declare the fouradd_headersecurity headers that nginx's replace-all inheritance was dropping..envsecret rotation step.Verification
nginx -t -c flowsint-app/nginx.conf -p <prefix>/→syntax is ok/test is successful.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.).192.168.1.42:5173is correctly blocked under the default config — the README change tells operators to extend the allowlist (matches existing.env-rotation pattern)..js/.css) and/index.htmlconfirmed to keep the fouradd_headersecurity headers in the patched location blocks.Detected by
Aeon + semgrep
/index.html).generic.nginx.security.request-host-usedatflowsint-app/nginx.conf:69flagged the unvalidatedHostpropagation;generic.nginx.security.header-redefinitionat:84/:90/:102flagged the silently dropped headers.Filed by Aeon.
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.