#1052·server

CORS and websocket origin allowlists match unanchored, so 'https://example.com' also allows 'https://example.com.evil.net'

Author: lbellowsCreated Sep 17, 2026Updated Sep 17, 2026

Both origin allowlists compile config entries with regexp.MustCompile and match with MatchString, which is a substring match unless the operator remembers to anchor:

  • auth/cors.go:45compileAllowedCORSOrigins, used by AllowOriginFunc
  • api/stream/stream.go:183compileAllowedWebSocketOrigins, used by Upgrader.CheckOrigin

So a natural-looking value matches any hostname that merely contains it:

$ GOTIFY_SERVER_CORS_ALLOWORIGINS='https://gotify.example.com' gotify serve

$ curl -sD- -o/dev/null -H 'Origin: https://gotify.example.com' localhost:8792/version
Access-Control-Allow-Origin: https://gotify.example.com
$ curl -sD- -o/dev/null -H 'Origin: https://gotify.example.com.evil.net' localhost:8792/version
Access-Control-Allow-Origin: https://gotify.example.com.evil.net      # <-- allowed
$ curl -sD- -o/dev/null -H 'Origin: http://evil.net' localhost:8792/version
(no ACAO header)

Impact is limited — AllowCredentials is never set, so the cookie is not sent cross-origin, and an attacker still needs a token to read anything — but the allowlist does not mean what an operator reading the env var name would assume, and the same footgun applies to GOTIFY_SERVER_STREAM_ALLOWEDORIGINS on the websocket.

Options, in rough order of preference:

  1. Anchor automatically: wrap each entry as ^(?:…)$ before compiling (breaks any existing config that relies on a trailing .*, so it would need a release note).
  2. Keep the regex but warn at startup when an entry is not anchored.
  3. Document the anchoring requirement in the config docs, at minimum.

Happy to send a PR for whichever you prefer.