The claim
An identity server's pages can carry a Content Security Policy that allows almost nothing, and they should. Two
things about that policy are not obvious from the specification: the form-action directive behaves differently
across browser engines on the redirects that every OAuth sign-in depends on, and base-uri 'none' changes how a
Blazor page must address its assets. Both have to be decided, and the decision has to be tested in a real browser.
The policy
Hexavera's identity host serves account pages (sign-in, consent, profile, security) and the protocol endpoints. The account pages run no script and use no inline style, by decision, so the policy allows neither:
Content-Security-Policy: default-src 'none'; style-src 'self'; img-src 'self';
object-src 'none'; base-uri 'none'; frame-ancestors 'none'
The two pages that hold a passkey ceremony load one module from the host and talk to the host, and only they add
script-src 'self'; connect-src 'self'. No page allows inline script or another origin.
OWASP ASVS 5.0.0 requirement 3.4.3 (level 2) asks that "as a minimum, a global policy must be used which includes the
directives object-src 'none' and base-uri 'none' and defines either an allowlist or uses nonces or hashes". The policy above is the allowlist form, with the allowlist reduced to the host itself.
The directive left out
The specification defines form-action as the directive that "restricts the URLs which can be used as the target of
a form submissions from a given context" (CSP Level 3, section 6.4.1). On an identity server, form-action 'self'
reads as the natural choice: the only form target is the host.
The pre-navigation check in section 6.4.1.1 matches the request's current URL, with its redirect count, against the directive, and the HTML Standard runs that check again for every redirect a navigation follows, so by the specification each hop after the submission is checked. Whether it should be is still debated, and the browsers do not agree: the working group's issue "CSP: form-action and redirects" has been open since 2015, and MDN's note on the directive records that "Firefox 57 doesn't block the redirects whereas Chrome 63 does".
An OAuth sign-in is a chain of redirects that starts with a form submission: the credentials are posted to the sign-in
page, the server redirects to the authorization endpoint, and the authorization endpoint redirects to the client's
registered address, which is another origin here, because the identity host has a hostname of its own. In a Chromium-based browser, form-action 'self' is applied
to each of those hops, so the last one is refused and the person never arrives at the application. It was measured in
Edge on 2026-10-02 while the host was being built.
So the policy carries no form-action. That is an accepted risk, written into the threat model with its reasoning,
and it comes with a compensating control: what form-action would guard against is injected markup posting the
form elsewhere, and the account pages are kept free of raw markup by a test rule that scans the host's components
and code for four constructs that bypass Razor's encoding (MarkupString, AddMarkupContent, @Html.Raw,
HtmlString) and its scripts for markup and eval sinks, and fails on any it finds. The alternative still on the list
is a per-response policy whose form-action names the validated client's origin; it is feasible, but every future
page that ends at a client would have to remember to declare that origin, and a forgotten one would break sign-in for
Chromium users only, where in-process tests cannot see it, so it waits for a reason.
What base-uri 'none' does to a Blazor page
base-uri "restricts the URLs which can be used in a Document's base element" (CSP Level 3, section 6.3.1). With
'none', any <base> element with an href is a violation, including the <base href="/" /> that the .NET 10 Blazor Web App
template puts in App.razor (the .NET 11 template renders it through a BasePath component instead). Blazor's
fingerprinted asset paths are written relative to that base: @Assets["app.css"] renders a
relative address. Without a working <base>, a page served at /account/sign-in resolves that address under its own
path and renders unstyled.
The host therefore has no <base> element, and every asset reference is root-relative: / prefixed to the
fingerprinted path. A test renders the sign-in and signed-out pages and asserts that every stylesheet and image
reference starts with / and loads; a passkey test asserts that the sign-in page's script is addressed from /
too.
This site keeps its <base href="/"> because it carries no base-uri directive; the two choices are consistent, and
each is written down.
The policy is chosen from the endpoint
Which pages carry the passkey variant of the policy is a per-endpoint decision, and it is made the way the second note in this series describes: the two pages mark themselves with an attribute, and the header middleware reads the attribute from the endpoint routing chose when the response starts. The policy was once chosen by comparing the path, and one spelling of the sign-in page received the strict policy, which refused the passkey module. Choosing from the endpoint means every spelling routing sends to those pages receives the right policy, and an error page re-executed for them does not.
What it cost
- A test rule over the account pages' markup, maintained alongside them.
- Root-relative asset paths and a test that guards them.
- A threat-model row that says, in writing, that
form-actionis absent and why.
What a team can take from this
Treat a Content Security Policy as a feature with browser-specific behaviour, not as a header you set once. Drive the real flows through a Chromium-based browser and through Firefox before you ship a directive, because the engines do not all implement the specification the same way. When a directive breaks a flow you need, do not weaken the whole policy; remove the one directive, write down the risk you accepted, and put a compensating control where a reviewer will find it.
Principal software engineer, Belgrade
Designs and builds secure, multi-tenant .NET platforms. Questions about this entry, or a different view on it, are welcome by email.