Server Action CSRF Origin Check
Hard · Server Actions
Server Actions are called with a POST request to the page that uses them. To stop CSRF attacks, Next.js compares the host in the request's Origin header with the app's own host (taken from x-forwarded-host, or host when that header is missing). Extra trusted hosts can be listed in serverActions.allowedOrigins in next.config.ts.
Write checkActionOrigin(request, allowedOrigins). request is { method, headers } with lowercase header names. Return { allowed, reason } using these rules, in order:
- The method is not POST (case-insensitive): { allowed: false, reason: "method-not-allowed" }. (A real non-POST request never reaches the action; this is our model.)
- No origin header: Next.js lets it through with a warning: { allowed: true, reason: "missing-origin" }.
- The origin host is new URL(origin).host (hostname plus port, the default port is dropped). If new URL throws (for example "null"): { allowed: false, reason: "invalid-origin" }.
- The app host is x-forwarded-host if present, otherwise host. If it equals the origin host: { allowed: true, reason: "same-origin" }.
- If some entry of allowedOrigins matches the origin host: { allowed: true, reason: "allowed-origin" }. Otherwise { allowed: false, reason: "origin-mismatch" }.
Matching an entry (case-insensitive), like the real implementation:
- An entry equal to the origin host matches.
- Otherwise split both on . and compare from the right. * matches exactly one non-empty label. ** matches one or more labels and is only valid as the first label. Any other label must be equal. After the pattern is used up, the host must have no labels left.
- An empty label in the pattern never matches. A pattern that is only * or ** never matches. There is no partial wildcard: app-*.example.com is compared literally.
- The port is part of the last label, so *.my-proxy.com does not match app.my-proxy.com:8443.