Skip to content

Reject wildcard CORS origin when credentials are enabled - #52

Merged
umputun merged 3 commits into
masterfrom
fix/cors-credentials-wildcard
Aug 18, 2026
Merged

Reject wildcard CORS origin when credentials are enabled#52
umputun merged 3 commits into
masterfrom
fix/cors-credentials-wildcard

Conversation

@paskal

@paskal paskal commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #49, so the diff here is the CORS change alone. The base retargets to master once #49 merges.

This is a breaking change and it breaks remark42 at boot. Details below.

CORS now panics at construction when "*" is among the allowed origins and CorsAllowCredentials(true) is set, including the case where the origin list is left at its default.

Previously that combination reflected any request Origin back together with Access-Control-Allow-Credentials: true. That is precisely the configuration the same-origin policy exists to prevent: any site a victim visits can issue credentialed requests to the service and read the responses. The CORSConfig documentation already stated that credentials cannot be combined with "*", so the code was contradicting its own contract rather than implementing a deliberate choice.

Who this breaks

Any caller doing rest.CORS(rest.CorsAllowCredentials(true)) without naming origins, or passing "*" explicitly alongside credentials, panics when the middleware is constructed.

remark42 is affected. corsMiddleware() in app/rest/api/middleware.go wires CorsAllowedOrigins("*") together with CorsAllowCredentials(true), and it is built inside routes() during server start, so this panics at boot rather than per request. Every deployment that does not set --proxy-cors is affected. remark42 has to enumerate its allowed origins before it can take a release containing this.

Worth stating plainly: that same configuration is a live exposure in remark42 today, so the fix is worth the coordination rather than the other way round.

Why a panic

The failure is deliberately loud and at construction time, so a misconfiguration cannot reach production silently. The alternative of quietly dropping the credentials header would break authentication at runtime in a way that is far harder to trace back to its cause, and would do so on the request path rather than at startup.

Callers that need credentials list their origins instead:

router.Use(rest.CORS(
    rest.CorsAllowedOrigins("https://app.example.com"),
    rest.CorsAllowCredentials(true),
))

The wildcard remains available without credentials, which is unchanged and safe.

The test that asserted the old behaviour is replaced with cases covering the default list, an explicit "*", and "*" alongside other origins.

@coveralls

coveralls commented Aug 18, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 32197183823

Coverage decreased (-0.3%) to 97.091%

Details

  • Coverage decreased (-0.3%) from the base build.
  • Patch coverage: 7 of 7 lines across 1 file are fully covered (100%).
  • 7 coverage regressions across 2 files.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

7 previously-covered lines in 2 files lost coverage.

File Lines Losing Coverage Coverage
file_server.go 4 88.24%
gzip.go 3 93.33%

Coverage Stats

Coverage Status
Relevant Lines: 1547
Covered Lines: 1502
Line Coverage: 97.09%
Coverage Strength: 48.61 hits per line

💛 - Coveralls

@paskal
paskal force-pushed the fix/cors-credentials-wildcard branch from 40c0257 to 906e698 Compare August 18, 2026 23:12
@paskal
paskal force-pushed the fix/middleware-findings branch from 5b0489e to 9ee60a7 Compare August 18, 2026 23:12
@umputun

umputun commented Aug 18, 2026

Copy link
Copy Markdown
Member

the underlying issue is real, wildcard plus credentials reflects any origin with Access-Control-Allow-Credentials: true, and that shouldn't be the silent default. But an unconditional panic doesn't work.

remark42 uses exactly that combination on purpose, there's a comment above corsMiddleware() in app/rest/api/middleware.go saying why. It serves a comment widget embedded on arbitrary third-party sites, so it can't enumerate origins. As written this doesn't ask it to change a setting, it removes the capability and crashes it at boot on the next go get -u.

pls keep the rejection as the default, but add an explicit opt-in with a name that makes the risk obvious, so a caller who knowingly wants origin reflection can still have it. Also note this is based on fix/middleware-findings, so it needs retargeting to master along with #51.

@paskal
paskal force-pushed the fix/cors-credentials-wildcard branch from 906e698 to 9079085 Compare August 18, 2026 23:13
@paskal
paskal changed the base branch from fix/middleware-findings to master August 18, 2026 23:23
paskal added 3 commits August 19, 2026 00:25
Previously, CORS(CorsAllowCredentials(true)) kept the default "*" origin list
and reflected any request Origin back together with
Access-Control-Allow-Credentials: true, which lets any site read
cookie-authenticated responses. The config doc already claimed the combination
was not allowed.

CORS now panics at construction when "*" is among the allowed origins and
credentials are on. This is a breaking change for callers relying on the old
behaviour, who have to enumerate their origins instead.
Rejecting "*" with credentials stays the default, but it no longer removes the
capability. A service that has to accept credentialed requests from arbitrary
third-party origins, an embeddable widget being the obvious case, can ask for
origin reflection by name, and the name and doc comment say what it costs.
@paskal
paskal force-pushed the fix/cors-credentials-wildcard branch from 9079085 to 98408c6 Compare August 18, 2026 23:26
@paskal

paskal commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator Author

