towerops/lib/towerops_web/plugs/security_headers.ex
Graham McIntire 39e588c686 fix: API token access control, admin form crash, MIB validation, CSP dedup
- Add organization membership check before API token creation
- Fix admin security allowlist form reading current_user instead of current_scope.user
- Use recursive File.ls instead of Path.wildcard to include hidden files in MIB validation
- Add upload size check before ZIP extraction in MIB controller
- Centralize CSP in SecurityHeaders plug with per-request nonces instead of 'unsafe-inline'
2026-05-12 09:08:57 -05:00

45 lines
1.4 KiB
Elixir

defmodule ToweropsWeb.Plugs.SecurityHeaders do
@moduledoc """
Adds security headers to all responses.
Headers added:
- Content-Security-Policy: Restricts resource loading to prevent XSS
- X-Frame-Options: Prevents clickjacking attacks
- X-Content-Type-Options: Prevents MIME type sniffing
- Referrer-Policy: Controls referrer information leakage
- Permissions-Policy: Disables browsing-topics API
"""
import Plug.Conn
@nonce_bytes 16
def init(opts), do: opts
def call(conn, _opts) do
nonce = @nonce_bytes |> :crypto.strong_rand_bytes() |> Base.encode64(padding: false)
conn
|> put_private(:csp_nonce, nonce)
|> put_resp_header("permissions-policy", "browsing-topics=()")
|> put_resp_header("content-security-policy", csp_header(nonce))
|> put_resp_header("x-frame-options", "DENY")
|> put_resp_header("x-content-type-options", "nosniff")
|> put_resp_header("referrer-policy", "strict-origin-when-cross-origin")
end
defp csp_header(nonce) do
Enum.join(
[
"default-src 'self' data:",
"script-src 'self' 'nonce-#{nonce}' https://a.w5isp.com",
"style-src 'self' 'unsafe-inline' 'unsafe-hashes'",
"img-src 'self' data: https:",
"font-src 'self' data:",
"connect-src 'self' ws: wss: https://a.w5isp.com https://*.tile.openstreetmap.org",
"frame-ancestors 'none'"
],
"; "
)
end
end