nx_http_security_headers.nx source
↩ module page · 289 lines · 11730 B
1// nx_http_security_headers.nx -- Enforces secure HTTP headers for XSS, clickjacking, MIME-sniffing, and privacy by default.
2const NXSH_MAGIC_31536000: i64 = 31536000
3// nx_http_security_headers.nx -- production HTTP security headers.
4//
5// One sealed-enum primitive replaces what WordPress needs 3-4
6// plugins for (Really Simple SSL / Wordfence / Headers / etc.).
7// Substrate's default-secure posture.
8//
9// Headers emitted (caller picks the policy strictness):
10//
11// Content-Security-Policy -- defense-in-depth for XSS;
12// complements nx_html_render's
13// typed-context escape (CWE-79
14// structural prevention).
15// X-Frame-Options -- clickjacking prevention
16// X-Content-Type-Options nosniff -- MIME-confusion prevention
17// Referrer-Policy -- privacy: don't leak URL on link-out
18// Permissions-Policy -- opt out of camera/mic/geo/etc.
19// Strict-Transport-Security -- HTTPS-only enforcement
20// (substrate emits even on HTTP;
21// browser stores for future TLS)
22// X-DNS-Prefetch-Control off -- privacy: don't prefetch
23//
24// Per cardinal feedback-no-third-party-trust-native-or-nothing:
25// substrate's own header set; no helmet.js, no security-middleware
26// gem, no WordPress plugin.
27//
28// Per cardinal feedback-privacy-by-default-no-tracking:
29// - Referrer-Policy defaults to "strict-origin-when-cross-origin"
30// (only origin sent, no path; doesn't leak user navigation)
31// - Permissions-Policy explicitly DENIES camera, microphone,
32// geolocation, payment, USB, midi, accelerometer, gyroscope,
33// magnetometer, battery (the standard privacy-leak surfaces)
34//
35// nx_capability_claims:
36// needs: [sealed_enum, byte_ops]
37// provides: [csp_header, x_frame_options, mime_sniff_protect,
38// referrer_policy, permissions_policy, hsts,
39// all_secure_defaults_one_call]
40// safety: [no_unchecked_deref, no_floating_point, no_syscall,
41// bounded_iteration, bit_equal_reproducible,
42// privacy_by_default_referrer_permissions]
43// verdict: [sealed_enum_4_state_verdict + sealed_enum_3_strictness]
44// license: ORIGINAL
45// kind: racing_crew_specialist
46// layer: L3 (algorithm: response header emit)
47
48// ---- Sealed enum: emit verdict -----------------------------------
49
50const NXSH_OK: i64 = 0
51const NXSH_OOM_BUFFER: i64 = 1
52const NXSH_BAD_LEVEL: i64 = 2
53const NXSH_BAD_ARG: i64 = 3
54const NXSH_VERDICT_N: i64 = 4
55
56func nxsh_verdict_is_valid(v: i64) -> i64 {
57 if v < 0 { return 0 }
58 if v >= NXSH_VERDICT_N { return 0 }
59 return 1
60}
61
62func nxsh_verdict_name(v: i64) -> *u8 {
63 if v == NXSH_OK { return "OK" as *u8 }
64 if v == NXSH_OOM_BUFFER { return "OOM_BUFFER" as *u8 }
65 if v == NXSH_BAD_LEVEL { return "BAD_LEVEL" as *u8 }
66 if v == NXSH_BAD_ARG { return "BAD_ARG" as *u8 }
67 return "INVALID" as *u8
68}
69
70// ---- Sealed enum: strictness level -------------------------------
71//
72// STRICT default-deny CSP, frame-options DENY, referrer NONE
73// MODERATE relaxed CSP for sites with embedded resources
74// PERMISSIVE minimal headers; for legacy compatibility
75
76const NXSH_STRICT: i64 = 0
77const NXSH_MODERATE: i64 = 1
78const NXSH_PERMISSIVE: i64 = 2
79const NXSH_LEVEL_N: i64 = 3
80
81func nxsh_level_is_valid(L: i64) -> i64 {
82 if L < 0 { return 0 }
83 if L >= NXSH_LEVEL_N { return 0 }
84 return 1
85}
86
87func nxsh_level_name(L: i64) -> *u8 {
88 if L == NXSH_STRICT { return "STRICT" as *u8 }
89 if L == NXSH_MODERATE { return "MODERATE" as *u8 }
90 if L == NXSH_PERMISSIVE { return "PERMISSIVE" as *u8 }
91 return "INVALID" as *u8
92}
93
94// ---- Byte emit helpers -------------------------------------------
95
96func nxsh_put(out: *u8, off: *i64, cap: i64, b: i64) -> i64 {
97 if *off >= cap { return NXSH_OOM_BUFFER }
98 out[*off] = b as u8
99 *off = *off + 1
100 return NXSH_OK
101}
102
103func nxsh_put_cstr(out: *u8, off: *i64, cap: i64, s: *u8) -> i64 {
104 var i: i64 = 0
105 while s[i] != 0 {
106 let rc: i64 = nxsh_put(out, off, cap, s[i] as i64)
107 if rc != NXSH_OK { return rc }
108 i = i + 1
109 }
110 return NXSH_OK
111}
112
113// ---- Individual header emitters ---------------------------------
114
115// Content-Security-Policy: tuned by strictness level.
116//
117// STRICT:
118// default-src 'none';
119// script-src 'self';
120// style-src 'self' 'unsafe-inline'; (inline styles needed for
121// substrate's dashboard CSS)
122// img-src 'self' data:;
123// font-src 'self';
124// connect-src 'self';
125// frame-ancestors 'none';
126// base-uri 'self';
127// form-action 'self';
128//
129// MODERATE: like STRICT but allows 'unsafe-inline' for scripts (for
130// legacy compatibility -- avoid for new code)
131//
132// PERMISSIVE: only default-src 'self'
133
134func nx_http_emit_csp(out: *u8, off: *i64, cap: i64, level: i64) -> i64 {
135 if out == (0 as *u8) { return NXSH_BAD_ARG }
136 if off == (0 as *i64) { return NXSH_BAD_ARG }
137 if cap <= 0 { return NXSH_BAD_ARG }
138 if nxsh_level_is_valid(level) != 1 { return NXSH_BAD_LEVEL }
139 let r1: i64 = nxsh_put_cstr(out, off, cap,
140 "Content-Security-Policy: " as *u8)
141 if r1 != NXSH_OK { return r1 }
142 if level == NXSH_STRICT {
143 let r2: i64 = nxsh_put_cstr(out, off, cap,
144 "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" as *u8)
145 if r2 != NXSH_OK { return r2 }
146 }
147 if level == NXSH_MODERATE {
148 let r3: i64 = nxsh_put_cstr(out, off, cap,
149 "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'self'" as *u8)
150 if r3 != NXSH_OK { return r3 }
151 }
152 if level == NXSH_PERMISSIVE {
153 let r4: i64 = nxsh_put_cstr(out, off, cap,
154 "default-src 'self'" as *u8)
155 if r4 != NXSH_OK { return r4 }
156 }
157 return nxsh_put_cstr(out, off, cap, "\r\n" as *u8)
158}
159
160// X-Frame-Options: clickjacking prevention.
161// STRICT -> DENY (cannot be framed at all)
162// MODERATE -> SAMEORIGIN
163// PERMISSIVE -> (header omitted)
164func nx_http_emit_x_frame_options(out: *u8, off: *i64, cap: i64,
165 level: i64) -> i64 {
166 if nxsh_level_is_valid(level) != 1 { return NXSH_BAD_LEVEL }
167 if level == NXSH_PERMISSIVE { return NXSH_OK }
168 let r1: i64 = nxsh_put_cstr(out, off, cap,
169 "X-Frame-Options: " as *u8)
170 if r1 != NXSH_OK { return r1 }
171 if level == NXSH_STRICT {
172 return nxsh_put_cstr(out, off, cap, "DENY\r\n" as *u8)
173 }
174 return nxsh_put_cstr(out, off, cap, "SAMEORIGIN\r\n" as *u8)
175}
176
177// X-Content-Type-Options: prevent MIME-sniff attacks. Always nosniff.
178func nx_http_emit_x_content_type_options(out: *u8, off: *i64, cap: i64) -> i64 {
179 return nxsh_put_cstr(out, off, cap,
180 "X-Content-Type-Options: nosniff\r\n" as *u8)
181}
182
183// Referrer-Policy: privacy default per cardinal feedback-privacy-by-
184// default-no-tracking.
185// STRICT -> no-referrer
186// MODERATE -> strict-origin-when-cross-origin (default; sends
187// origin for cross-origin, full URL same-origin)
188// PERMISSIVE -> origin-when-cross-origin
189func nx_http_emit_referrer_policy(out: *u8, off: *i64, cap: i64,
190 level: i64) -> i64 {
191 if nxsh_level_is_valid(level) != 1 { return NXSH_BAD_LEVEL }
192 let r1: i64 = nxsh_put_cstr(out, off, cap,
193 "Referrer-Policy: " as *u8)
194 if r1 != NXSH_OK { return r1 }
195 if level == NXSH_STRICT {
196 return nxsh_put_cstr(out, off, cap, "no-referrer\r\n" as *u8)
197 }
198 if level == NXSH_MODERATE {
199 return nxsh_put_cstr(out, off, cap,
200 "strict-origin-when-cross-origin\r\n" as *u8)
201 }
202 return nxsh_put_cstr(out, off, cap,
203 "origin-when-cross-origin\r\n" as *u8)
204}
205
206// Permissions-Policy: opt-out of privacy-leak browser APIs.
207// Substrate dashboard needs NONE of these; deny by default.
208func nx_http_emit_permissions_policy(out: *u8, off: *i64, cap: i64,
209 level: i64) -> i64 {
210 if nxsh_level_is_valid(level) != 1 { return NXSH_BAD_LEVEL }
211 if level == NXSH_PERMISSIVE { return NXSH_OK }
212 return nxsh_put_cstr(out, off, cap,
213 "Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), midi=(), accelerometer=(), gyroscope=(), magnetometer=(), battery=(), display-capture=()\r\n" as *u8)
214}
215
216// Strict-Transport-Security: HTTPS-only enforcement. Caller chooses
217// max_age_s (typically 1 year = 31536000 for production, 60 for
218// testing). We emit even on HTTP responses; modern browsers store
219// the directive for the next TLS handshake.
220func nx_http_emit_hsts(out: *u8, off: *i64, cap: i64,
221 max_age_s: i64,
222 include_subdomains: i64) -> i64 {
223 if out == (0 as *u8) { return NXSH_BAD_ARG }
224 if off == (0 as *i64) { return NXSH_BAD_ARG }
225 if max_age_s < 0 { return NXSH_BAD_ARG }
226 let r1: i64 = nxsh_put_cstr(out, off, cap,
227 "Strict-Transport-Security: max-age=" as *u8)
228 if r1 != NXSH_OK { return r1 }
229 if max_age_s == 0 {
230 let rc: i64 = nxsh_put(out, off, cap, 0x30)
231 if rc != NXSH_OK { return rc }
232 } else {
233 var n: i64 = max_age_s
234 let digit_start: i64 = *off
235 while n > 0 {
236 let rc: i64 = nxsh_put(out, off, cap, 0x30 + (n - (n / 10) * 10))
237 if rc != NXSH_OK { return rc }
238 n = n / 10
239 }
240 var lo: i64 = digit_start
241 var hi: i64 = *off - 1
242 while lo < hi {
243 let tmp: i64 = out[lo] as i64
244 out[lo] = out[hi]
245 out[hi] = tmp as u8
246 lo = lo + 1
247 hi = hi - 1
248 }
249 }
250 if include_subdomains == 1 {
251 let rs: i64 = nxsh_put_cstr(out, off, cap, "; includeSubDomains" as *u8)
252 if rs != NXSH_OK { return rs }
253 }
254 return nxsh_put_cstr(out, off, cap, "\r\n" as *u8)
255}
256
257// X-DNS-Prefetch-Control: privacy-default off
258func nx_http_emit_dns_prefetch_off(out: *u8, off: *i64, cap: i64) -> i64 {
259 return nxsh_put_cstr(out, off, cap,
260 "X-DNS-Prefetch-Control: off\r\n" as *u8)
261}
262
263// ---- All-secure-defaults bundle ---------------------------------
264//
265// One call to emit the production header set. Caller integrates
266// into their response between the status line and the blank line.
267
268func nx_http_emit_security_headers_bundle(
269 out: *u8, off: *i64, cap: i64, level: i64) -> i64 {
270 if out == (0 as *u8) { return NXSH_BAD_ARG }
271 if off == (0 as *i64) { return NXSH_BAD_ARG }
272 if cap <= 0 { return NXSH_BAD_ARG }
273 if nxsh_level_is_valid(level) != 1 { return NXSH_BAD_LEVEL }
274
275 let r1: i64 = nx_http_emit_csp(out, off, cap, level)
276 if r1 != NXSH_OK { return r1 }
277 let r2: i64 = nx_http_emit_x_frame_options(out, off, cap, level)
278 if r2 != NXSH_OK { return r2 }
279 let r3: i64 = nx_http_emit_x_content_type_options(out, off, cap)
280 if r3 != NXSH_OK { return r3 }
281 let r4: i64 = nx_http_emit_referrer_policy(out, off, cap, level)
282 if r4 != NXSH_OK { return r4 }
283 let r5: i64 = nx_http_emit_permissions_policy(out, off, cap, level)
284 if r5 != NXSH_OK { return r5 }
285 let r6: i64 = nx_http_emit_dns_prefetch_off(out, off, cap)
286 if r6 != NXSH_OK { return r6 }
287 // HSTS: 1 year + includeSubDomains by default for production-class
288 return nx_http_emit_hsts(out, off, cap, NXSH_MAGIC_31536000, 1)
289}