## TL;DR
Astro's CSRF middleware (`checkOrigin`, on by default since Astro 4.9) returns a 403 with this exact message when it rejects a form POST. Most of the time the submission is legitimate and only looks cross-site because the app runs behind a reverse proxy: declare your production domain in `security.allowedDomains` in `astro.config` so Astro trusts the proxy's forwarded headers, rebuild, and the forms go through.

```text
Cross-site POST form submissions are forbidden
```

## Use this when
- Every form POST 403s in production with this exact message while the same forms work in local dev
- The app sits behind Cloudflare, Cloud Run, Caddy, or any TLS-terminating proxy
- Login, sign-out, or contact forms fail but JSON posts work (the check only applies to form content types)

## Not for this skill when
- JSON or API posts fail (checkOrigin only inspects form-encoded POST, PATCH, PUT, DELETE)
- The site uses static output (checkOrigin only runs for SSR/on-demand pages)
- You want the quick fix of disabling checkOrigin (dont; see below)

## Steps

1. Confirm the pattern: 403 on form POSTs in production, fine in local dev. That means Astro is rejecting a legitimate same-origin POST, not an actual attack. Behind a proxy, Astro ignores `X-Forwarded-Host` and `X-Forwarded-Proto` unless your domain is allowlisted, so the Origin header no longer matches the URL Astro reconstructs.

2. In `astro.config`, add your real production hostname (requires Astro 5.14.2 or later):

```js
security: {
  allowedDomains: [{ hostname: 'YOUR_SITE_HOST', protocol: 'https' }]
}
```
Match the hostname and protocol the browser actually uses. Serving several subdomains? List each one explicitly, or use a wildcard pattern like `**.YOUR_SITE_HOST`.

3. Rebuild and redeploy, then resubmit the form. Expected: the POST returns 200 or 302 instead of 403.

4. On Astro below 5.14.2, `allowedDomains` does not exist: upgrade instead of hunting for another config knob.

## Do not disable checkOrigin
Setting `checkOrigin` to false globally clears the error by removing CSRF protection from every form in the app. It hides the misconfiguration instead of fixing it.

## Variant phrasings

### 403 Cross-site POST form submissions are forbidden
The exact message; same fix as above.

### Every form POST fails in production with a CSRF 403
Same root cause: the proxy hides the real origin from Astro's middleware.

### Sign-out or login form blocked behind a proxy
The most common shape of this error: auth forms POST in production and 403 while everything else looks fine.

## Why it happens
Astro's `checkOrigin` middleware compares the browser's Origin header against the URL Astro reconstructs for the request. Behind a proxy, the reconstruction uses the internal scheme and host, the Origin no longer matches, and every form-encoded write is rejected as cross-site. JSON posts are unaffected because the check only applies to form content types.

## Edge cases
- The allowlist entry `[{}]` trusts any forwarded host, which reopens host-header injection: reserve it for trusted dev setups.
- Only on-demand rendered (SSR) pages run the check; static output never triggers this error.

## Related skills
- cross-site post form submissions are forbidden astro (alternate coverage): https://vectle.com/skills/skl_c8OUWMxAb88csiXGJb0PjA

## Provenance
Verified against the official docs: https://docs.astro.build/en/reference/configuration-reference/
