add security headers
This commit is contained in:
parent
7ce5dc56d0
commit
e0552ac97d
3 changed files with 126 additions and 0 deletions
|
|
@ -46,6 +46,7 @@ defmodule ToweropsWeb.Endpoint do
|
|||
|
||||
plug Plug.RequestId
|
||||
plug ToweropsWeb.Plugs.RemoteIpLogger
|
||||
plug ToweropsWeb.Plugs.SecurityHeaders
|
||||
|
||||
plug Plug.Telemetry,
|
||||
event_prefix: [:phoenix, :endpoint],
|
||||
|
|
|
|||
53
lib/towerops_web/plugs/security_headers.ex
Normal file
53
lib/towerops_web/plugs/security_headers.ex
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
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
|
||||
72
test/towerops_web/plugs/security_headers_test.exs
Normal file
72
test/towerops_web/plugs/security_headers_test.exs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
defmodule ToweropsWeb.Plugs.SecurityHeadersTest do
|
||||
use ToweropsWeb.ConnCase, async: true
|
||||
|
||||
alias ToweropsWeb.Plugs.SecurityHeaders
|
||||
|
||||
describe "call/2" do
|
||||
test "adds security headers in production environment" do
|
||||
# Save original env
|
||||
original_env = Application.get_env(:towerops, :env)
|
||||
|
||||
try do
|
||||
# Set environment to production
|
||||
Application.put_env(:towerops, :env, :prod)
|
||||
|
||||
conn = SecurityHeaders.call(build_conn(), [])
|
||||
|
||||
# Verify all security headers are present
|
||||
assert ["default-src 'self'" <> _] = get_resp_header(conn, "content-security-policy")
|
||||
assert ["SAMEORIGIN"] = get_resp_header(conn, "x-frame-options")
|
||||
assert ["nosniff"] = get_resp_header(conn, "x-content-type-options")
|
||||
|
||||
assert ["strict-origin-when-cross-origin"] =
|
||||
get_resp_header(conn, "referrer-policy")
|
||||
|
||||
# Verify CSP contains required directives
|
||||
[csp] = get_resp_header(conn, "content-security-policy")
|
||||
assert csp =~ "default-src 'self'"
|
||||
assert csp =~ "script-src 'self' 'unsafe-inline' 'unsafe-eval'"
|
||||
assert csp =~ "style-src 'self' 'unsafe-inline' 'unsafe-hashes'"
|
||||
assert csp =~ "img-src 'self' data: https:"
|
||||
assert csp =~ "font-src 'self' data:"
|
||||
assert csp =~ "connect-src 'self' ws: wss:"
|
||||
assert csp =~ "frame-ancestors 'self'"
|
||||
after
|
||||
# Restore original env
|
||||
Application.put_env(:towerops, :env, original_env)
|
||||
end
|
||||
end
|
||||
|
||||
test "does not add security headers in non-production environment" do
|
||||
# Default test environment is :test, not :prod
|
||||
conn = SecurityHeaders.call(build_conn(), [])
|
||||
|
||||
# Verify no security headers are present
|
||||
assert [] = get_resp_header(conn, "content-security-policy")
|
||||
assert [] = get_resp_header(conn, "x-frame-options")
|
||||
assert [] = get_resp_header(conn, "x-content-type-options")
|
||||
assert [] = get_resp_header(conn, "referrer-policy")
|
||||
end
|
||||
|
||||
test "does not add security headers in development environment" do
|
||||
# Save original env
|
||||
original_env = Application.get_env(:towerops, :env)
|
||||
|
||||
try do
|
||||
# Set environment to development
|
||||
Application.put_env(:towerops, :env, :dev)
|
||||
|
||||
conn = SecurityHeaders.call(build_conn(), [])
|
||||
|
||||
# Verify no security headers are present
|
||||
assert [] = get_resp_header(conn, "content-security-policy")
|
||||
assert [] = get_resp_header(conn, "x-frame-options")
|
||||
assert [] = get_resp_header(conn, "x-content-type-options")
|
||||
assert [] = get_resp_header(conn, "referrer-policy")
|
||||
after
|
||||
# Restore original env
|
||||
Application.put_env(:towerops, :env, original_env)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue