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}