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