code wiki / (root) / nx_html_render.nx

nx_html_render.nx source

↩ module page · 418 lines · 15426 B

1// nx_html_render.nx -- typed-context HTML render primitive. 2// 3// Named by nx_bug_tape_intelligence.sh F1 rule (2026-05-16) as the 4// rank-2 next ship-order: CWE-79 Cross-Site Scripting. Phase-2 5// critical primitive for the audit-dashboard arc (per 6// docs/NISHI_AUDIT_DASHBOARD_ROADMAP.md). 7// 8// Why "typed context" instead of "escape this string": 9// 10// The XSS bug class isn't "you forgot to escape". It's "you 11// escaped for the wrong context." HTML body text needs `<` -> `&lt;`; 12// attribute-value-double-quoted needs `"` -> `&quot;`; 13// URL context needs %-encoding (RFC 3986); JavaScript string 14// context needs `\` + control-char escaping; <style> CSS context 15// needs yet another escape table. A primitive that takes a 16// single "value" parameter without specifying which context it's 17// being emitted into is a perpetual XSS-vector machine. 18// 19// Substrate's structural prevention: sealed-enum NxHtmlContext + 20// every emit takes a context + the escape is dispatched per context 21// at the boundary, never inferred from string content. 22// 23// Sealed enum NxHtmlContext (closes the XSS class): 24// NXH_CTX_TEXT <body> text, between tags 25// NXH_CTX_ATTR_DQ inside an attribute="..." (double-quoted) 26// NXH_CTX_ATTR_SQ inside an attribute='...' (single-quoted) 27// NXH_CTX_URL href / src / action attribute (URL value) 28// NXH_CTX_COMMENT inside <!-- ... --> 29// NXH_CTX_CDATA inside <![CDATA[...]]> (XHTML) 30// NXH_CTX_RAW EXPLICIT raw-html bypass; caller asserts 31// the bytes are already safe (e.g., a 32// pre-rendered nested fragment). Sealed- 33// enum status: AUDIT-WHITELIST. 34// 35// Sealed enum NxHtmlNodeKind: 36// NXH_NODE_ELEMENT <tag attrs>children</tag> 37// NXH_NODE_TEXT text content (auto-escaped per ctx) 38// NXH_NODE_RAW caller-asserted-safe raw bytes (audited) 39// NXH_NODE_COMMENT <!-- ... --> (text auto-escaped) 40// 41// Per cardinal feedback-licensing-absorb-vs-copy-discipline: this 42// is ORIGINAL substrate; no copyrighted templates copied. 43// 44// Per cardinal feedback-user-owns-every-bit: every output goes to 45// a caller-provided buffer. No implicit allocation. No globals. 46// 47// nx_capability_claims: 48// needs: [sealed_enum, integer_compare, byte_ops] 49// provides: [html_render_typed_context, xss_structural_prevention] 50// safety: [no_unchecked_deref, no_floating_point, no_syscall, 51// bit_equal_reproducible, target_agnostic, 52// context_validated_at_emit] 53// verdict: [sealed_enum_7_context, sealed_enum_4_nodekind] 54// license: ORIGINAL 55// kind: racing_crew_specialist 56// layer: L2 (transform over L1 byte-buffer container) 57// sss: [S6 sealed-enum verdicts; S11 timing-mode declared] 58// cwe: [CWE-79 XSS structural prevention] 59 60// ---- Sealed enum: NxHtmlContext ----------------------------------- 61 62const NXH_CTX_TEXT: i64 = 0 63const NXH_CTX_ATTR_DQ: i64 = 1 64const NXH_CTX_ATTR_SQ: i64 = 2 65const NXH_CTX_URL: i64 = 3 66const NXH_CTX_COMMENT: i64 = 4 67const NXH_CTX_CDATA: i64 = 5 68const NXH_CTX_RAW: i64 = 6 69const NXH_CTX_N: i64 = 7 70 71func nxh_ctx_is_valid(c: i64) -> i64 { 72 if c < 0 { return 0 } 73 if c >= NXH_CTX_N { return 0 } 74 return 1 75} 76 77func nxh_ctx_name(c: i64) -> *u8 { 78 if c == NXH_CTX_TEXT { return "TEXT" as *u8 } 79 if c == NXH_CTX_ATTR_DQ { return "ATTR_DQ" as *u8 } 80 if c == NXH_CTX_ATTR_SQ { return "ATTR_SQ" as *u8 } 81 if c == NXH_CTX_URL { return "URL" as *u8 } 82 if c == NXH_CTX_COMMENT { return "COMMENT" as *u8 } 83 if c == NXH_CTX_CDATA { return "CDATA" as *u8 } 84 if c == NXH_CTX_RAW { return "RAW" as *u8 } 85 return "INVALID" as *u8 86} 87 88// ---- Sealed enum: NxHtmlNodeKind ---------------------------------- 89 90const NXH_NODE_ELEMENT: i64 = 0 91const NXH_NODE_TEXT: i64 = 1 92const NXH_NODE_RAW: i64 = 2 93const NXH_NODE_COMMENT: i64 = 3 94const NXH_NODE_N: i64 = 4 95 96func nxh_node_is_valid(k: i64) -> i64 { 97 if k < 0 { return 0 } 98 if k >= NXH_NODE_N { return 0 } 99 return 1 100} 101 102// ---- Render verdict (sealed enum) --------------------------------- 103 104const NXH_OK: i64 = 0 105const NXH_OOM_BUFFER: i64 = 1 106const NXH_BAD_CTX: i64 = 2 107const NXH_BAD_INPUT: i64 = 3 108const NXH_BAD_URL: i64 = 4 109const NXH_VERDICT_N: i64 = 5 110 111func nxh_verdict_is_valid(v: i64) -> i64 { 112 if v < 0 { return 0 } 113 if v >= NXH_VERDICT_N { return 0 } 114 return 1 115} 116 117// ---- Internal helpers --------------------------------------------- 118 119// Append a single byte to out[*off] with bounds-check. Returns 0 on 120// success, NXH_OOM_BUFFER if the write would exceed out_cap. 121func nxh_put(out: *u8, off: *i64, cap: i64, b: i64) -> i64 { 122 if *off >= cap { return NXH_OOM_BUFFER } 123 out[*off] = b as u8 124 *off = *off + 1 125 return NXH_OK 126} 127 128// Append a NUL-terminated literal to out[*off..]; bounds-checked. 129func nxh_put_lit(out: *u8, off: *i64, cap: i64, lit: *u8) -> i64 { 130 var i: i64 = 0 131 while lit[i] != 0 { 132 let rc: i64 = nxh_put(out, off, cap, lit[i] as i64) 133 if rc != NXH_OK { return rc } 134 i = i + 1 135 } 136 return NXH_OK 137} 138 139// Append a sized buffer (no NUL required). 140func nxh_put_bytes(out: *u8, off: *i64, cap: i64, src: *u8, n: i64) -> i64 { 141 var i: i64 = 0 142 while i < n { 143 let rc: i64 = nxh_put(out, off, cap, src[i] as i64) 144 if rc != NXH_OK { return rc } 145 i = i + 1 146 } 147 return NXH_OK 148} 149 150// Predicate: byte safe for URL context (RFC 3986 unreserved + reserved 151// punctuation). ASCII letters / digits / `-_.~` + a tightly-bounded 152// punctuation set used in URL path segments and query keys. Anything 153// else gets %HH-encoded. 154func nxh_url_safe(b: i64) -> i64 { 155 if b >= 0x30 && b <= 0x39 { return 1 } // 0-9 156 if b >= 0x41 && b <= 0x5a { return 1 } // A-Z 157 if b >= 0x61 && b <= 0x7a { return 1 } // a-z 158 if b == 0x2d { return 1 } // - 159 if b == 0x5f { return 1 } // _ 160 if b == 0x2e { return 1 } // . 161 if b == 0x7e { return 1 } // ~ 162 // Reserved-but-commonly-permitted in path segments: 163 if b == 0x2f { return 1 } // / 164 if b == 0x3a { return 1 } // : 165 if b == 0x3f { return 1 } // ? 166 if b == 0x3d { return 1 } // = 167 if b == 0x26 { return 1 } // & 168 if b == 0x23 { return 1 } // # 169 return 0 170} 171 172// Hex nibble -> ASCII (uppercase). 173func nxh_hex_nibble(n: i64) -> i64 { 174 if n < 10 { return 0x30 + n } // '0' + n 175 return 0x41 + (n - 10) // 'A' + (n-10) 176} 177 178// ---- The escape primitive (the load-bearing entry point) ---------- 179// 180// Writes `src[0..n]` into `out[*off..]` with the escaping rules for 181// the given context. Returns NXH_OK on success or NXH_OOM_BUFFER / 182// NXH_BAD_CTX / NXH_BAD_INPUT / NXH_BAD_URL on failure. 183// 184// The escape tables (per context): 185// 186// TEXT & < > -> &amp; &lt; &gt; 187// (apostrophe + quote optional but harmless) 188// ATTR_DQ & < > " -> &amp; &lt; &gt; &quot; 189// ATTR_SQ & < > ' -> &amp; &lt; &gt; &#x27; 190// URL per nxh_url_safe; unsafe bytes -> %HH (uppercase) 191// COMMENT must not contain `--` sequence; reject as BAD_INPUT 192// CDATA must not contain `]]>` sequence; reject as BAD_INPUT 193// RAW verbatim copy; caller-asserted safe 194// 195// Per substrate cardinal: emission FAILS LOUD on context mismatch 196// (sealed-enum verdict). Never silently "fixes up" input. 197 198func nx_html_escape(out: *u8, off: *i64, cap: i64, 199 src: *u8, n: i64, ctx: i64) -> i64 { 200 if nxh_ctx_is_valid(ctx) != 1 { return NXH_BAD_CTX } 201 if n < 0 { return NXH_BAD_INPUT } 202 if out == (0 as *u8) { return NXH_BAD_INPUT } 203 if off == (0 as *i64) { return NXH_BAD_INPUT } 204 if src == (0 as *u8) && n > 0 { return NXH_BAD_INPUT } 205 206 if ctx == NXH_CTX_RAW { 207 return nxh_put_bytes(out, off, cap, src, n) 208 } 209 210 if ctx == NXH_CTX_COMMENT { 211 // Refuse if `--` appears in src; HTML comments cannot contain it. 212 var i: i64 = 0 213 while i < n - 1 { 214 if src[i] == 0x2d && src[i+1] == 0x2d { return NXH_BAD_INPUT } 215 i = i + 1 216 } 217 return nxh_put_bytes(out, off, cap, src, n) 218 } 219 220 if ctx == NXH_CTX_CDATA { 221 // Refuse if `]]>` appears in src. 222 var i: i64 = 0 223 while i < n - 2 { 224 if src[i] == 0x5d && src[i+1] == 0x5d && src[i+2] == 0x3e { 225 return NXH_BAD_INPUT 226 } 227 i = i + 1 228 } 229 return nxh_put_bytes(out, off, cap, src, n) 230 } 231 232 if ctx == NXH_CTX_URL { 233 var i: i64 = 0 234 while i < n { 235 let b: i64 = src[i] as i64 236 if nxh_url_safe(b) == 1 { 237 let rc: i64 = nxh_put(out, off, cap, b) 238 if rc != NXH_OK { return rc } 239 } else { 240 let rc1: i64 = nxh_put(out, off, cap, 0x25) // % 241 if rc1 != NXH_OK { return rc1 } 242 let rc2: i64 = nxh_put(out, off, cap, nxh_hex_nibble((b >> 4) & 0xf)) 243 if rc2 != NXH_OK { return rc2 } 244 let rc3: i64 = nxh_put(out, off, cap, nxh_hex_nibble(b & 0xf)) 245 if rc3 != NXH_OK { return rc3 } 246 } 247 i = i + 1 248 } 249 return NXH_OK 250 } 251 252 // TEXT / ATTR_DQ / ATTR_SQ -- ampersand-entity escaping per context. 253 var i: i64 = 0 254 while i < n { 255 let b: i64 = src[i] as i64 256 var rc: i64 = NXH_OK 257 if b == 0x26 { 258 rc = nxh_put_lit(out, off, cap, "&amp;" as *u8) 259 } else { 260 if b == 0x3c { 261 rc = nxh_put_lit(out, off, cap, "&lt;" as *u8) 262 } else { 263 if b == 0x3e { 264 rc = nxh_put_lit(out, off, cap, "&gt;" as *u8) 265 } else { 266 if ctx == NXH_CTX_ATTR_DQ && b == 0x22 { 267 rc = nxh_put_lit(out, off, cap, "&quot;" as *u8) 268 } else { 269 if ctx == NXH_CTX_ATTR_SQ && b == 0x27 { 270 rc = nxh_put_lit(out, off, cap, "&#x27;" as *u8) 271 } else { 272 rc = nxh_put(out, off, cap, b) 273 } 274 } 275 } 276 } 277 } 278 if rc != NXH_OK { return rc } 279 i = i + 1 280 } 281 return NXH_OK 282} 283 284// ---- Element emission helpers ------------------------------------- 285// 286// Caller pattern: 287// 288// var off: i64 = 0 289// let cap: i64 = 4096 290// let buf: *u8 = sys_mmap(cap) 291// nx_html_open_tag(buf, &off, cap, "div" as *u8, 3) 292// nx_html_attr(buf, &off, cap, "class" as *u8, 5, "card" as *u8, 4) 293// nx_html_close_open_tag(buf, &off, cap) 294// nx_html_text(buf, &off, cap, user_supplied, user_len) 295// nx_html_end_tag(buf, &off, cap, "div" as *u8, 3) 296// 297// Sealed-enum NXH_OK / NXH_OOM_BUFFER returned at every step. 298 299// Predicate: ASCII alphanumeric byte (for tag + attribute names). 300func nxh_is_alnum(b: i64) -> i64 { 301 if b >= 0x30 && b <= 0x39 { return 1 } 302 if b >= 0x41 && b <= 0x5a { return 1 } 303 if b >= 0x61 && b <= 0x7a { return 1 } 304 return 0 305} 306 307// Predicate: ASCII alphanumeric OR dash (for attribute names). 308func nxh_is_attr_name_char(b: i64) -> i64 { 309 if nxh_is_alnum(b) == 1 { return 1 } 310 if b == 0x2d { return 1 } // - 311 return 0 312} 313 314// Emit `<tag` (no closing `>` -- attributes follow). Caller calls 315// nx_html_close_open_tag before children. 316func nx_html_open_tag(out: *u8, off: *i64, cap: i64, 317 tag: *u8, tag_n: i64) -> i64 { 318 if tag_n <= 0 { return NXH_BAD_INPUT } 319 let rc1: i64 = nxh_put(out, off, cap, 0x3c) // < 320 if rc1 != NXH_OK { return rc1 } 321 // Tag-name must be ASCII alphanumeric (constrained per HTML5). 322 var i: i64 = 0 323 while i < tag_n { 324 let b: i64 = tag[i] as i64 325 if nxh_is_alnum(b) == 0 { return NXH_BAD_INPUT } 326 let rc: i64 = nxh_put(out, off, cap, b) 327 if rc != NXH_OK { return rc } 328 i = i + 1 329 } 330 return NXH_OK 331} 332 333// Emit ` name="value"` (with attribute-context escaping of value). 334func nx_html_attr(out: *u8, off: *i64, cap: i64, 335 name: *u8, name_n: i64, 336 value: *u8, value_n: i64) -> i64 { 337 if name_n <= 0 { return NXH_BAD_INPUT } 338 let rc_sp: i64 = nxh_put(out, off, cap, 0x20) 339 if rc_sp != NXH_OK { return rc_sp } 340 // Attribute name: ASCII alphanumeric + `-`. 341 var i: i64 = 0 342 while i < name_n { 343 let b: i64 = name[i] as i64 344 if nxh_is_attr_name_char(b) == 0 { return NXH_BAD_INPUT } 345 let rc: i64 = nxh_put(out, off, cap, b) 346 if rc != NXH_OK { return rc } 347 i = i + 1 348 } 349 let rc_eq: i64 = nxh_put(out, off, cap, 0x3d) // = 350 if rc_eq != NXH_OK { return rc_eq } 351 let rc_q1: i64 = nxh_put(out, off, cap, 0x22) // " 352 if rc_q1 != NXH_OK { return rc_q1 } 353 let rc_v: i64 = nx_html_escape(out, off, cap, value, value_n, NXH_CTX_ATTR_DQ) 354 if rc_v != NXH_OK { return rc_v } 355 return nxh_put(out, off, cap, 0x22) // " 356} 357 358// Emit ` href="<url>"` with URL-context escape on the value. 359func nx_html_attr_url(out: *u8, off: *i64, cap: i64, 360 name: *u8, name_n: i64, 361 url: *u8, url_n: i64) -> i64 { 362 if name_n <= 0 { return NXH_BAD_INPUT } 363 let rc_sp: i64 = nxh_put(out, off, cap, 0x20) 364 if rc_sp != NXH_OK { return rc_sp } 365 let rc_n: i64 = nxh_put_bytes(out, off, cap, name, name_n) 366 if rc_n != NXH_OK { return rc_n } 367 let rc_eq: i64 = nxh_put(out, off, cap, 0x3d) 368 if rc_eq != NXH_OK { return rc_eq } 369 let rc_q1: i64 = nxh_put(out, off, cap, 0x22) 370 if rc_q1 != NXH_OK { return rc_q1 } 371 let rc_u: i64 = nx_html_escape(out, off, cap, url, url_n, NXH_CTX_URL) 372 if rc_u != NXH_OK { return rc_u } 373 return nxh_put(out, off, cap, 0x22) 374} 375 376// Close the opening tag with `>`. Use after open_tag + attrs. 377func nx_html_close_open_tag(out: *u8, off: *i64, cap: i64) -> i64 { 378 return nxh_put(out, off, cap, 0x3e) // > 379} 380 381// Self-closing variant: `/>`. Use for void elements (br/hr/img/...). 382func nx_html_self_close(out: *u8, off: *i64, cap: i64) -> i64 { 383 let rc1: i64 = nxh_put(out, off, cap, 0x2f) // / 384 if rc1 != NXH_OK { return rc1 } 385 return nxh_put(out, off, cap, 0x3e) 386} 387 388// Emit </tag>. Tag is bounds-checked to ASCII alphanumeric. 389func nx_html_end_tag(out: *u8, off: *i64, cap: i64, 390 tag: *u8, tag_n: i64) -> i64 { 391 let rc1: i64 = nxh_put(out, off, cap, 0x3c) // < 392 if rc1 != NXH_OK { return rc1 } 393 let rc2: i64 = nxh_put(out, off, cap, 0x2f) // / 394 if rc2 != NXH_OK { return rc2 } 395 var i: i64 = 0 396 while i < tag_n { 397 let b: i64 = tag[i] as i64 398 if nxh_is_alnum(b) == 0 { return NXH_BAD_INPUT } 399 let rc: i64 = nxh_put(out, off, cap, b) 400 if rc != NXH_OK { return rc } 401 i = i + 1 402 } 403 return nxh_put(out, off, cap, 0x3e) 404} 405 406// Emit text content (TEXT-context escaped). 407func nx_html_text(out: *u8, off: *i64, cap: i64, 408 src: *u8, n: i64) -> i64 { 409 return nx_html_escape(out, off, cap, src, n, NXH_CTX_TEXT) 410} 411 412// Emit a raw bypass (caller asserts the bytes are safe). Audit 413// every call site -- this is the XSS bypass we explicitly allow 414// for nested-fragment composition. 415func nx_html_raw(out: *u8, off: *i64, cap: i64, 416 src: *u8, n: i64) -> i64 { 417 return nx_html_escape(out, off, cap, src, n, NXH_CTX_RAW) 418}