Added the opt-in as CorsUnsafeAnyOriginWithCredentials(true) in 98408c6. Default is unchanged, wildcard plus credentials still panics, but a caller who needs origin reflection can ask for it by name and the doc comment spells out that any site can then read authenticated responses. remark42 keeps working by adding that one option.

Also retargeted this to master and rebased off fix/middleware-findings, so the diff here is just the CORS change.

@umputun
umputun merged commit 896d9aa into master Aug 18, 2026
4 of 6 checks passed
@umputun
umputun deleted the fix/cors-credentials-wildcard branch August 18, 2026 23:28
paskal added a commit to umputun/remark42 that referenced this pull request Aug 19, 2026
rest.CORS refuses "*" together with credentials since go-pkgz/rest#52, so the
bump and the option have to land together: the option does not exist in v1.22.0
and the panic fires at construction, inside routes(), which makes it a startup
failure rather than a request-time one.

The wildcard stays. The comment widget is embedded on arbitrary third-party
sites, so the set of origins is not knowable, which is why the escape hatch was
asked for upstream instead of accepting the panic. What it costs is unchanged
and now written next to the call: any site a signed-in user visits can read
authenticated responses, so state-changing requests have to keep being protected
by something other than the origin, X-XSRF-Token today.

Pinned to master for now rather than a release, as the fixes are not tagged yet.
The bump also carries testify to v1.12.0, which drops go-spew and go-difflib
from the module graph.
paskal added a commit to umputun/remark42 that referenced this pull request Aug 19, 2026
rest.CORS refuses "*" together with credentials since go-pkgz/rest#52, so the
bump and the option have to land together: the option does not exist in v1.22.0
and the panic fires at construction, inside routes(), which makes it a startup
failure rather than a request-time one.

The wildcard stays. The comment widget is embedded on arbitrary third-party
sites, so the set of origins is not knowable, which is why the escape hatch was
asked for upstream instead of accepting the panic. What it costs is unchanged
and now written next to the call: any site a signed-in user visits can read
authenticated responses, so state-changing requests have to keep being protected
by something other than the origin, X-XSRF-Token today.

Pinned to master for now rather than a release, as the fixes are not tagged yet.
The bump also carries testify to v1.12.0, which drops go-spew and go-difflib
from the module graph.
paskal added a commit to umputun/remark42 that referenced this pull request Aug 19, 2026
…entials

rest.CORS refuses "*" together with credentials since go-pkgz/rest#52, so the
bump and the option have to land together: the option does not exist in v1.22.0
and the panic fires at construction, inside routes(), which makes it a startup
failure rather than a request-time one.

The wildcard stays. The comment widget is embedded on arbitrary third-party
sites, so the set of origins is not knowable, which is why the escape hatch was
asked for upstream instead of accepting the panic. What it costs is unchanged
and now written next to the call: any site a signed-in user visits can read
authenticated responses, so state-changing requests have to keep being protected
by something other than the origin, X-XSRF-Token today.

The example module is tidied in the same commit, as it reaches go-pkgz/rest
through the replace directive and its indirect graph would otherwise keep the
old pin and fail the readonly module check in CI.

The bump also carries testify to v1.12.0, which drops go-spew and go-difflib
from the module graph.
paskal added a commit to umputun/remark42 that referenced this pull request Aug 19, 2026
…entials

rest.CORS refuses "*" together with credentials since go-pkgz/rest#52, so the
bump and the option have to land together: the option does not exist in v1.22.0
and the panic fires at construction, inside routes(), which makes it a startup
failure rather than a request-time one.

The wildcard stays. The comment widget is embedded on arbitrary third-party
sites, so the set of origins is not knowable, which is why the escape hatch was
asked for upstream instead of accepting the panic. What it costs is unchanged
and now written next to the call: any site a signed-in user visits can read
authenticated responses, so state-changing requests have to keep being protected
by something other than the origin, X-XSRF-Token today.

The example module is tidied in the same commit, as it reaches go-pkgz/rest
through the replace directive and its indirect graph would otherwise keep the
old pin and fail the readonly module check in CI.

The bump also carries testify to v1.12.0, which drops go-spew and go-difflib
from the module graph.
umputun pushed a commit to umputun/remark42 that referenced this pull request Aug 19, 2026
…entials (#2157)

rest.CORS refuses "*" together with credentials since go-pkgz/rest#52, so the
bump and the option have to land together: the option does not exist in v1.22.0
and the panic fires at construction, inside routes(), which makes it a startup
failure rather than a request-time one.

The wildcard stays. The comment widget is embedded on arbitrary third-party
sites, so the set of origins is not knowable, which is why the escape hatch was
asked for upstream instead of accepting the panic. What it costs is unchanged
and now written next to the call: any site a signed-in user visits can read
authenticated responses, so state-changing requests have to keep being protected
by something other than the origin, X-XSRF-Token today.

The example module is tidied in the same commit, as it reaches go-pkgz/rest
through the replace directive and its indirect graph would otherwise keep the
old pin and fail the readonly module check in CI.

The bump also carries testify to v1.12.0, which drops go-spew and go-difflib
from the module graph.
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.

3 participants