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 < 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<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 & Wesson <LLP> "quote" 'tick'" as *u8)
101 total = total + 1; pass = pass + ht_kat_span(20, "flags <main>/<nav> landmark-loss & \"x\" 'y'" as *u8, "flags <main>/<nav> landmark-loss & "x" 'y'" 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}