code wiki / (root) / nx_path_canonicalize.nx

nx_path_canonicalize.nx source

↩ module page · 191 lines · 6946 B

1// nx_path_canonicalize.nx -- structural CWE-22 path-traversal prevention. 2// 3// Named by nx_bug_tape_intelligence.sh F1 rule as rank-5 next ship- 4// order. Closes the path-traversal class STRUCTURALLY: caller 5// provides a base directory + a user-supplied relative path; this 6// primitive returns either NXP_OK with the canonical path, or one 7// of five sealed-enum rejection verdicts. No "partial" path return, 8// no silent fixup, no late escape. 9// 10// The CWE-22 attack shape: user submits `../../../etc/passwd` (or 11// URL-encoded equivalent), web framework joins it with base + opens 12// the resulting path, leaking outside the intended directory. 13// 14// Substrate's structural prevention: 15// 1. Reject ANY `..` segment outright (no "join + normalize") 16// 2. Reject leading `/` (caller's base is the only absolute path) 17// 3. Reject NUL bytes (truncation attacks) 18// 4. Reject backslash on POSIX targets (no Windows-path confusion) 19// 5. Reject empty path (no implicit index) 20// 6. Reject paths > caller's max_len (resource bound) 21// 22// Sealed-enum verdict: 23// NXP_OK validated path written to out_buf 24// NXP_TRAVERSAL contains `..` segment or leading `/` 25// NXP_NUL contains NUL byte 26// NXP_BACKSLASH contains backslash (POSIX target) 27// NXP_EMPTY zero-length input 28// NXP_TOO_LONG exceeds max_out 29// NXP_BAD_ARG null pointers / negative sizes 30// 31// Per cardinal feedback-defensive-at-boundaries-trusting-internally: 32// canonicalize ONCE at the user-input boundary; trust the canonical 33// path internally. 34// 35// Per cardinal user-owns-every-bit: caller provides base + max_len + 36// out_buf. Substrate never reads/writes filesystem; this is a pure 37// validator + byte-copier. 38// 39// nx_capability_claims: 40// needs: [sealed_enum, byte_ops] 41// provides: [path_validation, cwe_22_structural_prevention] 42// safety: [no_unchecked_deref, no_floating_point, no_syscall, 43// bit_equal_reproducible, target_agnostic] 44// verdict: [sealed_enum_7_state] 45// license: ORIGINAL 46// kind: racing_crew_specialist 47// layer: L2 (transform over L1 byte-buffer container) 48// cwe: [CWE-22 Path Traversal structural prevention] 49 50// ---- Sealed enum: path verdict ----------------------------------- 51 52const NXP_OK: i64 = 0 53const NXP_TRAVERSAL: i64 = 1 54const NXP_NUL: i64 = 2 55const NXP_BACKSLASH: i64 = 3 56const NXP_EMPTY: i64 = 4 57const NXP_TOO_LONG: i64 = 5 58const NXP_BAD_ARG: i64 = 6 59const NXP_VERDICT_N: i64 = 7 60 61func nxp_verdict_is_valid(v: i64) -> i64 { 62 if v < 0 { return 0 } 63 if v >= NXP_VERDICT_N { return 0 } 64 return 1 65} 66 67func nxp_verdict_name(v: i64) -> *u8 { 68 if v == NXP_OK { return "OK" as *u8 } 69 if v == NXP_TRAVERSAL { return "TRAVERSAL" as *u8 } 70 if v == NXP_NUL { return "NUL" as *u8 } 71 if v == NXP_BACKSLASH { return "BACKSLASH" as *u8 } 72 if v == NXP_EMPTY { return "EMPTY" as *u8 } 73 if v == NXP_TOO_LONG { return "TOO_LONG" as *u8 } 74 if v == NXP_BAD_ARG { return "BAD_ARG" as *u8 } 75 return "INVALID" as *u8 76} 77 78// ---- Validation predicates --------------------------------------- 79 80// Returns 1 if `src[i..i+2]` is the literal `..` AND surrounding 81// characters are segment boundaries (start, end, or `/`). Catches: 82// .. (whole path) 83// ../ (start of path) 84// /../ (middle of path) 85// /.. (end of path) 86// but NOT `..foo` (filename containing two dots at start) which is a 87// VALID filename, e.g., ..bashrc. 88func nxp_is_dotdot_segment(src: *u8, i: i64, n: i64) -> i64 { 89 if i + 1 >= n { return 0 } 90 if src[i] != 0x2e { return 0 } 91 if src[i+1] != 0x2e { return 0 } 92 // Left boundary: must be start-of-string or `/`. 93 if i > 0 { 94 if src[i-1] != 0x2f { return 0 } 95 } 96 // Right boundary: must be end-of-string or `/`. 97 if i + 2 < n { 98 if src[i+2] != 0x2f { return 0 } 99 } 100 return 1 101} 102 103// ---- The validator entry point ----------------------------------- 104// 105// canonicalize(src, src_n, out_buf, max_out, out_n) where: 106// src -- user-supplied path bytes 107// src_n -- length (positive) 108// out_buf -- caller-allocated destination (>= src_n bytes) 109// max_out -- maximum acceptable length 110// out_n -- (out) bytes written on success 111// Returns one of NXP_* verdicts. 112 113func nx_path_canonicalize(src: *u8, src_n: i64, 114 out_buf: *u8, max_out: i64, 115 out_n: *i64) -> i64 { 116 if src == (0 as *u8) { return NXP_BAD_ARG } 117 if out_buf == (0 as *u8) { return NXP_BAD_ARG } 118 if out_n == (0 as *i64) { return NXP_BAD_ARG } 119 if src_n < 0 { return NXP_BAD_ARG } 120 if max_out <= 0 { return NXP_BAD_ARG } 121 if src_n == 0 { return NXP_EMPTY } 122 if src_n > max_out { return NXP_TOO_LONG } 123 124 // Leading `/` would make caller's base irrelevant -- reject. 125 if src[0] == 0x2f { return NXP_TRAVERSAL } 126 127 // Scan for traversal / NUL / backslash. 128 var i: i64 = 0 129 while i < src_n { 130 let b: i64 = src[i] as i64 131 if b == 0 { return NXP_NUL } 132 if b == 0x5c { return NXP_BACKSLASH } 133 if b == 0x2e { 134 // Check if this `.` begins a `..` segment. 135 if nxp_is_dotdot_segment(src, i, src_n) == 1 { 136 return NXP_TRAVERSAL 137 } 138 } 139 i = i + 1 140 } 141 142 // All checks passed -- copy src to out_buf. 143 var j: i64 = 0 144 while j < src_n { 145 out_buf[j] = src[j] 146 j = j + 1 147 } 148 *out_n = src_n 149 return NXP_OK 150} 151 152// ---- Join helper: <base>/<canonical_path> ------------------------ 153// 154// Joins caller's base directory + a pre-canonicalized relative path 155// with a single `/` separator. Returns NXP_OK on success or 156// NXP_TOO_LONG / NXP_BAD_ARG on failure. Does NOT re-canonicalize 157// the relative part -- caller must have called nx_path_canonicalize 158// first. 159// 160// This separation is intentional: canonicalization is a SAFETY check 161// at the boundary; join is a pure string operation internally. 162 163func nx_path_join(base: *u8, base_n: i64, 164 rel: *u8, rel_n: i64, 165 out_buf: *u8, max_out: i64, 166 out_n: *i64) -> i64 { 167 if base == (0 as *u8) { return NXP_BAD_ARG } 168 if rel == (0 as *u8) { return NXP_BAD_ARG } 169 if out_buf == (0 as *u8) { return NXP_BAD_ARG } 170 if out_n == (0 as *i64) { return NXP_BAD_ARG } 171 if base_n < 0 { return NXP_BAD_ARG } 172 if rel_n < 0 { return NXP_BAD_ARG } 173 if max_out <= 0 { return NXP_BAD_ARG } 174 175 let need: i64 = base_n + 1 + rel_n 176 if need > max_out { return NXP_TOO_LONG } 177 178 var i: i64 = 0 179 while i < base_n { 180 out_buf[i] = base[i] 181 i = i + 1 182 } 183 out_buf[base_n] = 0x2f as u8 // / 184 var j: i64 = 0 185 while j < rel_n { 186 out_buf[base_n + 1 + j] = rel[j] 187 j = j + 1 188 } 189 *out_n = need 190 return NXP_OK 191}