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}