# Plug.CrossOriginProtection
[](https://hex.pm/packages/plug_cross_origin_protection)
[](https://hexdocs.pm/plug_cross_origin_protection)
A Plug to protect against Cross-Site Request Forgery (CSRF) attacks using modern
header-based checks instead of tokens.
Based on [Filippo Valsorda's blog post](https://words.filippo.io/csrf/) and the
[Go 1.25 `net/http` CrossOriginProtection](https://pkg.go.dev/net/http@go1.25rc2#CrossOriginProtection).
## How it works
Modern browsers (since 2023) send the `Sec-Fetch-Site` header which reliably
indicates whether a request is same-origin, same-site, cross-site, or
user-initiated. This plug uses that header (with a fallback to `Origin` header
comparison) to reject cross-origin requests without requiring CSRF tokens.
1. **Safe methods** (GET, HEAD, OPTIONS) are always allowed
2. If `Origin` header matches a **trusted origin**, the request is allowed
3. If `Sec-Fetch-Site` is `same-origin` or `none`, the request is allowed
4. If `Sec-Fetch-Site` indicates cross-origin, the request is **rejected**
5. If no headers are present, the request is allowed (non-browser client)
6. If only `Origin` is present, it's compared against the `Host` header
## Installation
Add `plug_cross_origin_protection` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:plug_cross_origin_protection, "~> 0.1.0"}
]
end
```
## Usage
### Basic usage
```elixir
# In your Phoenix endpoint or router
plug Plug.CrossOriginProtection
```
### With trusted origins
For SSO callbacks or partner integrations:
```elixir
plug Plug.CrossOriginProtection,
trusted_origins: [
"https://sso.example.com",
"https://partner.example.com"
]
```
### Exception mode
Raise an exception instead of returning 403 Forbidden:
```elixir
plug Plug.CrossOriginProtection, with: :exception
```
### Skipping protection for specific routes
For webhooks or public API endpoints:
```elixir
# In a controller
defmodule MyApp.WebhookController do
use MyApp, :controller
plug :skip_csrf when action in [:receive]
defp skip_csrf(conn, _opts) do
Plug.CrossOriginProtection.skip(conn)
end
def receive(conn, params) do
# Handle webhook...
end
end
```
Or using `put_private` directly:
```elixir
Plug.Conn.put_private(conn, :plug_skip_cross_origin_protection, true)
```
## Security considerations
- **Safe methods**: Ensure your application never performs state-changing actions
on GET, HEAD, or OPTIONS requests
- **HTTPS**: Use HTTPS in production. The `Sec-Fetch-Site` header is only sent to
secure origins
- **HSTS**: Consider using HTTP Strict Transport Security to protect against
HTTP→HTTPS attacks on older browsers
- **Browser support**: `Sec-Fetch-Site` is supported in all major browsers since
2023. Older browsers fall back to Origin/Host comparison
## License
MIT License. See [LICENSE](LICENSE) for details.