towerops/lib/towerops_web/plugs/security_headers.ex

53 lines
1.8 KiB
Elixir

defmodule ToweropsWeb.Plugs.SecurityHeaders do
@moduledoc """
Adds security headers to all responses in production.
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
These headers are only applied in production to avoid interfering with
development tools like LiveReload.
"""
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
if Application.get_env(:towerops, :env) == :prod do
conn
|> put_resp_header("content-security-policy", csp_header())
|> put_resp_header("x-frame-options", "SAMEORIGIN")
|> put_resp_header("x-content-type-options", "nosniff")
|> put_resp_header("referrer-policy", "strict-origin-when-cross-origin")
else
conn
end
end
# Content Security Policy
# - default-src 'self': Only load resources from same origin by default
# - script-src: Allow inline scripts (needed for LiveView), eval, and self
# - style-src: Allow inline styles (needed for Tailwind), unsafe-hashes, and self
# - img-src: Allow images from self, data URIs, and https
# - font-src: Allow fonts from self and data URIs
# - connect-src: Allow WebSocket connections for LiveView
# - frame-ancestors: Same as X-Frame-Options (SAMEORIGIN)
defp csp_header do
Enum.join(
[
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"style-src 'self' 'unsafe-inline' 'unsafe-hashes'",
"img-src 'self' data: https:",
"font-src 'self' data:",
"connect-src 'self' ws: wss:",
"frame-ancestors 'self'"
],
"; "
)
end
end