@@ -7,21 +7,62 @@ use serde_bytes::ByteBuf;
77
88mod metrics;
99
10+ /// CORS-safe security headers for OPTIONS requests
11+ /// These headers provide security without interfering with CORS preflight requests
12+ fn cors_safe_security_headers ( ) -> Vec < HeaderField > {
13+ vec ! [
14+ // X-Frame-Options: DENY
15+ // Prevents the page from being displayed in a frame, iframe, embed or object
16+ // This helps prevent clickjacking attacks
17+ ( "X-Frame-Options" . to_string( ) , "DENY" . to_string( ) ) ,
18+
19+ // X-Content-Type-Options: nosniff
20+ // Prevents browsers from MIME-sniffing the content type
21+ // Forces browsers to respect the declared Content-Type header
22+ ( "X-Content-Type-Options" . to_string( ) , "nosniff" . to_string( ) ) ,
23+
24+ // Strict-Transport-Security (HSTS)
25+ // Forces browsers to use HTTPS for all future requests to this domain
26+ // max-age=31536000: Valid for 1 year (365 days)
27+ // includeSubDomains: Also applies to all subdomains
28+ (
29+ "Strict-Transport-Security" . to_string( ) ,
30+ "max-age=31536000 ; includeSubDomains" . to_string( ) ,
31+ ) ,
32+
33+ // Referrer-Policy: same-origin
34+ // Controls how much referrer information is sent with requests
35+ // same-origin: Only send referrer to same-origin requests
36+ ( "Referrer-Policy" . to_string( ) , "same-origin" . to_string( ) ) ,
37+
38+ // Content-Security-Policy: default-src 'none'
39+ // Minimal CSP for OPTIONS - blocks all content since no scripts should execute
40+ (
41+ "Content-Security-Policy" . to_string( ) ,
42+ "default-src 'none'" . to_string( ) ,
43+ ) ,
44+ ]
45+ }
46+
1047fn http_options_request ( ) -> HttpResponse {
48+ let mut headers = vec ! [
49+ ( "Access-Control-Allow-Origin" . to_string( ) , "*" . to_string( ) ) ,
50+ (
51+ "Access-Control-Allow-Methods" . to_string( ) ,
52+ "GET, POST, OPTIONS" . to_string( ) ,
53+ ) ,
54+ (
55+ "Access-Control-Allow-Headers" . to_string( ) ,
56+ "Content-Type" . to_string( ) ,
57+ ) ,
58+ ( "Content-Length" . to_string( ) , "0" . to_string( ) ) ,
59+ ] ;
60+
61+ headers. append ( & mut cors_safe_security_headers ( ) ) ;
62+
1163 HttpResponse {
1264 status_code : 204 ,
13- headers : vec ! [
14- ( "Access-Control-Allow-Origin" . to_string( ) , "*" . to_string( ) ) ,
15- (
16- "Access-Control-Allow-Methods" . to_string( ) ,
17- "GET, POST, OPTIONS" . to_string( ) ,
18- ) ,
19- (
20- "Access-Control-Allow-Headers" . to_string( ) ,
21- "Content-Type" . to_string( ) ,
22- ) ,
23- ( "Content-Length" . to_string( ) , "0" . to_string( ) ) ,
24- ] ,
65+ headers,
2566 body : ByteBuf :: from ( vec ! [ ] ) ,
2667 upgrade : None ,
2768 streaming_strategy : None ,
@@ -127,7 +168,7 @@ pub fn http_request(req: HttpRequest) -> HttpResponse {
127168/// These headers enable browser security features (like limit access to platform apis and set
128169/// iFrame policies, etc.).
129170///
130- /// Integrity hashes for scripts must be speficied .
171+ /// Integrity hashes for scripts must be specified .
131172pub fn security_headers (
132173 integrity_hashes : Vec < String > ,
133174 maybe_related_origins : Option < Vec < String > > ,
@@ -142,19 +183,45 @@ pub fn security_headers(
142183 } ) ;
143184
144185 vec ! [
186+ // X-Frame-Options: DENY
187+ // Prevents the page from being displayed in a frame, iframe, embed or object
188+ // This is a legacy header, also enforced by CSP frame-ancestors directive
145189 ( "X-Frame-Options" . to_string( ) , "DENY" . to_string( ) ) ,
190+
191+ // X-Content-Type-Options: nosniff
192+ // Prevents browsers from MIME-sniffing a response away from the declared content-type
193+ // Reduces risk of drive-by downloads and serves as defense against MIME confusion attacks
146194 ( "X-Content-Type-Options" . to_string( ) , "nosniff" . to_string( ) ) ,
195+
196+ // Content-Security-Policy (CSP)
197+ // Comprehensive policy to prevent XSS attacks and data injection
198+ // See content_security_policy_header() function for detailed explanation
147199 (
148200 "Content-Security-Policy" . to_string( ) ,
149201 content_security_policy_header( integrity_hashes, maybe_related_origins) ,
150202 ) ,
203+
204+ // Strict-Transport-Security (HSTS)
205+ // Forces browsers to use HTTPS for all future requests to this domain
206+ // max-age=31536000: Valid for 1 year (31,536,000 seconds)
207+ // includeSubDomains: Also applies to all subdomains of this domain
151208 (
152209 "Strict-Transport-Security" . to_string( ) ,
153210 "max-age=31536000 ; includeSubDomains" . to_string( ) ,
154211 ) ,
155- // "Referrer-Policy: no-referrer" would be more strict, but breaks local dev deployment
156- // same-origin is still ok from a security perspective
212+
213+ // Referrer-Policy: same-origin
214+ // Controls how much referrer information is sent with outgoing requests
215+ // same-origin: Only send referrer to same-origin requests (no cross-origin leakage)
216+ // Note: "no-referrer" would be more strict but breaks local dev deployment
157217 ( "Referrer-Policy" . to_string( ) , "same-origin" . to_string( ) ) ,
218+
219+ // Permissions-Policy (formerly Feature-Policy)
220+ // Controls which browser features and APIs can be used
221+ // Most permissions are denied by default, with specific exceptions:
222+ // - clipboard-write=(self): Allow copying to clipboard from same origin
223+ // - publickey-credentials-get: Allow WebAuthn from self and related origins
224+ // - sync-xhr=(self): Allow synchronous XMLHttpRequest from same origin
158225 (
159226 "Permissions-Policy" . to_string( ) ,
160227 format!(
@@ -207,31 +274,44 @@ pub fn security_headers(
207274
208275/// Full content security policy delivered via HTTP response header.
209276///
210- /// script-src 'strict-dynamic' ensures that only scripts with hashes listed here (through
211- /// 'integrity_hashes') can be loaded. Transitively loaded scripts don't need their hashes
212- /// listed.
277+ /// CSP directives explained:
278+ ///
279+ /// default-src 'none':
280+ /// Default policy for all resource types - deny everything by default
281+ ///
282+ /// connect-src 'self' https::
283+ /// Allow network requests to same origin and any HTTPS endpoint
284+ /// - 'self': fetch JS bundles from same origin
285+ /// - https://icp-api.io: official IC HTTP API domain for canister calls
286+ /// - https://*.icp0.io: HTTP fetches for /.well-known/ii-alternative-origins
287+ /// - https://*.ic0.app: legacy domain support for alternative origins
288+ ///
289+ /// img-src 'self' data: https://*.googleusercontent.com:
290+ /// Allow images from same origin, data URIs, and Google profile pictures
291+ ///
292+ /// script-src with 'strict-dynamic':
293+ /// - 'strict-dynamic': Only scripts with listed hashes can load, transitively loaded scripts inherit permission
294+ /// - 'unsafe-eval': Required for WebAssembly modules used by agent-js for BLS signature validation
295+ /// - 'unsafe-inline' https:: Backwards compatibility fallback (ignored by modern browsers)
296+ ///
297+ /// base-uri 'none':
298+ /// Prevents injection of <base> tags that could redirect relative URLs
213299///
214- /// script-src 'unsafe-eval' is required because agent-js uses a WebAssembly module for the
215- /// validation of bls signatures.
216- /// There is currently no other way to allow execution of WebAssembly modules with CSP.
217- /// See https://github.com/WebAssembly/content-security-policy/blob/main/proposals/CSP.md.
300+ /// form-action 'none':
301+ /// Prevents forms from being submitted anywhere (II doesn't use forms)
218302///
219- /// script-src 'unsafe-inline' https: are only there for backwards compatibility and ignored
220- /// by modern browsers.
303+ /// style-src 'self' 'unsafe-inline':
304+ /// Allow stylesheets from same origin and inline styles
305+ /// 'unsafe-inline' needed due to how styles are currently handled in the app
221306///
222- /// connect-src is used to ensure fetch requests can only be made against known domains:
223- /// * 'self': used fetch the JS bundles
224- /// * https://icp-api.io: the official IC HTTP API domain for canister calls to the canister
225- /// * https://*.icp0.io: HTTP fetches for checking /.well-known/ii-alternative-origins on
226- /// other canisters (authenticating canisters setting a derivationOrigin)
227- /// * https://*.ic0.app: same as above, but legacy
307+ /// font-src 'self':
308+ /// Allow fonts only from same origin
228309///
229- /// style-src 'unsafe-inline' is currently required due to the way styles are handled by the
230- /// application. Adding hashes would require a big restructuring of the application and build
231- /// infrastructure.
310+ /// frame-ancestors and frame-src:
311+ /// Control embedding - allow self and related origins for cross-domain WebAuthn
232312///
233- /// upgrade-insecure-requests is omitted when building in dev mode to allow loading II on localhost
234- /// with Safari.
313+ /// upgrade-insecure-requests (production only):
314+ /// Automatically upgrade HTTP requests to HTTPS (omitted in dev for localhost)
235315fn content_security_policy_header (
236316 integrity_hashes : Vec < String > ,
237317 maybe_related_origins : Option < Vec < String > > ,
@@ -257,7 +337,7 @@ fn content_security_policy_header(
257337 #[ cfg( feature = "dev_csp" ) ]
258338 let connect_src = format ! ( "{connect_src} http:" ) ;
259339
260- // Allow related origins to embed one another
340+ // Allow related origins to embed one another for cross-domain WebAuthn
261341 let frame_src = maybe_related_origins
262342 . unwrap_or_default ( )
263343 . iter ( )
@@ -276,7 +356,8 @@ fn content_security_policy_header(
276356 frame-ancestors {frame_src};\
277357 frame-src {frame_src};"
278358 ) ;
279- // for the dev build skip upgrading all connections to II to https
359+ // For production builds, upgrade all HTTP connections to HTTPS
360+ // Omitted in dev builds to allow localhost development
280361 #[ cfg( not( feature = "dev_csp" ) ) ]
281362 let csp = format ! ( "{csp}upgrade-insecure-requests;" ) ;
282363 csp
0 commit comments