# `Macaw.Plug.SecurityHeaders`
[🔗](https://github.com/dcoai/macaw_plug/blob/v0.2.0/lib/macaw/plug/security_headers.ex#L1)

A strict Content-Security-Policy and the standard security headers.

    plug Macaw.Plug.SecurityHeaders

**The default policy is closed:** everything from the app's own origin,
nothing inline, nothing evaluated, nothing framed:

    default-src 'self'; script-src 'self'; style-src 'self';
    img-src 'self' data:; font-src 'self'; connect-src 'self';
    object-src 'none'; base-uri 'none'; form-action 'self';
    frame-ancestors 'none'

A host changes a directive only by naming it, `csp: [img_src: "'self'"]`,
so every loosening is visible in the host's code.

**A host's own inline script** is allowed by its SHA-256 hash, never by
`'unsafe-inline'`. The policy is written just before the response is sent,
so a controller that renders such a script adds its hash on the way:

    conn |> Macaw.Plug.SecurityHeaders.put_script_hash(script_body)

**Also set:** `x-content-type-options: nosniff`, `referrer-policy:
strict-origin-when-cross-origin`, `x-frame-options: DENY` (for browsers
that predate `frame-ancestors`), `cross-origin-opener-policy:
same-origin`, a `permissions-policy` that turns off camera, microphone,
geolocation, payment and USB, and over https
`strict-transport-security: max-age=31536000`. Each can be replaced with
`headers: %{"name" => value}`, or removed with `nil`. This replaces
Phoenix's `put_secure_browser_headers`.

## Options

| Option | Default | |
|---|---|---|
| `preset:` | `:strict` | `:compatible` starts from Phoenix's `put_secure_browser_headers` set |
| `csp:` | `[]` | directives by name, `[img_src: "'self' https://tiles.example"]` |
| `csp_mode:` | `:enforce` | `:report_only` reports violations and blocks nothing; `:off` sends no policy |
| `permissions:` | `[]` | Permissions-Policy features by name, `[geolocation: :self]`; `nil` removes one |
| `headers:` | `%{}` | any header by name; `nil` removes it |
| `hsts:` | `"max-age=31536000"` | over https; not a string turns it off |

Every option applies on top of the preset, and an unknown option raises.

**Permissions** are allowlists: `:none` (`()`), `:self` (`(self)`), `:all`
(`*`), or a list of `:self` and `http(s)://` origins:
`permissions: [camera: [:self, "https://meet.example"]]` gives
`camera=(self "https://meet.example")`.

## Migrating from `put_secure_browser_headers`

1. Start with `preset: :compatible`: the headers Phoenix sends, plus HSTS
   over https. Nothing a Phoenix page did stops working.
2. Try the closed policy without enforcing it: `csp_mode: :report_only`
   (with the strict preset) sends it as
   `content-security-policy-report-only`, so the browser console lists
   what it would block. Add `csp: [report_uri: "/csp-reports"]` to collect
   the reports.
3. Allow what the pages need by name: an inline script by its hash
   (`put_script_hash/2`), a directive with `csp:`, a browser feature with
   `permissions:`. Phoenix's live-reload iframe needs, in development
   only, `csp: [frame_ancestors: "'self'"]` and
   `headers: %{"x-frame-options" => "SAMEORIGIN"}`.
4. Enforce: drop `csp_mode:` (and `preset:`).

# `hash`

```elixir
@spec hash(String.t()) :: String.t()
```

A script body's CSP hash source, `'sha256-…'`.

# `permissions_policy`

```elixir
@spec permissions_policy(keyword()) :: String.t()
```

The Permissions-Policy string for features and their allowlists (the
defaults, or as `init/1` merged them):

    iex> Macaw.Plug.SecurityHeaders.permissions_policy(geolocation: :self, camera: :all)
    "geolocation=(self), camera=*"

An entry that is not `{feature, allowlist}` raises `ArgumentError`.

# `policy`

```elixir
@spec policy(keyword(String.t()), [String.t()]) :: String.t()
```

The policy string for directives (the defaults, or as `init/1` merged
them) and script hashes, which are added to `script-src`.

# `put_script_hash`

```elixir
@spec put_script_hash(Plug.Conn.t(), String.t()) :: Plug.Conn.t()
```

Allows an inline script on this response by its SHA-256 hash. Takes the
script's body (the text between `<script>` and `</script>`), exactly.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
