cover/Elixir.DaProductAppWeb.Plugs.SecurityHeaders.html

1 defmodule DaProductAppWeb.Plugs.SecurityHeaders do
2 @moduledoc """
3 Plug to set comprehensive security headers for the UPI PSP Platform.
4
5 This addresses the security vulnerabilities identified in the ZAP scan:
6 - Content Security Policy (CSP) Header Not Set [10038]
7 - Missing Anti-clickjacking Header [10020]
8 - Insufficient Site Isolation Against Spectre Vulnerability [90004]
9 - Permissions Policy Header Not Set [10063]
10 - X-Content-Type-Options Header Missing [10021]
11 - Cache control directives [10015]
12
13 Note: HSTS (Strict-Transport-Security) and HTTPS redirect are handled by
14 Phoenix's built-in force_ssl configuration in prod.exs for better maintainability.
15
16 Environment-specific security policies:
17 - Development: HTTP is allowed for easier development workflow
18 - Production: HTTPS is enforced with upgrade-insecure-requests CSP directive
19 """
20
21 @behaviour Plug
22 import Plug.Conn
23
24 10 def init(opts), do: opts
25
26 def call(conn, _opts) do
27 conn
28 |> put_content_security_policy()
29 |> put_anti_clickjacking_headers()
30 |> put_spectre_protection_headers()
31 |> put_permissions_policy()
32 |> put_content_type_options()
33 |> put_cache_control_headers()
34 10 |> put_additional_security_headers()
35 end
36
37 # Content Security Policy - prevents XSS attacks
38 defp put_content_security_policy(conn) do
39 10 csp_value = build_csp_header()
40
41 10 put_resp_header(conn, "content-security-policy", csp_value)
42 end
43
44 # Build CSP header based on application needs
45 defp build_csp_header do
46 10 base_policy = [
47 "default-src 'self'",
48 "font-src 'self' https://fonts.gstatic.com",
49 "img-src 'self' data: https:",
50 "connect-src 'self' wss: ws:",
51 "frame-src 'none'",
52 "object-src 'none'",
53 "base-uri 'self'",
54 "form-action 'self'",
55 "frame-ancestors 'none'"
56 ]
57
58 # Only upgrade insecure requests in production for security
59 10 base_policy = if Mix.env() == :prod do
60
:-(
base_policy ++ ["upgrade-insecure-requests"]
61 else
62 10 base_policy
63 end
64
65 10 policy =
66 if Mix.env() == :prod do
67 # Stricter production policy for financial platform security
68
:-(
base_policy ++ [
69 "script-src 'self' https://cdn.tailwindcss.com",
70 "style-src 'self' https://fonts.googleapis.com https://cdn.tailwindcss.com"
71 ]
72 else
73 # Lenient development policy to allow hot reloading and development tools
74 10 base_policy ++ [
75 "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.tailwindcss.com",
76 "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.tailwindcss.com"
77 ]
78 end
79
80 10 policy |> Enum.join("; ")
81 end
82
83 # Anti-clickjacking headers
84 defp put_anti_clickjacking_headers(conn) do
85 10 put_resp_header(conn, "x-frame-options", "DENY")
86 end
87
88 # Spectre vulnerability protection
89 defp put_spectre_protection_headers(conn) do
90 conn
91 |> put_resp_header("cross-origin-embedder-policy", "require-corp")
92 |> put_resp_header("cross-origin-opener-policy", "same-origin")
93 10 |> put_resp_header("cross-origin-resource-policy", "same-origin")
94 end
95
96 # Permissions Policy (formerly Feature Policy)
97 defp put_permissions_policy(conn) do
98 10 permissions_policy = [
99 "geolocation=()",
100 "microphone=()",
101 "camera=()",
102 "payment=(self)",
103 "usb=()",
104 "magnetometer=()",
105 "gyroscope=()",
106 "accelerometer=()",
107 "ambient-light-sensor=()",
108 "autoplay=()",
109 "encrypted-media=()",
110 "fullscreen=(self)",
111 "picture-in-picture=()"
112 ]
113 |> Enum.join(", ")
114
115 conn
116 10 |> put_resp_header("permissions-policy", permissions_policy)
117 end
118
119 # Prevent MIME type sniffing
120 defp put_content_type_options(conn) do
121 10 put_resp_header(conn, "x-content-type-options", "nosniff")
122 end
123
124 # Cache control headers for security
125 defp put_cache_control_headers(conn) do
126 10 case conn.request_path do
127 # API endpoints should not be cached
128 "/api/" <> _ ->
129 conn
130 |> put_resp_header("cache-control", "no-store, no-cache, must-revalidate, private")
131 |> put_resp_header("pragma", "no-cache")
132 1 |> put_resp_header("expires", "0")
133
134 # Default: no cache for sensitive content (static assets are handled by Plug.Static)
135 _ ->
136 conn
137 |> put_resp_header("cache-control", "no-store, no-cache, must-revalidate, private")
138 |> put_resp_header("pragma", "no-cache")
139 9 |> put_resp_header("expires", "0")
140 end
141 end
142
143 # Additional security headers
144 defp put_additional_security_headers(conn) do
145 conn
146 |> put_resp_header("x-download-options", "noopen")
147 |> put_resp_header("x-permitted-cross-domain-policies", "none")
148 |> put_resp_header("referrer-policy", "strict-origin-when-cross-origin")
149 10 |> put_resp_header("x-robots-tag", "noindex, nofollow") # Financial platform should not be indexed
150 end
151 end
Line Hits Source