Skip to content

security: tighten app-wide Referrer-Policy to same-origin (#383) - #384

Merged
hokiepokedad2 merged 1 commit into
mainfrom
fix/383-referrer-policy-same-origin
Aug 5, 2026
Merged

security: tighten app-wide Referrer-Policy to same-origin (#383)#384
hokiepokedad2 merged 1 commit into
mainfrom
fix/383-referrer-policy-same-origin

Conversation

@hokiepokedad2

Copy link
Copy Markdown
Contributor

Closes #383. Follow-up to #242 / #382, which fixed this per-element for the gym picker only.

The change

Program.cs sent Referrer-Policy: strict-origin-when-cross-origin — the browser default, which discloses the origin on every cross-origin request. It now sends same-origin: full referrer within the site, nothing at all to third parties.

That covers, in one line, every remote resource the SPA loads:

  • uicons from raw.githubusercontent.com (icon.service.ts:5, operator-overridable so possibly a self-hosted mirror) across the Pokémon, raid, egg, lure, invasion, gym and quick-pick lists and dialogs
  • Discord avatars from cdn.discordapp.com (discord-avatar.component.ts, admin-users.component.html)
  • the Google Fonts stylesheets and font files in index.html

For a private or invite-only instance the hostname is the part worth withholding.

Why not no-referrer

AuthController reads Request.Headers.Referer in three places — DiscordLogin (:85), the OIDC login path (:263), and OIDC RP-initiated logout (:312) — to recover which frontend origin the user came from, validate it against the configured CORS origins, and stash it in oauth_origin so the callback can redirect back there.

no-referrer blanks that on same-origin requests too. All three would silently degrade to selfOrigin. Harmless when the SPA is served by this host; wrong when it isn't, which is exactly the split-origin dev setup. same-origin keeps the same-origin referrer intact and still sends nothing cross-origin, so it gets the privacy win without touching auth.

There's a <remarks> block on SecurityHeaders.ReferrerPolicy saying this, plus a test that fails if someone "tightens" it to no-referrer later. That trap is the whole reason this PR carries tests for a one-value change.

Refactor, and why

The header values moved out of the inline lambda in Program.cs into Configuration/SecurityHeaders.cs. Values in a top-level statement lambda can't be asserted without booting the app, and booting it drags in MySQL and PoracleNG — Microsoft.AspNetCore.Mvc.Testing is referenced in the test project but unused, and there's no WebApplicationFactory harness to extend. A static Apply(IHeaderDictionary) is testable directly.

The CSP came across byte-identical, split over three concatenated lines for readability. Apply_SetsContentSecurityPolicy_UnchangedFromTheInlineVersion pins it against the original single-line literal so the reformatting can't have introduced a typo.

Tests

SecurityHeadersTests (11 cases):

  • the policy is same-origin
  • it is never no-referrer, with the AuthController reasoning in the doc comment
  • it is none of the origin-leaking values (strict-origin-when-cross-origin, no-referrer-when-downgrade, origin, origin-when-cross-origin, unsafe-url)
  • CSP unchanged from the inline version
  • the remaining hardening headers still land
  • an upstream-set Referrer-Policy is overwritten, not appended to

AuthControllerLoginOriginTests (5 cases) — new coverage for the origin recovery this change depends on, which had none. Runs against the real AuthController and asserts the oauth_origin cookie: allowed CORS referer honored, same-origin referer honored when no CORS origins are configured, disallowed referer rejected, non-absolute referer rejected, absent referer falling back to self. That last one is the no-referrer scenario pinned as a test rather than an argument.

Full suite: 1457 passed, 0 failed (was 1452 before, +5 net after the additions — the 11 header cases replace nothing).

On the acceptance list in #383

Two items I did not tick, stated plainly rather than left implied:

  • Live login/logout in both topologies is not manually verified. That needs real Discord OAuth credentials, the Poracle DB and PoracleNG. The contract is covered by unit tests against the real controller instead, and the mechanism is unchanged — same-origin still sends a same-origin referrer, so those code paths see exactly what they saw before. Worth a smoke test on dev before release.
  • uicons / avatars / Google Fonts rendering not visually re-checked. None of them require a referrer, and cross-origin requests already sent only an origin, not a URL — but I didn't load a browser against a running instance to confirm.

The per-element referrerpolicy attributes from #242 are left in place. They're redundant under this header but harmless, and they hold if the header is ever relaxed.

Remote resource hosts were told the origin of every PoracleWeb instance
a user browsed: uicons on raw.githubusercontent.com, Discord avatars on
cdn.discordapp.com, and the Google Fonts stylesheets. #242 fixed this
per-element for the gym picker; annotating every tag in the app does not
scale.

The security-headers middleware sent strict-origin-when-cross-origin --
the browser default, which sends the origin cross-origin. It now sends
same-origin: full referrer within the site, nothing to third parties.
One line, every case.

no-referrer was rejected. AuthController reads the Referer header on
DiscordLogin, the OIDC login path, and OIDC RP-initiated logout to
recover which frontend origin the user came from, validate it against
the configured CORS origins, and redirect back there after the provider
callback. Blanking the same-origin referrer degrades all three to this
host's own origin and bounces users to the wrong place after login.

Header values moved out of the inline lambda in Program.cs into a
SecurityHeaders class so they can be asserted without booting the app.
The CSP is carried over byte-identical; a test pins it against the
original literal.

Tests cover the policy value, a guard that it never becomes no-referrer
or any of the origin-leaking values, and the AuthController origin
recovery it depends on -- which had no coverage at all before.

Closes #383
@github-actions github-actions Bot added the fix label Aug 5, 2026
@hokiepokedad2
hokiepokedad2 merged commit 3f8d38a into main Aug 5, 2026
7 checks passed
@hokiepokedad2
hokiepokedad2 deleted the fix/383-referrer-policy-same-origin branch August 5, 2026 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security: tighten the app-wide Referrer-Policy so third-party images stop leaking the instance origin

1 participant