nx_html_render_test.nx source
↩ module page · 202 lines · 7742 B
1// nx_html_render_test.nx -- smoke + XSS-attack-vector tests.
2//
3// Exercises every context + every escape rule + the bounds-check
4// + the known-XSS attack-string corpus.
5//
6// expect_exit: 0
7//
8// license_tier: ORIGINAL
9
10import "nx_syscalls_x86_64.nx"
11import "nx_html_render.nx"
12
13func bytes_eq(a: *u8, b: *u8, n: i64) -> i64 {
14 var i: i64 = 0
15 while i < n {
16 if a[i] != b[i] { return 0 }
17 i = i + 1
18 }
19 return 1
20}
21
22// Helper: render `escape(src, ctx)` into a fresh buffer + verify it
23// matches expected. Returns 0 on match, error-code-with-base on miss.
24func check_escape(src: *u8, src_n: i64, ctx: i64,
25 expected: *u8, expected_n: i64,
26 err_base: i64) -> i64 {
27 let buf: *u8 = sys_mmap(1024)
28 var off: i64 = 0
29 let rc: i64 = nx_html_escape(buf, &off, 1024, src, src_n, ctx)
30 if rc != NXH_OK { return err_base + 90 }
31 if off != expected_n { return err_base + 91 }
32 if bytes_eq(buf, expected, expected_n) != 1 { return err_base + 92 }
33 return 0
34}
35
36func main() -> i64 {
37 // ---- Context-enum validity gate ----
38 if nxh_ctx_is_valid(NXH_CTX_TEXT) != 1 { return 1 }
39 if nxh_ctx_is_valid(NXH_CTX_RAW) != 1 { return 2 }
40 if nxh_ctx_is_valid(NXH_CTX_N) != 0 { return 3 }
41 if nxh_ctx_is_valid(-1) != 0 { return 4 }
42
43 if nxh_node_is_valid(NXH_NODE_ELEMENT) != 1 { return 5 }
44 if nxh_node_is_valid(NXH_NODE_N) != 0 { return 6 }
45 if nxh_verdict_is_valid(NXH_OK) != 1 { return 7 }
46 if nxh_verdict_is_valid(NXH_VERDICT_N) != 0 { return 8 }
47
48 // ---- TEXT context: < > & escape ----
49 var rc: i64 = check_escape(
50 "hello <world> & friends" as *u8, 23, NXH_CTX_TEXT,
51 "hello <world> & friends" as *u8, 33, 100)
52 if rc != 0 { return rc }
53
54 // ---- ATTR_DQ context: & < > " ----
55 rc = check_escape(
56 "a&b<c>d\"e" as *u8, 9, NXH_CTX_ATTR_DQ,
57 "a&b<c>d"e" as *u8, 24, 200)
58 if rc != 0 { return rc }
59
60 // ---- ATTR_SQ context: & < > ' ----
61 rc = check_escape(
62 "a&b'c" as *u8, 5, NXH_CTX_ATTR_SQ,
63 "a&b'c" as *u8, 14, 300)
64 if rc != 0 { return rc }
65
66 // ---- URL context: %HH-encode unsafe bytes ----
67 rc = check_escape(
68 "hello world" as *u8, 11, NXH_CTX_URL,
69 "hello%20world" as *u8, 13, 400)
70 if rc != 0 { return rc }
71
72 rc = check_escape(
73 "a<b" as *u8, 3, NXH_CTX_URL,
74 "a%3Cb" as *u8, 5, 410)
75 if rc != 0 { return rc }
76
77 // URL: safe punctuation (/ : ? = & #) passes through
78 rc = check_escape(
79 "http://x/y?a=b&c=d" as *u8, 18, NXH_CTX_URL,
80 "http://x/y?a=b&c=d" as *u8, 18, 420)
81 if rc != 0 { return rc }
82
83 // ---- RAW context: verbatim copy (caller-asserted-safe) ----
84 rc = check_escape(
85 "<b>bold</b>" as *u8, 11, NXH_CTX_RAW,
86 "<b>bold</b>" as *u8, 11, 500)
87 if rc != 0 { return rc }
88
89 // ---- COMMENT context: rejects `--` ----
90 let buf: *u8 = sys_mmap(256)
91 var off: i64 = 0
92 let rc_c: i64 = nx_html_escape(buf, &off, 256,
93 "ok -- bad" as *u8, 9, NXH_CTX_COMMENT)
94 if rc_c != NXH_BAD_INPUT { return 600 }
95
96 // COMMENT: clean text passes
97 off = 0
98 let rc_c2: i64 = nx_html_escape(buf, &off, 256,
99 "all good" as *u8, 8, NXH_CTX_COMMENT)
100 if rc_c2 != NXH_OK { return 610 }
101 if off != 8 { return 611 }
102
103 // ---- CDATA context: rejects `]]>` ----
104 off = 0
105 let rc_cd: i64 = nx_html_escape(buf, &off, 256,
106 "ok ]]> bad" as *u8, 10, NXH_CTX_CDATA)
107 if rc_cd != NXH_BAD_INPUT { return 700 }
108
109 // ---- BAD_CTX path ----
110 off = 0
111 let rc_bc: i64 = nx_html_escape(buf, &off, 256,
112 "x" as *u8, 1, 99)
113 if rc_bc != NXH_BAD_CTX { return 800 }
114
115 // ---- OOM_BUFFER path ----
116 let small: *u8 = sys_mmap(4)
117 off = 0
118 let rc_oom: i64 = nx_html_escape(small, &off, 4,
119 "abcdefghij" as *u8, 10, NXH_CTX_TEXT)
120 if rc_oom != NXH_OOM_BUFFER { return 810 }
121
122 // ---- Known XSS attack vectors -- all must be NEUTRALIZED ----
123
124 // 1. <script>alert(1)</script> in TEXT context
125 let xss1_src: *u8 = "<script>alert(1)</script>" as *u8
126 let xss1_exp: *u8 = "<script>alert(1)</script>" as *u8
127 rc = check_escape(xss1_src, 25, NXH_CTX_TEXT, xss1_exp, 37, 900)
128 if rc != 0 { return rc }
129
130 // 2. attribute-breakout via " in ATTR_DQ
131 let xss2_src: *u8 = "x\" onerror=alert(1) x=\"" as *u8
132 let xss2_exp: *u8 = "x" onerror=alert(1) x="" as *u8
133 rc = check_escape(xss2_src, 23, NXH_CTX_ATTR_DQ, xss2_exp, 33, 1000)
134 if rc != 0 { return rc }
135
136 // 3. javascript:alert(1) -- URL escape encodes `(` and `)` as %28
137 // %29, but the colon + alnum survive (URL context is permissive on
138 // path punctuation). Protection against javascript: scheme MUST
139 // happen at scheme-allow-list layer (queued: nx_url_scheme_
140 // allowlist). nx_html_render's job is byte-level context escape.
141 let xss3_src: *u8 = "javascript:alert(1)" as *u8
142 let xss3_exp: *u8 = "javascript:alert%281%29" as *u8
143 rc = check_escape(xss3_src, 19, NXH_CTX_URL, xss3_exp, 23, 1100)
144 if rc != 0 { return rc }
145
146 // 4. Open-tag injection in URL value: `<svg onload=...>` byte-encoded.
147 let xss4_src: *u8 = "<svg onload=alert(1)>" as *u8
148 // < -> %3C, space -> %20, > -> %3E, ( -> %28, ) -> %29
149 let xss4_exp: *u8 = "%3Csvg%20onload=alert%281%29%3E" as *u8
150 rc = check_escape(xss4_src, 21, NXH_CTX_URL, xss4_exp, 31, 1200)
151 if rc != 0 { return rc }
152
153 // 5. Single-quote breakout in ATTR_SQ
154 let xss5_src: *u8 = "x' onerror='alert(1)" as *u8
155 let xss5_exp: *u8 = "x' onerror='alert(1)" as *u8
156 rc = check_escape(xss5_src, 20, NXH_CTX_ATTR_SQ, xss5_exp, 30, 1300)
157 if rc != 0 { return rc }
158
159 // ---- Element-emission flow ----
160 let ebuf: *u8 = sys_mmap(512)
161 var eoff: i64 = 0
162 if nx_html_open_tag(ebuf, &eoff, 512, "div" as *u8, 3) != NXH_OK { return 1400 }
163 if nx_html_attr(ebuf, &eoff, 512,
164 "class" as *u8, 5,
165 "card x>y" as *u8, 8) != NXH_OK { return 1401 }
166 if nx_html_close_open_tag(ebuf, &eoff, 512) != NXH_OK { return 1402 }
167 if nx_html_text(ebuf, &eoff, 512, "hello <world>" as *u8, 13) != NXH_OK { return 1403 }
168 if nx_html_end_tag(ebuf, &eoff, 512, "div" as *u8, 3) != NXH_OK { return 1404 }
169
170 // Expected: <div class="card x>y">hello <world></div>
171 let exp: *u8 = "<div class=\"card x>y\">hello <world></div>" as *u8
172 if eoff != 50 { return 1410 }
173 if bytes_eq(ebuf, exp, 50) != 1 { return 1411 }
174
175 // ---- BAD_INPUT: invalid tag name ----
176 eoff = 0
177 if nx_html_open_tag(ebuf, &eoff, 512, "di<v" as *u8, 4) != NXH_BAD_INPUT { return 1500 }
178
179 // BAD_INPUT: attribute name with quote
180 eoff = 0
181 if nx_html_open_tag(ebuf, &eoff, 512, "div" as *u8, 3) != NXH_OK { return 1510 }
182 if nx_html_attr(ebuf, &eoff, 512,
183 "cls\"" as *u8, 4,
184 "x" as *u8, 1) != NXH_BAD_INPUT { return 1511 }
185
186 // ---- URL-attr emission ----
187 eoff = 0
188 if nx_html_open_tag(ebuf, &eoff, 512, "a" as *u8, 1) != NXH_OK { return 1600 }
189 if nx_html_attr_url(ebuf, &eoff, 512,
190 "href" as *u8, 4,
191 "/audit?ts=1" as *u8, 11) != NXH_OK { return 1601 }
192 if nx_html_close_open_tag(ebuf, &eoff, 512) != NXH_OK { return 1602 }
193 if nx_html_text(ebuf, &eoff, 512, "open" as *u8, 4) != NXH_OK { return 1603 }
194 if nx_html_end_tag(ebuf, &eoff, 512, "a" as *u8, 1) != NXH_OK { return 1604 }
195
196 // Expected: <a href="/audit?ts=1">open</a>
197 let exp_a: *u8 = "<a href=\"/audit?ts=1\">open</a>" as *u8
198 if eoff != 30 { return 1610 }
199 if bytes_eq(ebuf, exp_a, 30) != 1 { return 1611 }
200
201 return 0
202}