/src/freeradius-server/src/lib/util/skip.c
Line | Count | Source |
1 | | /* |
2 | | * This library is free software; you can redistribute it and/or |
3 | | * modify it under the terms of the GNU Lesser General Public |
4 | | * License as published by the Free Software Foundation; either |
5 | | * version 2.1 of the License, or (at your option) any later version. |
6 | | * |
7 | | * This library is distributed in the hope that it will be useful, |
8 | | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
9 | | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
10 | | * Lesser General Public License for more details. |
11 | | * |
12 | | * You should have received a copy of the GNU Lesser General Public |
13 | | * License along with this library; if not, write to the Free Software |
14 | | * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA |
15 | | */ |
16 | | |
17 | | /** Preparse input by skipping known tokens |
18 | | * |
19 | | * @file src/lib/util/skip.c |
20 | | * |
21 | | * @copyright 2025 Network RADIUS SAS (legal@networkradius.com) |
22 | | */ |
23 | | RCSID("$Id: bbc208c9634734c435147b40e7e0c40f60fb8ea8 $") |
24 | | |
25 | | #include <freeradius-devel/util/misc.h> |
26 | | #include <freeradius-devel/util/skip.h> |
27 | | |
28 | | /** Skip a quoted string. |
29 | | * |
30 | | * @param[in] start start of the string, pointing to the quotation character |
31 | | * @param[in] end end of the string (or NULL for zero-terminated strings) |
32 | | * @return |
33 | | * >0 length of the string which was parsed |
34 | | * <=0 on error |
35 | | */ |
36 | | ssize_t fr_skip_string(char const *start, char const *end) |
37 | 437 | { |
38 | 437 | char const *p = start; |
39 | 437 | char quote; |
40 | | |
41 | 437 | quote = *(p++); |
42 | | |
43 | 1.30M | while (end ? ((p < end) && *p) : *p) { |
44 | | /* |
45 | | * Stop at the quotation character |
46 | | */ |
47 | 1.30M | if (*p == quote) { |
48 | 435 | p++; |
49 | 435 | return p - start; |
50 | 435 | } |
51 | | |
52 | | /* |
53 | | * Not an escape character: it's OK. |
54 | | */ |
55 | 1.30M | if (*p != '\\') { |
56 | 1.30M | p++; |
57 | 1.30M | continue; |
58 | 1.30M | } |
59 | | |
60 | 0 | if (end && ((p + 2) >= end)) { |
61 | 0 | fail: |
62 | 0 | fr_strerror_const("Unexpected escape at end of string"); |
63 | 0 | return -(p - start); |
64 | 0 | } |
65 | | |
66 | | /* |
67 | | * Escape at EOL is not allowed. |
68 | | */ |
69 | 0 | if (p[1] < ' ') goto fail; |
70 | | |
71 | | /* |
72 | | * \r or \n, etc. |
73 | | */ |
74 | 0 | if (!isdigit((uint8_t) p[1])) { |
75 | 0 | p += 2; |
76 | 0 | continue; |
77 | 0 | } |
78 | | |
79 | | /* |
80 | | * Double-quoted strings use \000 |
81 | | * Regexes use \0 |
82 | | */ |
83 | 0 | if (quote == '/') { |
84 | 0 | p++; |
85 | 0 | continue; |
86 | 0 | } |
87 | | |
88 | 0 | if (end && ((p + 4) >= end)) goto fail; |
89 | | |
90 | | /* |
91 | | * Allow for \1f in single quoted strings |
92 | | */ |
93 | 0 | if ((quote == '\'') && isxdigit((uint8_t) p[1]) && isxdigit((uint8_t) p[2])) { |
94 | 0 | p += 3; |
95 | 0 | continue; |
96 | 0 | } |
97 | | |
98 | 0 | if (!isdigit((uint8_t) p[2]) || !isdigit((uint8_t) p[3])) { |
99 | 0 | fr_strerror_const("Invalid octal escape"); |
100 | 0 | return -(p - start); |
101 | 0 | } |
102 | | |
103 | 0 | p += 4; |
104 | 0 | } |
105 | | |
106 | | /* |
107 | | * Unexpected end of string. |
108 | | */ |
109 | 2 | fr_strerror_const("Unexpected end of string"); |
110 | 2 | return -(p - start); |
111 | 437 | } |
112 | | |
113 | | /* |
114 | | * Recursion cap shared by fr_skip_brackets and fr_skip_xlat, which |
115 | | * are mutually recursive. Real configs nest far below this; the |
116 | | * cap exists so untrusted input (config-file fuzzer) can't exhaust |
117 | | * the C stack via `((((...` or `${${${...`. |
118 | | */ |
119 | 3.47k | #define SKIP_MAX_DEPTH 64 |
120 | | |
121 | | static ssize_t skip_brackets(char const *start, char const *end, char end_quote, unsigned int depth); |
122 | | static ssize_t skip_xlat(char const *start, char const *end, unsigned int depth); |
123 | | |
124 | | static ssize_t skip_brackets(char const *start, char const *end, char end_quote, unsigned int depth) |
125 | 1.73k | { |
126 | 1.73k | ssize_t slen; |
127 | 1.73k | char const *p = start; |
128 | | |
129 | 1.73k | if (depth >= SKIP_MAX_DEPTH) { |
130 | 0 | fr_strerror_const("Nesting too deep"); |
131 | 0 | return -(p - start); |
132 | 0 | } |
133 | | |
134 | 488k | while (end ? ((p < end) && *p) : *p) { |
135 | 488k | if (*p == end_quote) { |
136 | 1.73k | p++; |
137 | 1.73k | return p - start; |
138 | 1.73k | } |
139 | | |
140 | | /* |
141 | | * Expressions. Arguably we want to |
142 | | * differentiate conditions and function |
143 | | * arguments, but it's not clear how to do that |
144 | | * in a pre-parsing stage. |
145 | | */ |
146 | 487k | if (*p == '(') { |
147 | 1 | p++; |
148 | 1 | slen = skip_brackets(p, end, ')', depth + 1); |
149 | | |
150 | 1.73k | next: |
151 | 1.73k | if (slen <= 0) return slen - (p - start); |
152 | | |
153 | 1.72k | fr_assert((size_t) slen <= (size_t) (end - p)); |
154 | 1.72k | p += slen; |
155 | 1.72k | continue; |
156 | 1.73k | } |
157 | | |
158 | | /* |
159 | | * A quoted string. |
160 | | */ |
161 | 487k | if ((*p == '"') || (*p == '\'') || (*p == '`')) { |
162 | 437 | slen = fr_skip_string(p, end); |
163 | 437 | goto next; |
164 | 437 | } |
165 | | |
166 | | /* |
167 | | * Nested expansion. |
168 | | */ |
169 | 486k | if ((p[0] == '$') || (p[0] == '%')) { |
170 | 12.0k | if (end && (p + 2) >= end) break; |
171 | | |
172 | | /* |
173 | | * %% inside of an xlat |
174 | | */ |
175 | 12.0k | if ((p[0] == '%') && (p[1] == '%')) { |
176 | 7.32k | p += 2; |
177 | 7.32k | continue; |
178 | 7.32k | } |
179 | | |
180 | 4.74k | if ((p[1] == '{') || (p[1] == '(')) { |
181 | 1.29k | slen = skip_xlat(p, end, depth + 1); |
182 | 1.29k | goto next; |
183 | 1.29k | } |
184 | | |
185 | | /* |
186 | | * Bare $ or %, just leave it alone. |
187 | | */ |
188 | 3.44k | p++; |
189 | 3.44k | continue; |
190 | 4.74k | } |
191 | | |
192 | | /* |
193 | | * Escapes are special. |
194 | | */ |
195 | 474k | if (*p != '\\') { |
196 | 474k | p++; |
197 | 474k | continue; |
198 | 474k | } |
199 | | |
200 | 2 | if (end && ((p + 2) >= end)) break; |
201 | | |
202 | | /* |
203 | | * Escapes here are only one-character escapes. |
204 | | */ |
205 | 2 | if (p[1] < ' ') break; |
206 | 2 | p += 2; |
207 | 2 | } |
208 | | |
209 | | /* |
210 | | * Unexpected end of xlat |
211 | | */ |
212 | 2 | fr_strerror_const("Unexpected end of expansion"); |
213 | 2 | return -(p - start); |
214 | 1.73k | } |
215 | | |
216 | | static ssize_t skip_xlat(char const *start, char const *end, unsigned int depth) |
217 | 1.73k | { |
218 | 1.73k | ssize_t slen; |
219 | 1.73k | char const *p = start; |
220 | | |
221 | 1.73k | if (depth >= SKIP_MAX_DEPTH) { |
222 | 0 | fr_strerror_const("Nesting too deep"); |
223 | 0 | return -(p - start); |
224 | 0 | } |
225 | | |
226 | | /* |
227 | | * At least %{1} or $(.) |
228 | | */ |
229 | 1.73k | if (end && ((end - start) < 4)) { |
230 | 0 | fr_strerror_const("Invalid expansion"); |
231 | 0 | return 0; |
232 | 0 | } |
233 | | |
234 | 1.73k | if (!((memcmp(p, "%{", 2) == 0) || /* xlat */ |
235 | 1.73k | (memcmp(p, "${", 2) == 0) || /* config file macro */ |
236 | 0 | (memcmp(p, "$(", 2) == 0))) { /* shell expansion in an back-ticks argument */ |
237 | 0 | fr_strerror_const("Invalid expansion"); |
238 | 0 | return 0; |
239 | 0 | } |
240 | 1.73k | p++; |
241 | | |
242 | 1.73k | if (*p == '(') { |
243 | 0 | p++; /* skip the '(' */ |
244 | 0 | slen = skip_brackets(p, end, ')', depth + 1); |
245 | |
|
246 | 1.73k | } else if (*p == '{') { |
247 | 1.73k | p++; /* skip the '{' */ |
248 | 1.73k | slen = skip_brackets(p, end, '}', depth + 1); |
249 | | |
250 | 1.73k | } else { |
251 | 0 | char const *q = p; |
252 | | |
253 | | /* |
254 | | * New xlat syntax: %foo(...) |
255 | | */ |
256 | 0 | while (isalnum((int) *q) || (*q == '.') || (*q == '_') || (*q == '-')) { |
257 | 0 | q++; |
258 | 0 | } |
259 | |
|
260 | 0 | if (*q != '(') { |
261 | 0 | fr_strerror_const("Invalid character after '%'"); |
262 | 0 | return -(p - start); |
263 | 0 | } |
264 | | |
265 | 0 | p = q + 1; |
266 | |
|
267 | 0 | slen = skip_brackets(p, end, ')', depth + 1); |
268 | 0 | } |
269 | | |
270 | 1.73k | if (slen <= 0) return slen - (p - start); |
271 | 1.73k | return slen + (p - start); |
272 | 1.73k | } |
273 | | |
274 | | /** Skip a generic {...} or (...) arguments |
275 | | * |
276 | | */ |
277 | | ssize_t fr_skip_brackets(char const *start, char const *end, char end_quote) |
278 | 0 | { |
279 | 0 | return skip_brackets(start, end, end_quote, 0); |
280 | 0 | } |
281 | | |
282 | | /** Skip an xlat expression. |
283 | | * |
284 | | * This is a simple "peek ahead" parser which tries to not be wrong. It may accept |
285 | | * some things which will later parse as invalid (e.g. unknown attributes, etc.) |
286 | | * But it also rejects all malformed expressions. |
287 | | * |
288 | | * It's used as a quick hack because the full parser isn't always available. |
289 | | * |
290 | | * @param[in] start start of the expression, MUST point to the "%{" or "%(" |
291 | | * @param[in] end end of the string (or NULL for zero-terminated strings) |
292 | | * @return |
293 | | * >0 length of the string which was parsed |
294 | | * <=0 on error |
295 | | */ |
296 | | ssize_t fr_skip_xlat(char const *start, char const *end) |
297 | 444 | { |
298 | 444 | return skip_xlat(start, end, 0); |
299 | 444 | } |
300 | | |
301 | | /** Skip a conditional expression. |
302 | | * |
303 | | * This is a simple "peek ahead" parser which tries to not be wrong. It may accept |
304 | | * some things which will later parse as invalid (e.g. unknown attributes, etc.) |
305 | | * But it also rejects all malformed expressions. |
306 | | * |
307 | | * It's used as a quick hack because the full parser isn't always available. |
308 | | * |
309 | | * @param[in] start start of the condition. |
310 | | * @param[in] end end of the string (or NULL for zero-terminated strings) |
311 | | * @param[in] terminal terminal character(s) |
312 | | * @param[out] eol did the parse error happen at eol? |
313 | | * @return |
314 | | * >0 length of the string which was parsed. *eol is false. |
315 | | * <=0 on error, *eol may be set. |
316 | | */ |
317 | | ssize_t fr_skip_condition(char const *start, char const *end, bool const terminal[static SBUFF_CHAR_CLASS], bool *eol) |
318 | 677 | { |
319 | 677 | char const *p = start; |
320 | 677 | bool was_regex = false; |
321 | 677 | int depth = 0; |
322 | 677 | ssize_t slen; |
323 | | |
324 | 677 | if (eol) *eol = false; |
325 | | |
326 | | /* |
327 | | * Keep parsing the condition until we hit EOS or EOL. |
328 | | */ |
329 | 267k | while (end ? ((p < end) && *p) : *p) { |
330 | 266k | if (isspace((uint8_t) *p)) { |
331 | 282 | p++; |
332 | 282 | continue; |
333 | 282 | } |
334 | | |
335 | | /* |
336 | | * In the configuration files, conditions end with ") {" or just "{" |
337 | | */ |
338 | 266k | if ((depth == 0) && terminal[(uint8_t) *p]) { |
339 | 470 | return p - start; |
340 | 470 | } |
341 | | |
342 | | /* |
343 | | * "recurse" to get more conditions. |
344 | | */ |
345 | 266k | if (*p == '(') { |
346 | 0 | p++; |
347 | 0 | depth++; |
348 | 0 | was_regex = false; |
349 | 0 | continue; |
350 | 0 | } |
351 | | |
352 | 266k | if (*p == ')') { |
353 | 0 | if (!depth) { |
354 | 0 | fr_strerror_const("Too many ')'"); |
355 | 0 | return -(p - start); |
356 | 0 | } |
357 | | |
358 | 0 | p++; |
359 | 0 | depth--; |
360 | 0 | was_regex = false; |
361 | 0 | continue; |
362 | 0 | } |
363 | | |
364 | | /* |
365 | | * Parse xlats. They cannot span EOL. |
366 | | */ |
367 | 266k | if ((*p == '$') || (*p == '%')) { |
368 | 2.36k | if (end && ((p + 2) >= end)) { |
369 | 0 | fr_strerror_const("Expansions cannot extend across end of line"); |
370 | 0 | return -(p - start); |
371 | 0 | } |
372 | | |
373 | 2.36k | if ((p[1] == '{') || ((p[0] == '$') && (p[1] == '('))) { |
374 | 223 | slen = fr_skip_xlat(p, end); |
375 | | |
376 | 223 | check: |
377 | 223 | if (slen <= 0) return -(p - start) + slen; |
378 | | |
379 | 222 | p += slen; |
380 | 222 | continue; |
381 | 223 | } |
382 | | |
383 | | /* |
384 | | * Bare $ or %, just leave it alone. |
385 | | */ |
386 | 2.13k | p++; |
387 | 2.13k | was_regex = false; |
388 | 2.13k | continue; |
389 | 2.36k | } |
390 | | |
391 | | /* |
392 | | * Parse quoted strings. They cannot span EOL. |
393 | | */ |
394 | 263k | if ((*p == '"') || (*p == '\'') || (*p == '`') || (was_regex && (*p == '/'))) { |
395 | 0 | was_regex = false; |
396 | |
|
397 | 0 | slen = fr_skip_string((char const *) p, end); |
398 | 0 | goto check; |
399 | 0 | } |
400 | | |
401 | | /* |
402 | | * 192.168/16 is a netmask. So we only |
403 | | * allow regex after a regex operator. |
404 | | * |
405 | | * This isn't perfect, but is good enough |
406 | | * for most purposes. |
407 | | */ |
408 | 263k | if ((p[0] == '=') || (p[0] == '!')) { |
409 | 44.8k | if (end && ((p + 2) >= end)) { |
410 | 0 | fr_strerror_const("Operators cannot extend across end of line"); |
411 | 0 | return -(p - start); |
412 | 0 | } |
413 | | |
414 | 44.8k | if (p[1] == '~') { |
415 | 0 | was_regex = true; |
416 | 0 | p += 2; |
417 | 0 | continue; |
418 | 0 | } |
419 | | |
420 | | /* |
421 | | * Some other '==' or '!=', just leave it alone. |
422 | | */ |
423 | 44.8k | p++; |
424 | 44.8k | was_regex = false; |
425 | 44.8k | continue; |
426 | 44.8k | } |
427 | | |
428 | | /* |
429 | | * Any control characters (other than \t) cause an error. |
430 | | */ |
431 | 218k | if (*p < ' ') { |
432 | 1 | fr_strerror_const("Invalid escape in condition"); |
433 | 1 | return -(p - start); |
434 | 1 | } |
435 | | |
436 | 218k | was_regex = false; |
437 | | |
438 | | /* |
439 | | * Normal characters just get skipped. |
440 | | */ |
441 | 218k | if (*p != '\\') { |
442 | 218k | p++; |
443 | 218k | continue; |
444 | 218k | } |
445 | | |
446 | | /* |
447 | | * Backslashes at EOL are ignored. |
448 | | */ |
449 | 0 | if (end && ((p + 2) >= end)) break; |
450 | | |
451 | | /* |
452 | | * Escapes here are only one-character escapes. |
453 | | */ |
454 | 0 | if (p[1] < ' ') break; |
455 | 0 | p += 2; |
456 | 0 | } |
457 | | |
458 | | /* |
459 | | * We've fallen off of the end of a string. It may be OK? |
460 | | */ |
461 | 205 | if (eol) *eol = (depth > 0); |
462 | | |
463 | 205 | if (terminal[(uint8_t) *p]) return p - start; |
464 | | |
465 | 0 | fr_strerror_const("Unexpected end of condition"); |
466 | 0 | return -(p - start); |
467 | 205 | } |
468 | | |