code wiki / (root) / nx_http_template.nx

nx_http_template.nx source

↩ module page · 292 lines · 10688 B

1// nx_http_template.nx -- sealed-enum context-typed HTML templates. 2// 3// Production-class template engine with STRUCTURAL XSS prevention. 4// Every variable substitution flows through nx_html_render's 5// typed-context escape primitive (which closes CWE-79 at the 6// substrate boundary). Substrate doesn't trust callers to remember 7// to escape -- the template syntax FORCES context declaration. 8// 9// Template syntax: 10// 11// <h1>Hello, {{ name }}!</h1> (default TEXT context) 12// <a href="{{ url | url }}">click</a> (URL context) 13// <div class="{{ cls | attr }}">... (ATTR_DQ context) 14// <p>{{ html_fragment | raw }}</p> (RAW; audited bypass) 15// 16// Unknown modifiers reject the entire template (BAD_MODIFIER verdict). 17// This makes "I forgot to escape" structurally impossible. 18// 19// Per cardinal feedback-no-third-party-trust-native-or-nothing: 20// substrate's own template engine; no Twig, no Handlebars, no 21// Jinja, no Mustache. ~300 LOC; auditable. 22// 23// Per cardinal feedback-defensive-at-boundaries-trusting-internally: 24// the template VAR LOOKUP is at the boundary; once a variable is 25// substituted with the correct escape, the resulting bytes are 26// trusted as safe HTML. 27// 28// nx_capability_claims: 29// needs: [sealed_enum, byte_ops, html_render_typed_context] 30// provides: [template_emit_with_context_typed_substitution, 31// structural_xss_prevention_at_template_layer] 32// safety: [no_unchecked_deref, no_floating_point, no_syscall, 33// bounded_iteration, xss_prevented_by_construction] 34// verdict: [sealed_enum_6_state] 35// license: ORIGINAL 36// kind: racing_crew_specialist 37// layer: L4 (composite over L2 nx_html_render) 38 39import "nx_syscalls_x86_64.nx" 40import "nx_html_render.nx" 41 42// ---- Sealed enum: template verdict ------------------------------- 43 44const NXT_OK: i64 = 0 45const NXT_OOM_BUFFER: i64 = 1 46const NXT_BAD_TEMPLATE: i64 = 2 // missing }} or other syntax error 47const NXT_BAD_MODIFIER: i64 = 3 // unknown | modifier 48const NXT_VAR_NOT_FOUND: i64 = 4 // template references unknown var 49const NXT_BAD_ARG: i64 = 5 50const NXT_VERDICT_N: i64 = 6 51 52func nxt_verdict_is_valid(v: i64) -> i64 { 53 if v < 0 { return 0 } 54 if v >= NXT_VERDICT_N { return 0 } 55 return 1 56} 57 58func nxt_verdict_name(v: i64) -> *u8 { 59 if v == NXT_OK { return "OK" as *u8 } 60 if v == NXT_OOM_BUFFER { return "OOM_BUFFER" as *u8 } 61 if v == NXT_BAD_TEMPLATE { return "BAD_TEMPLATE" as *u8 } 62 if v == NXT_BAD_MODIFIER { return "BAD_MODIFIER" as *u8 } 63 if v == NXT_VAR_NOT_FOUND { return "VAR_NOT_FOUND" as *u8 } 64 if v == NXT_BAD_ARG { return "BAD_ARG" as *u8 } 65 return "INVALID" as *u8 66} 67 68// ---- Var table struct -------------------------------------------- 69// 70// Caller-allocated. Parallel arrays for var_name + var_value. 71// Lookup is linear scan -- substrate's templates are small enough 72// (typically < 50 vars per render) for this to be fine. 73 74struct NxTemplateVars { 75 n: i64, 76 names: *u8, 77 name_offs: *i64, 78 name_lens: *i64, 79 values: *u8, 80 value_offs: *i64, 81 value_lens: *i64, 82} 83 84const NX_TEMPLATE_VARS_BYTES: i64 = 56 85 86func nx_template_vars_init( 87 vars: *NxTemplateVars, 88 names: *u8, name_offs: *i64, name_lens: *i64, 89 values: *u8, value_offs: *i64, value_lens: *i64, 90 n: i64) -> i64 { 91 if vars == (0 as *NxTemplateVars) { return NXT_BAD_ARG } 92 if n < 0 { return NXT_BAD_ARG } 93 vars.n = n 94 vars.names = names 95 vars.name_offs = name_offs 96 vars.name_lens = name_lens 97 vars.values = values 98 vars.value_offs = value_offs 99 vars.value_lens = value_lens 100 return NXT_OK 101} 102 103// ---- Var lookup -------------------------------------------------- 104// 105// Returns 1 if found + sets *out_off / *out_len. Returns 0 if absent. 106 107func nxt_find_var(vars: *NxTemplateVars, 108 name: *u8, name_n: i64, 109 out_off: *i64, out_len: *i64) -> i64 { 110 if vars == (0 as *NxTemplateVars) { return 0 } 111 var i: i64 = 0 112 while i < vars.n { 113 let ln: i64 = vars.name_lens[i] 114 if ln == name_n { 115 let lo: i64 = vars.name_offs[i] 116 var eq: i64 = 1 117 var j: i64 = 0 118 while j < ln { 119 if vars.names[lo + j] != name[j] { eq = 0; j = ln } 120 j = j + 1 121 } 122 if eq == 1 { 123 *out_off = vars.value_offs[i] 124 *out_len = vars.value_lens[i] 125 return 1 126 } 127 } 128 i = i + 1 129 } 130 return 0 131} 132 133// ---- Whitespace + identifier scanning ---------------------------- 134 135func nxt_is_space(b: i64) -> i64 { 136 if b == 0x20 { return 1 } 137 if b == 0x09 { return 1 } 138 return 0 139} 140 141func nxt_is_ident_char(b: i64) -> i64 { 142 if b >= 0x30 && b <= 0x39 { return 1 } // 0-9 143 if b >= 0x41 && b <= 0x5a { return 1 } // A-Z 144 if b >= 0x61 && b <= 0x7a { return 1 } // a-z 145 if b == 0x5f { return 1 } // _ 146 if b == 0x2e { return 1 } // . (allow dotted names like page.title) 147 return 0 148} 149 150func nxt_skip_ws(tmpl: *u8, end: i64, start: i64) -> i64 { 151 var p: i64 = start 152 while p < end { 153 if nxt_is_space(tmpl[p] as i64) == 0 { return p } 154 p = p + 1 155 } 156 return end 157} 158 159// Scan an identifier; returns offset of the byte AFTER the identifier. 160func nxt_scan_ident(tmpl: *u8, end: i64, start: i64) -> i64 { 161 var p: i64 = start 162 while p < end { 163 if nxt_is_ident_char(tmpl[p] as i64) == 0 { return p } 164 p = p + 1 165 } 166 return end 167} 168 169func nxt_bytes_eq(a: *u8, b: *u8, n: i64) -> i64 { 170 var i: i64 = 0 171 while i < n { 172 if a[i] != b[i] { return 0 } 173 i = i + 1 174 } 175 return 1 176} 177 178// ---- Modifier name -> nx_html_render context --------------------- 179// 180// Returns NXH_CTX_* value, or -1 if unknown. 181 182func nxt_modifier_to_ctx(mod: *u8, mod_n: i64) -> i64 { 183 if mod_n == 4 { 184 if nxt_bytes_eq(mod, "text" as *u8, 4) == 1 { return NXH_CTX_TEXT } 185 if nxt_bytes_eq(mod, "attr" as *u8, 4) == 1 { return NXH_CTX_ATTR_DQ } 186 } 187 if mod_n == 3 { 188 if nxt_bytes_eq(mod, "url" as *u8, 3) == 1 { return NXH_CTX_URL } 189 if nxt_bytes_eq(mod, "raw" as *u8, 3) == 1 { return NXH_CTX_RAW } 190 } 191 return -1 192} 193 194// ---- Template render -------------------------------------------- 195// 196// Walks template[0..tmpl_n], copying literal bytes to out and 197// substituting {{ var_name [| modifier] }} via nx_html_render. 198// 199// Caller pattern: 200// 201// let vars: *NxTemplateVars = ... 202// nx_template_vars_init(vars, ...) 203// nx_http_template_render(tmpl, tmpl_n, vars, out, out_cap, &out_n) 204 205func nx_http_template_render( 206 tmpl: *u8, tmpl_n: i64, 207 vars: *NxTemplateVars, 208 out: *u8, out_cap: i64, 209 out_n: *i64) -> i64 { 210 if tmpl == (0 as *u8) { return NXT_BAD_ARG } 211 if vars == (0 as *NxTemplateVars) { return NXT_BAD_ARG } 212 if out == (0 as *u8) { return NXT_BAD_ARG } 213 if out_n == (0 as *i64) { return NXT_BAD_ARG } 214 if tmpl_n < 0 { return NXT_BAD_ARG } 215 if out_cap <= 0 { return NXT_BAD_ARG } 216 217 var p: i64 = 0 218 var off: i64 = 0 219 while p < tmpl_n { 220 // Detect `{{` opener. 221 if p + 1 < tmpl_n { 222 if tmpl[p] == 0x7b as u8 { 223 if tmpl[p + 1] == 0x7b as u8 { 224 // Found `{{`. Parse: ws? ident ws? (`|` ws? mod ws?)? `}}` 225 let after_open: i64 = nxt_skip_ws(tmpl, tmpl_n, p + 2) 226 if after_open >= tmpl_n { return NXT_BAD_TEMPLATE } 227 let name_end: i64 = nxt_scan_ident(tmpl, tmpl_n, after_open) 228 if name_end == after_open { return NXT_BAD_TEMPLATE } 229 let name_ptr: *u8 = ((tmpl as i64) + after_open) as *u8 230 let name_len: i64 = name_end - after_open 231 // Optional modifier. 232 var ctx: i64 = NXH_CTX_TEXT 233 var after_mod: i64 = nxt_skip_ws(tmpl, tmpl_n, name_end) 234 if after_mod >= tmpl_n { return NXT_BAD_TEMPLATE } 235 if tmpl[after_mod] == 0x7c as u8 { 236 // `|` -- modifier. 237 let mod_start: i64 = nxt_skip_ws(tmpl, tmpl_n, after_mod + 1) 238 if mod_start >= tmpl_n { return NXT_BAD_TEMPLATE } 239 let mod_end: i64 = nxt_scan_ident(tmpl, tmpl_n, mod_start) 240 let mod_ptr: *u8 = ((tmpl as i64) + mod_start) as *u8 241 let mod_n: i64 = mod_end - mod_start 242 let ctx_id: i64 = nxt_modifier_to_ctx(mod_ptr, mod_n) 243 if ctx_id < 0 { return NXT_BAD_MODIFIER } 244 ctx = ctx_id 245 after_mod = nxt_skip_ws(tmpl, tmpl_n, mod_end) 246 } 247 // Expect `}}`. 248 if after_mod + 2 > tmpl_n { return NXT_BAD_TEMPLATE } 249 if tmpl[after_mod] != 0x7d as u8 { return NXT_BAD_TEMPLATE } 250 if tmpl[after_mod + 1] != 0x7d as u8 { return NXT_BAD_TEMPLATE } 251 let after_close: i64 = after_mod + 2 252 253 // Resolve var. 254 let v_off: *i64 = sys_mmap(8) as *i64 255 let v_len: *i64 = sys_mmap(8) as *i64 256 if nxt_find_var(vars, name_ptr, name_len, v_off, v_len) != 1 { 257 return NXT_VAR_NOT_FOUND 258 } 259 let v_ptr: *u8 = ((vars.values as i64) + v_off[0]) as *u8 260 261 // Substitute via nx_html_render typed-context escape. 262 let esc_rc: i64 = nx_html_escape(out, &off, out_cap, 263 v_ptr, v_len[0], ctx) 264 if esc_rc != NXH_OK { return NXT_OOM_BUFFER } 265 266 p = after_close 267 } else { 268 // Single `{`; literal. 269 if off >= out_cap { return NXT_OOM_BUFFER } 270 out[off] = tmpl[p] 271 off = off + 1 272 p = p + 1 273 } 274 } else { 275 if off >= out_cap { return NXT_OOM_BUFFER } 276 out[off] = tmpl[p] 277 off = off + 1 278 p = p + 1 279 } 280 } else { 281 if off >= out_cap { return NXT_OOM_BUFFER } 282 out[off] = tmpl[p] 283 off = off + 1 284 p = p + 1 285 } 286 } 287 *out_n = off 288 return NXT_OK 289} 290 291// (sys_mmap used internally for nxt_find_var's out-pointer scratch; 292// imported at top of file.)