origin-strict: a change with neither Sec-Fetch-Site nor Origin is refused
Agreed with the owner on 2026-10-13 (in place of csrf-tokens), taken up on
2026-10-16 ("Да"). originAllowed (internal/web/security.go) returns false
in the "neither header" branch: such a POST is answered 403 and logged
like a cross-origin one. It was let through until now as an accepted
risk, which left a browser that sends neither open to a request forged by
any site. A browser that follows Fetch § 3.2 never sends a POST without
Origin; one too old to (Firefox before 70) can no longer save anything.
What posts to the panel without a browser: the e2e suite's client, which
now sends Origin on every form (panelClient.postForm); cmd/panel's tests
already did. Nothing else posts to it (the CLI runs in the container).
Tests: security_test's case of no headers is refused; e2e posts with
neither header and gets 403 (testFormPostWithTheBrowsersOrigin). go vet +
go test (26 packages) and make e2e on the test server for this tree.
Docs: security.md - the accepted risk struck, the ADR's decision and
trade-off say refused, the origin-check bullet; architecture § Web,
guide § the proxy; CHANGELOG [Unreleased] § Security; roadmap row done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+9
-15
@@ -204,6 +204,8 @@ assigned outbound domains and inbound domains **separately**, each list with
|
||||
host (the panel's `Referrer-Policy` is `same-origin`, so a browser puts the
|
||||
panel's own origin on a form post of the panel); the session cookie is
|
||||
`SameSite` as well.
|
||||
- A request with neither header is refused as well (origin-strict,
|
||||
`2.10.4`).
|
||||
- There are **no CSRF tokens**. The decision, its price and the conditions
|
||||
for revisiting it are in the accepted risks and the ADR below.
|
||||
|
||||
@@ -512,17 +514,6 @@ operator can act on it.
|
||||
An accepted risk is a decision with a condition for revisiting it, not a
|
||||
deferred item from the roadmap.
|
||||
|
||||
- **A `POST` with neither `Sec-Fetch-Site` nor `Origin` is allowed through.**
|
||||
A client that sends neither — a genuinely old browser, or a webview with a
|
||||
frozen engine — stays vulnerable to CSRF from any site. Accepted
|
||||
deliberately: every panel user (role `global` or `domain`) is an operator who
|
||||
picks their own browser, not an untrusted party the panel needs to defend
|
||||
against, and a strict mode would not "protect" such a client, it would
|
||||
simply break the panel in it. Tightening is one line in `originAllowed`
|
||||
([internal/web/security.go](../internal/web/security.go)): return `false`
|
||||
instead of `true` in the "neither header present" branch. That tightening
|
||||
is agreed (owner, 2026-10-13) as
|
||||
[origin-strict](roadmap.md#origin-strict); this bullet goes when it ships.
|
||||
- **Session-bound CSRF tokens are not implemented.** The origin check closes the
|
||||
neighbouring-subdomain case but depends on browser behaviour; a token does
|
||||
not. The price is a hidden field in roughly two dozen forms. The trigger to
|
||||
@@ -844,7 +835,8 @@ through the browser.
|
||||
**Decision.** `originAllowed` in
|
||||
[internal/web/security.go](../internal/web/security.go) checks `Sec-Fetch-Site`
|
||||
(when the browser sends it) or `Origin` (fallback) against the panel's host; a
|
||||
request carrying neither header is **allowed through** rather than rejected.
|
||||
request carrying neither header is **refused** (403), since `2.10.4`
|
||||
(origin-strict; until then it was let through as an accepted risk).
|
||||
There are no session-bound tokens embedded in forms. The check applies the same
|
||||
way regardless of the requesting user's role.
|
||||
|
||||
@@ -864,9 +856,11 @@ and sends the request itself. XSS is handled by `html/template` auto-escaping
|
||||
and CSP, so that is a separate line of defence, not a CSRF token.
|
||||
|
||||
**Trade-off.** A client that sends neither `Sec-Fetch-Site` nor `Origin` (a
|
||||
genuinely old browser, or a webview with a frozen engine) stays vulnerable — see
|
||||
"Accepted risks" above. This is a deliberate choice not to break the panel in
|
||||
such a client, at the price of a narrow residual surface.
|
||||
genuinely old browser - Firefox before 70 sent no Origin with a same-origin
|
||||
form - or a webview with a frozen engine) can no longer save anything in the
|
||||
panel: refused rather than left open to a request forged by any site. Until
|
||||
`2.10.4` the choice was the other way round, as an accepted risk. A script
|
||||
that posts to the panel sends `Origin` itself (the e2e suite's client does).
|
||||
|
||||
**Revisit if:** a requirement appears for protection that does not depend on
|
||||
browser behaviour, or `domain` accounts stop being trusted operators (for
|
||||
|
||||
Reference in New Issue
Block a user