code wiki / _hdl_build / nx_html_sanitize_test.nx

nx_html_sanitize_test.nx source

↩ module page · 115 lines · 7522 B

1// nx_html_sanitize_test.nx -- ENGINEER verification of the CMS sanitizer keystone: byte-exact KATs 2// over REAL stored-XSS vectors (script, event handlers, javascript: URLs, case games, unterminated 3// tags, style content, quote-embedded '>'), plus the escape-on-render path. Exit 0 = all green. 4import "nx_gate_verdict.nx" 5import "nx_html_sanitize.nx" 6import "nx_syscalls.nx" 7 8func ht_w(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 } 9func ht_len(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 10 11// run one sanitize KAT: returns 1 on byte-exact match, prints a FAIL line otherwise 12func ht_kat(id: i64, inp: *u8, want: *u8) -> i64 { 13 let out: *u8 = sys_mmap(4096) 14 let ol: i64 = hs_sanitize(inp, ht_len(inp), out, 4095) 15 let wl: i64 = ht_len(want) 16 var ok: i64 = 1 17 if ol != wl { ok = 0 } 18 if ok == 1 { 19 var i: i64 = 0 20 while i < ol { if (out[i] as i64) != (want[i] as i64) { ok = 0 } i = i + 1 } 21 } 22 if ok == 0 { 23 ht_w("FAIL KAT " as *u8) 24 let d: *u8 = sys_mmap(8); d[0] = (48 + id/10) as u8; d[1] = (48 + id%10) as u8; d[2] = 0 as u8 25 ht_w(d); ht_w(": got [" as *u8); sys_write(1, out, ol); ht_w("] want [" as *u8); ht_w(want); ht_w("]\n" as *u8) 26 } 27 return ok 28} 29 30// seq677: the span-adapter every HTML emitter is meant to share MUST agree with hs_escape byte-for-byte, 31// and must neutralise the exact payload that was LIVE on /standup (journal text containing <main>/<nav>). 32// Non-vacuous by construction: it also asserts a mid-buffer span (offset preserved) and control->space. 33func ht_kat_span(id: i64, inp: *u8, want: *u8) -> i64 { 34 let a: *u8 = sys_mmap(4096) 35 let n: i64 = ht_len(inp) 36 let al: i64 = hs_escape(inp, n, a, 4095) 37 let b: *u8 = sys_mmap(4096) 38 let bl: i64 = hs_cat_esc_span(b, 0, inp, 0, n, 4095) 39 var ok: i64 = 1 40 if al != bl { ok = 0 } 41 if ok == 1 { var i: i64 = 0; while i < al { if (a[i] as i64) != (b[i] as i64) { ok = 0 } i = i + 1 } } 42 let wl: i64 = ht_len(want) 43 if bl != wl { ok = 0 } 44 if ok == 1 { var j: i64 = 0; while j < wl { if (b[j] as i64) != (want[j] as i64) { ok = 0 } j = j + 1 } } 45 let c: *u8 = sys_mmap(4096) 46 var co: i64 = 0 47 co = hs_emit(c, co, 4095, "PRE:" as *u8) 48 co = hs_cat_esc_span(c, co, inp, 0, n, 4095) 49 if co != 4 + bl { ok = 0 } 50 if ok == 1 { var k: i64 = 0; while k < bl { if (c[4+k] as i64) != (b[k] as i64) { ok = 0 } k = k + 1 } } 51 if ok == 0 { ht_w("FAIL SPAN KAT\n" as *u8) } 52 return ok 53} 54 55func ht_kat_esc(id: i64, inp: *u8, want: *u8) -> i64 { 56 let out: *u8 = sys_mmap(4096) 57 let ol: i64 = hs_escape(inp, ht_len(inp), out, 4095) 58 let wl: i64 = ht_len(want) 59 var ok: i64 = 1 60 if ol != wl { ok = 0 } 61 if ok == 1 { 62 var i: i64 = 0 63 while i < ol { if (out[i] as i64) != (want[i] as i64) { ok = 0 } i = i + 1 } 64 } 65 if ok == 0 { ht_w("FAIL ESC KAT\n" as *u8) } 66 return ok 67} 68 69func main() -> i64 { 70 // MIGRATED OFF A HAND-ROLLED VERDICT 2026-08-14. This gate was PROMOTED, REGISTERED, AUTHORISED -- 71 // and its live binary was 7.7 DAYS OLDER THAN ITS SOURCE (26,819 B live against 43,248 B rebuilt). 72 // The cause was this very defect: the D001 promote door refuses a gate that rolls its own verdict, 73 // so an unmigrated gate CANNOT BE RE-PROMOTED and its live copy freezes at whatever predates the 74 // door, while /api/gate_run keeps returning a cheerful green from the fossil. 75 // -- A GREEN FROM A STALE ARTIFACT IS INDISTINGUISHABLE FROM A GREEN FROM YOUR CHANGE, and here the 76 // staleness was manufactured by the guard meant to raise quality. 77 // The KAT vectors below are deliberately untouched; only the verdict changed. 78 gv_head("nx_html_sanitize gate -- byte-exact KATs over real stored-XSS vectors" as *u8) 79 let ctr: *i64 = gv_ctr() 80 var pass: i64 = 0 81 var total: i64 = 0 82 total = total + 1; pass = pass + ht_kat(1, "<script>alert(1)</script>Hello" as *u8, "Hello" as *u8) 83 total = total + 1; pass = pass + ht_kat(2, "<img src=x onerror=alert(1)>Hi" as *u8, "Hi" as *u8) 84 total = total + 1; pass = pass + ht_kat(3, "<a href=\"javascript:alert(1)\">x</a>" as *u8, "<a>x</a>" as *u8) 85 total = total + 1; pass = pass + ht_kat(4, "<a href=\"https://ok.com\">x</a>" as *u8, "<a href=\"https://ok.com\">x</a>" as *u8) 86 total = total + 1; pass = pass + ht_kat(5, "<b onmouseover=evil()>bold</b>" as *u8, "<b>bold</b>" as *u8) 87 total = total + 1; pass = pass + ht_kat(6, "a < b" as *u8, "a &lt; b" as *u8) 88 total = total + 1; pass = pass + ht_kat(7, "<P>case</P>" as *u8, "<p>case</p>" as *u8) 89 total = total + 1; pass = pass + ht_kat(8, "<div><b>x</b></div>" as *u8, "<b>x</b>" as *u8) 90 total = total + 1; pass = pass + ht_kat(9, "ok<b" as *u8, "ok&lt;b" as *u8) 91 total = total + 1; pass = pass + ht_kat(10, "<STYLE>p{color:red}</STYLE>ok" as *u8, "ok" as *u8) 92 total = total + 1; pass = pass + ht_kat(11, "<a href=javascript:x>y</a>" as *u8, "<a>y</a>" as *u8) 93 total = total + 1; pass = pass + ht_kat(12, "<a href=\"/about\">y</a>" as *u8, "<a href=\"/about\">y</a>" as *u8) 94 total = total + 1; pass = pass + ht_kat(13, "<a href=\"java script:alert(1)\">y</a>" as *u8, "<a>y</a>" as *u8) 95 total = total + 1; pass = pass + ht_kat(14, "<img alt=\"a>b\" onerror=alert(1)>z" as *u8, "z" as *u8) 96 total = total + 1; pass = pass + ht_kat(15, "<SCRIPT SRC=evil.js></SCRIPT>tail" as *u8, "tail" as *u8) 97 total = total + 1; pass = pass + ht_kat(16, "<ul><li>one</li></ul>" as *u8, "<ul><li>one</li></ul>" as *u8) 98 total = total + 1; pass = pass + ht_kat(17, "<A HREF='HTTPS://X.Y'>u</A>" as *u8, "<a href=\"HTTPS://X.Y\">u</a>" as *u8) 99 total = total + 1; pass = pass + ht_kat(18, "<form action=\"/steal\"><input></form>f" as *u8, "f" as *u8) 100 total = total + 1; pass = pass + ht_kat_esc(19, "Smith & Wesson <LLP> \"quote\" 'tick'" as *u8, "Smith &amp; Wesson &lt;LLP&gt; &quot;quote&quot; &#39;tick&#39;" as *u8) 101 total = total + 1; pass = pass + ht_kat_span(20, "flags <main>/<nav> landmark-loss & \"x\" 'y'" as *u8, "flags &lt;main&gt;/&lt;nav&gt; landmark-loss &amp; &quot;x&quot; &#39;y&#39;" as *u8) 102 ht_w("SANITIZE KATs pass=" as *u8) 103 let d: *u8 = sys_mmap(8); d[0] = (48 + pass/10) as u8; d[1] = (48 + pass%10) as u8; d[2] = 47 as u8; d[3] = (48 + total/10) as u8; d[4] = (48 + total%10) as u8; d[5] = 10 as u8; d[6] = 0 as u8 104 ht_w(d) 105 gv_check("every KAT matches its expected output BYTE-EXACTLY" as *u8, pass == total, ctr) 106 // BIND THE AGGREGATE TO ITS DENOMINATOR. pass==total is trivially true when total is 0, so a KAT 107 // silently dropped from the list would otherwise read as a clean pass on a shorter suite. 108 gv_check("the full KAT vector set actually ran" as *u8, total == 20, ctr) 109 // WHY THIS SUITE IS NOT VACUOUS, recorded because it is the load-bearing property: it is NOT a pile 110 // of strip-everything tests. KATs 4, 12, 16 and 17 require safe markup to SURVIVE untouched (an 111 // https href, a relative href, a list, an uppercase anchor), so a sanitizer that returned the empty 112 // string for every input -- the trivial way to pass an XSS suite -- fails four of these twenty. 113 return gv_verdict("nx_html_sanitize_test" as *u8, ctr, 114 "20 byte-exact KATs: script, event handlers, javascript and java-script URLs, case games, unterminated tags, style bodies, quote-embedded gt, form theft, plus preserve-safe-markup positives and the escape and span-adapter paths" as *u8) 115}