code wiki / _hdl_build / nx_cms_forms_gate.nx
nx_cms_forms_gate.nx source
↩ module page · 275 lines · 13698 B
1// nx_cms_forms_gate.nx -- CMS W3 GATE (re-runnable, evidence-driven): FORMS + anti-spam (the
2// Contact Form 7 / Akismet class). Proves: a public POST /form/<name> is stored in the ADDITIVE
3// submissions log (never lost); a honeypot-filled submission is silently DISCARDED (bot defense);
4// a per-IP rate cap (config DATA in form.conf) returns 429 and does NOT store; the submissions list
5// is session-only. Appends "CMSGATE row=nx_cms_forms ... verdict=PASS|FAIL". Exit 0 iff all rows
6// pass. license_tier: ORIGINAL
7import "nx_cms_store.nx"
8import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host
9import "nx_sha256.nx"
10import "nx_syscalls.nx"
11import "_hdl_build/nx_kill_portable.nx"
12
13const FG_PORT: i64 = 8090
14const FG_SITE: *u8 = "/tmp/_cms_forms_site"
15
16func fg_w(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
17func fg_len(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
18func fg_cat(dst: *u8, off: i64, s: *u8) -> i64 { var o: i64=off; var k: i64=0; while s[k]!=(0 as u8){dst[o]=s[k];o=o+1;k=k+1} return o }
19func fg_catn(dst: *u8, off: i64, s: *u8, n: i64) -> i64 { var o: i64=off; var k: i64=0; while k<n{dst[o]=s[k];o=o+1;k=k+1} return o }
20func fg_index(hay: *u8, n: i64, needle: *u8) -> i64 {
21 let nl: i64 = fg_len(needle)
22 var i: i64 = 0
23 while i + nl <= n {
24 var q: i64 = 0
25 var ok: i64 = 1
26 while q < nl { if (hay[i+q] as i64) != (needle[q] as i64) { ok = 0; q = nl } q = q + 1 }
27 if ok == 1 { return i }
28 i = i + 1
29 }
30 return 0 - 1
31}
32func fg_has(hay: *u8, n: i64, needle: *u8) -> i64 { if fg_index(hay, n, needle) >= 0 { return 1 } return 0 }
33
34func fg_http(req: *u8, rl: i64, resp: *u8, cap: i64) -> i64 {
35 let fd: i64 = sys_socket(2, 1, 0)
36 if fd < 0 { return 0 - 1 }
37 let addr: *u8 = sys_mmap(16)
38 addr[0] = 2 as u8; addr[1] = 0 as u8
39 addr[2] = ((FG_PORT >> 8) & 0xff) as u8
40 addr[3] = (FG_PORT & 0xff) as u8
41 addr[4] = 127 as u8; addr[5] = 0 as u8; addr[6] = 0 as u8; addr[7] = 1 as u8
42 var zi: i64 = 8
43 while zi < 16 { addr[zi] = 0 as u8; zi = zi + 1 }
44 if nx_connect_bounded(fd, addr, 16, NX_CONN_DEFAULT_MS) < 0 { sys_close(fd); return 0 - 1 }
45 var off: i64 = 0
46 var go: i64 = 1
47 while go == 1 { go = 0; if off < rl { let w: i64 = sys_write(fd, (req + off) as *u8, rl - off); if w > 0 { off = off + w; go = 1 } } }
48 sys_set_socket_timeout(fd, 10)
49 var total: i64 = 0
50 go = 1
51 while go == 1 { go = 0; if total < cap { let r: i64 = sys_read(fd, (resp + total) as *u8, cap - total); if r > 0 { total = total + r; go = 1 } } }
52 sys_close(fd)
53 return total
54}
55func fg_post(req: *u8, path: *u8, cookie: *u8, body: *u8) -> i64 {
56 var o: i64 = fg_cat(req, 0, "POST " as *u8)
57 o = fg_cat(req, o, path)
58 o = fg_cat(req, o, " HTTP/1.1\r\nHost: gate\r\nContent-Type: application/x-www-form-urlencoded\r\n" as *u8)
59 if (cookie[0] as i64) != 0 { o = fg_cat(req, o, "Cookie: nsess=" as *u8); o = fg_catn(req, o, cookie, 32); o = fg_cat(req, o, "\r\n" as *u8) }
60 o = fg_cat(req, o, "Content-Length: " as *u8)
61 let bl: i64 = fg_len(body)
62 let t: *u8 = sys_mmap(24)
63 var m: i64 = bl
64 var k: i64 = 0
65 if m == 0 { t[0] = 48 as u8; k = 1 }
66 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
67 var i: i64 = 0
68 while i < k { req[o] = t[k-1-i]; o = o + 1; i = i + 1 }
69 o = fg_cat(req, o, "\r\n\r\n" as *u8)
70 o = fg_cat(req, o, body)
71 return o
72}
73func fg_get(req: *u8, path: *u8, cookie: *u8) -> i64 {
74 var o: i64 = fg_cat(req, 0, "GET " as *u8)
75 o = fg_cat(req, o, path)
76 o = fg_cat(req, o, " HTTP/1.1\r\nHost: gate\r\n" as *u8)
77 if (cookie[0] as i64) != 0 { o = fg_cat(req, o, "Cookie: nsess=" as *u8); o = fg_catn(req, o, cookie, 32); o = fg_cat(req, o, "\r\n" as *u8) }
78 o = fg_cat(req, o, "\r\n" as *u8)
79 return o
80}
81func fg_row(id: i64, ok: i64, what: *u8) -> i64 {
82 fg_w("FORMROW " as *u8)
83 let d: *u8 = sys_mmap(8); d[0] = (48 + id/10) as u8; d[1] = (48 + id%10) as u8; d[2] = 32 as u8; d[3] = 0 as u8
84 fg_w(d)
85 if ok == 1 { fg_w("PASS " as *u8) }
86 if ok == 0 { fg_w("FAIL " as *u8) }
87 fg_w(what); fg_w("\n" as *u8)
88 return ok
89}
90func fg_mkdir(path: *u8, mode: i64) -> i64 { let nb: *i64 = sys_mmap(8) as *i64; nb[0] = 258; return __syscall(nb[0], 0 - 100, path, mode, 0, 0, 0) }
91func fg_writefile(path: *u8, buf: *u8, n: i64) -> i64 { let fd: i64 = sys_openat_wr(path, 0x1a4); if fd < 0 { return 0 } sys_write(fd, buf, n); sys_close(fd); return 1 }
92
93// login -> session into sess; returns 1/0
94func fg_login(req: *u8, resp: *u8, sess: *u8) -> i64 {
95 let nocookie: *u8 = sys_mmap(8)
96 nocookie[0] = 0 as u8
97 let rl: i64 = fg_post(req, "/admin/login" as *u8, nocookie, "pw=forms-gate-pw-1" as *u8)
98 let rn: i64 = fg_http(req, rl, resp, 524287)
99 if rn > 0 { if fg_has(resp, rn, "303" as *u8) == 1 {
100 let sp: i64 = fg_index(resp, rn, "nsess=" as *u8)
101 if sp >= 0 { var m2: i64 = 0; while m2 < 32 { sess[m2] = resp[sp+6+m2]; m2 = m2 + 1 } sess[32] = 0 as u8; return 1 }
102 } }
103 return 0
104}
105
106func main() -> i64 {
107 fg_mkdir(FG_SITE, 0x1ed)
108 cst_copy("knowledge/cms/andelinwest/template.html" as *u8, "/tmp/_cms_forms_site/template.html" as *u8)
109 cst_copy("knowledge/cms/andelinwest/content.txt" as *u8, "/tmp/_cms_forms_site/content.txt" as *u8)
110 sys_renameat("/tmp/_cms_forms_site/draft.txt" as *u8, "/tmp/_cms_forms_site/draft.stale" as *u8)
111 sys_renameat("/tmp/_cms_forms_site/submissions.txt" as *u8, "/tmp/_cms_forms_site/submissions.stale" as *u8)
112 // rate cap = 3 per window, big window so the 4th in this run is refused (config DATA)
113 let conf: *u8 = "max_per_window=3\nwindow_sec=86400\n" as *u8
114 fg_writefile("/tmp/_cms_forms_site/form.conf" as *u8, conf, fg_len(conf))
115 let dig: *u8 = sys_mmap(32)
116 sha256_digest("forms-gate-pw-1" as *u8, 15, dig)
117 let hexs: *u8 = sys_mmap(80)
118 let hxc: *u8 = "0123456789abcdef" as *u8
119 var i: i64 = 0
120 while i < 32 { hexs[i*2] = hxc[((dig[i] as i64) >> 4) & 15]; hexs[i*2+1] = hxc[(dig[i] as i64) & 15]; i = i + 1 }
121 fg_writefile("/tmp/_cms_forms_site/admin_pw.sha256" as *u8, hexs, 64)
122
123 let bpid: i64 = sys_fork()
124 if bpid == 0 {
125 let dn: i64 = sys_openat_wr("/dev/null" as *u8, 0x1a4)
126 if dn >= 0 { sys_dup3(dn, 1, 0); sys_dup3(dn, 2, 0) }
127 let bargv: *i64 = sys_mmap(32) as *i64
128 bargv[0] = "_offc/nx_sov_build_run.elf" as *u8 as i64
129 bargv[1] = "nx_cms_admin" as *u8 as i64
130 bargv[2] = 0
131 let benvp: *i64 = sys_mmap(16) as *i64; benvp[0] = 0
132 sys_execve("_offc/nx_sov_build_run.elf" as *u8, bargv, benvp); sys_exit(127)
133 }
134 let bst: *i64 = sys_mmap(16) as *i64
135 sys_wait4(bpid, bst, 0)
136 // CALLER-GAP FIX 2026-08-01 (seq1267 class, left behind in 11 cms gates): the runner emits
137 // buildroot/_build/<t>.sov.elf under the no-/tmp doctrine while this probed /tmp. Proven by control:
138 // buildroot/_build/nx_cms_admin.sov.elf = 208962B, /tmp/nx_cms_admin.sov.elf unreadable.
139 // SEED-SOURCE GUARD (same class as nx_cms_seo_gate, 2026-08-01): if this gate seeds a fixture by
140 // copying from a site corpus, an absent corpus makes EVERY row red for one missing file. Check the
141 // copy's RETURN, never the destination's existence -- a stale /tmp fixture from a prior run passes
142 // an existence test and silently supplies last run's data. (Applies where this gate copies; the
143 // probe below is the build-path half of the same lesson.)
144 var bchk: i64 = sys_openat_rd("buildroot/_build/nx_cms_admin.sov.elf" as *u8)
145 var badm: *u8 = "buildroot/_build/nx_cms_admin.sov.elf" as *u8
146 if bchk < 0 { bchk = sys_openat_rd("/tmp/nx_cms_admin.sov.elf" as *u8); badm = "/tmp/nx_cms_admin.sov.elf" as *u8 }
147 if bchk < 0 { fg_w("CMSGATE row=nx_cms_forms ADMIN-BUILD-MISSING (probed buildroot/_build AND /tmp)\n" as *u8); sys_exit(1) }
148 sys_close(bchk)
149 let pid: i64 = sys_fork()
150 if pid == 0 {
151 let lg: i64 = sys_openat_wr("/tmp/_cms_admin_forms_gate.log" as *u8, 0x1a4)
152 if lg >= 0 { sys_dup3(lg, 1, 0); sys_dup3(lg, 2, 0) }
153 let argvv: *i64 = sys_mmap(8*6) as *i64
154 argvv[0] = badm as i64
155 argvv[1] = "8090" as *u8 as i64
156 argvv[2] = FG_SITE as i64
157 argvv[3] = "99" as *u8 as i64
158 argvv[4] = 0
159 let envp: *i64 = sys_mmap(16) as *i64; envp[0] = 0
160 sys_execve(badm, argvv, envp)
161 sys_exit(127)
162 }
163
164 let req: *u8 = sys_mmap(262144)
165 let resp: *u8 = sys_mmap(524288)
166 let nocookie: *u8 = sys_mmap(8)
167 nocookie[0] = 0 as u8
168 let sess: *u8 = sys_mmap(64)
169 sess[0] = 0 as u8
170
171 // F0: boot poll
172 var rn: i64 = 0 - 1
173 var tries: i64 = 0
174 while tries < 50 {
175 let rl0: i64 = fg_get(req, "/" as *u8, nocookie)
176 rn = fg_http(req, rl0, resp, 524287)
177 if rn > 0 { tries = 50 }
178 if rn <= 0 { sys_sleep_ms(100); tries = tries + 1 }
179 }
180 var pass: i64 = 0
181 var rows: i64 = 0
182 var ok: i64 = 0
183 if rn > 0 { ok = 1 }
184 rows = rows + 1; pass = pass + fg_row(0, ok, "daemon up" as *u8)
185
186 // F1: a legit submission -> 303 thanks, stored in the additive log
187 var rl: i64 = fg_post(req, "/form/contact" as *u8, nocookie, "name=GateUserOne&email=a@b.co&message=HelloFromGate&hp_url=" as *u8)
188 rn = fg_http(req, rl, resp, 524287)
189 ok = 0
190 if rn > 0 { if fg_has(resp, rn, "303" as *u8) == 1 { ok = 1 } }
191 rows = rows + 1; pass = pass + fg_row(1, ok, "legit submission accepted 303" as *u8)
192
193 // F2: it shows up in the session-only submissions list
194 if fg_login(req, resp, sess) == 1 {
195 rl = fg_get(req, "/admin/submissions" as *u8, sess)
196 rn = fg_http(req, rl, resp, 524287)
197 ok = 0
198 if rn > 0 { if fg_has(resp, rn, "HelloFromGate" as *u8) == 1 { if fg_has(resp, rn, "Fcontact" as *u8) == 1 { ok = 1 } } }
199 }
200 rows = rows + 1; pass = pass + fg_row(2, ok, "submission stored + listed (additive)" as *u8)
201
202 // F3: a honeypot-filled submission is silently DISCARDED (200, not stored)
203 rl = fg_post(req, "/form/contact" as *u8, nocookie, "name=SpamBot&message=BuyCheapStuff&hp_url=http://spam.example" as *u8)
204 rn = fg_http(req, rl, resp, 524287)
205 var got200: i64 = 0
206 if rn > 0 { if fg_has(resp, rn, "200" as *u8) == 1 { got200 = 1 } }
207 rl = fg_get(req, "/admin/submissions" as *u8, sess)
208 rn = fg_http(req, rl, resp, 524287)
209 ok = 0
210 if got200 == 1 { if rn > 0 { if fg_has(resp, rn, "BuyCheapStuff" as *u8) == 0 { ok = 1 } } }
211 rows = rows + 1; pass = pass + fg_row(3, ok, "honeypot submission discarded (not stored)" as *u8)
212
213 // F4: rate cap (3/window): submissions 2 and 3 ok, the 4th is 429 and not stored.
214 // (submission 1 above used 1 of 3; honeypot did not count.) Send 2 more legit -> total 3 used.
215 rl = fg_post(req, "/form/contact" as *u8, nocookie, "name=U2&message=MsgTwoKeep&hp_url=" as *u8)
216 fg_http(req, rl, resp, 524287)
217 rl = fg_post(req, "/form/contact" as *u8, nocookie, "name=U3&message=MsgThreeKeep&hp_url=" as *u8)
218 fg_http(req, rl, resp, 524287)
219 // 4th legit -> over the cap -> 429
220 rl = fg_post(req, "/form/contact" as *u8, nocookie, "name=U4&message=MsgFourBLOCKED&hp_url=" as *u8)
221 rn = fg_http(req, rl, resp, 524287)
222 var got429: i64 = 0
223 if rn > 0 { if fg_has(resp, rn, "429" as *u8) == 1 { got429 = 1 } }
224 rl = fg_get(req, "/admin/submissions" as *u8, sess)
225 rn = fg_http(req, rl, resp, 524287)
226 ok = 0
227 if got429 == 1 { if rn > 0 { if fg_has(resp, rn, "MsgThreeKeep" as *u8) == 1 { if fg_has(resp, rn, "MsgFourBLOCKED" as *u8) == 0 { ok = 1 } } } }
228 rows = rows + 1; pass = pass + fg_row(4, ok, "per-IP rate cap 429 + blocked not stored" as *u8)
229
230 // F5: submissions list is session-only (unauth -> 403, no leak)
231 rl = fg_get(req, "/admin/submissions" as *u8, nocookie)
232 rn = fg_http(req, rl, resp, 524287)
233 ok = 0
234 if rn > 0 { if fg_has(resp, rn, "403" as *u8) == 1 { if fg_has(resp, rn, "HelloFromGate" as *u8) == 0 { ok = 1 } } }
235 rows = rows + 1; pass = pass + fg_row(5, ok, "submissions list refused without session" as *u8)
236
237 // ---- teardown ----
238 nxk_kill(pid, 9)
239 let stbuf: *i64 = sys_mmap(16) as *i64
240 sys_wait4(pid, stbuf, 0)
241
242 var verdict: *u8 = "FAIL" as *u8
243 if pass == rows { verdict = "PASS" as *u8 }
244 let line: *u8 = sys_mmap(512)
245 var lo: i64 = fg_cat(line, 0, "CMSGATE row=nx_cms_forms rows=" as *u8)
246 let t2: *u8 = sys_mmap(24)
247 var mm: i64 = rows
248 var kk: i64 = 0
249 if mm == 0 { t2[0] = 48 as u8; kk = 1 }
250 while mm > 0 { t2[kk] = (48 + (mm % 10)) as u8; mm = mm / 10; kk = kk + 1 }
251 var ii: i64 = 0
252 while ii < kk { line[lo] = t2[kk-1-ii]; lo = lo + 1; ii = ii + 1 }
253 lo = fg_cat(line, lo, " pass=" as *u8)
254 mm = pass; kk = 0
255 if mm == 0 { t2[0] = 48 as u8; kk = 1 }
256 while mm > 0 { t2[kk] = (48 + (mm % 10)) as u8; mm = mm / 10; kk = kk + 1 }
257 ii = 0
258 while ii < kk { line[lo] = t2[kk-1-ii]; lo = lo + 1; ii = ii + 1 }
259 lo = fg_cat(line, lo, " verdict=" as *u8)
260 lo = fg_cat(line, lo, verdict)
261 lo = fg_cat(line, lo, " epoch=" as *u8)
262 mm = sys_now_realtime_sec(); kk = 0
263 if mm == 0 { t2[0] = 48 as u8; kk = 1 }
264 while mm > 0 { t2[kk] = (48 + (mm % 10)) as u8; mm = mm / 10; kk = kk + 1 }
265 ii = 0
266 while ii < kk { line[lo] = t2[kk-1-ii]; lo = lo + 1; ii = ii + 1 }
267 line[lo] = 10 as u8
268 lo = lo + 1
269 sys_write(1, line, lo)
270 let gf: i64 = sys_openat_append("knowledge/status/cms_gate.log" as *u8, 0x1a4)
271 if gf >= 0 { sys_write(gf, line, lo); sys_close(gf) }
272 if pass == rows { sys_exit(0) }
273 sys_exit(1)
274 return 1
275}