Skip to content

Origin Policy

wiki edited this page Sep 4, 2026 · 1 revision

Origin policy

type OriginPolicy struct {
	AllowedOrigins   []string
	AllowCredentials bool
}

One allowlist of browser origins, declared here so CORS and CSRF read the same configuration and neither module imports the other.

policy := rextension.OriginPolicy{
	AllowedOrigins:   []string{"https://app.example.com", "https://admin.example.com"},
	AllowCredentials: true,
}

if err := policy.Valid(); err != nil {
	return err // fail the deployment, not the request
}

CORS does not prevent CSRF

The two consumers share a list and nothing else. It is worth being explicit about why, because the belief that CORS covers CSRF is widespread and wrong.

A simple request — a form POST with application/x-www-form-urlencoded, multipart/form-data or text/plain — gets no preflight. The browser dispatches it, the server receives it, and the server commits the state change. CORS then prevents the attacker from reading the response, which is no consolation for a transfer that already happened.

So: CORS answers "may this origin read my responses?". CSRF answers "did this request really come from my own application?". Different questions, one allowlist.

Exact matching only

Origins are matched exactly — scheme, host and port:

"https://app.example.com"        // right
"app.example.com"                // never matches; no scheme
"https://app.example.com:8443"   // a different origin than the same host on 443

There is no pattern or wildcard-subdomain matching, deliberately. Origin patterns are a classic source of over-permissive policies:

  • a naive suffix check for .example.com matches evil-example.com
  • a prefix check for https://app.example matches https://app.example.attacker.com

Both have appeared in real advisories. List the origins.

"*" and credentials are mutually exclusive

"*" allows any origin, and is valid only without credentials.

This is not convention. The Fetch specification requires that Access-Control-Allow-Origin echo a concrete origin when Access-Control-Allow-Credentials is true, and browsers reject the response otherwise. Allows therefore refuses "*" when AllowCredentials is set, rather than emitting a combination the browser will silently discard — a failure that looks like a server bug from the client's side and produces no useful error anywhere.

Valid() reports the contradiction, and it is checked at startup so a self-contradictory policy stops a deployment rather than producing responses nobody can debug.

The empty origin is never allowed

Allows("") is always false, including under a "*" policy. A same-origin request carries no Origin header for most methods, and such a request needs no CORS decision at all — treating "" as allowed would make every allowlist meaningless.

Helpers

// The request's Origin header. Deliberately does not fall back to Referer.
func RequestOrigin(r *http.Request) string

// Safe in the RFC 9110 sense: does not change server state.
func SafeMethod(method string) bool

Referer is not consulted as a fallback. It is stripped by privacy settings, by referrer policies and by some proxies — so treating its absence as "no cross-origin request" would be a bypass, and treating its presence as authoritative would let a referrer policy weaken the check.

CSRF protection applies only to unsafe methods. That is not a convenience: a GET that changes state is a bug in its own right, and one CSRF protection cannot fix, because a browser will issue it from an <img> tag with no way for the server to distinguish it.

API

Allows(origin string) bool is this origin permitted
IsWildcard() bool does the policy allow any origin
Valid() error is the policy internally consistent, and why not

Valid returns *OriginPolicyError, which names the offending entry and the reason.

Clone this wiki locally