code wiki / (root) / nx_http_security_headers.nx

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}