The FreeRADIUS server $Id: 15bac2a4c627c01d1aa2047687b3418955ac7f00 $
Loading...
Searching...
No Matches
skip.c
Go to the documentation of this file.
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 */
23RCSID("$Id: e9ac32fb36cfcd151d798ac5dc662dd355cb2679 $")
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 */
36ssize_t fr_skip_string(char const *start, char const *end)
37{
38 char const *p = start;
39 char quote;
40
41 quote = *(p++);
42
43 while ((end && (p < end)) || *p) {
44 /*
45 * Stop at the quotation character
46 */
47 if (*p == quote) {
48 p++;
49 return p - start;
50 }
51
52 /*
53 * Not an escape character: it's OK.
54 */
55 if (*p != '\\') {
56 p++;
57 continue;
58 }
59
60 if (end && ((p + 2) >= end)) {
61 fail:
62 fr_strerror_const("Unexpected escape at end of string");
63 return -(p - start);
64 }
65
66 /*
67 * Escape at EOL is not allowed.
68 */
69 if (p[1] < ' ') goto fail;
70
71 /*
72 * \r or \n, etc.
73 */
74 if (!isdigit((uint8_t) p[1])) {
75 p += 2;
76 continue;
77 }
78
79 /*
80 * Double-quoted strings use \000
81 * Regexes use \0
82 */
83 if (quote == '/') {
84 p++;
85 continue;
86 }
87
88 if (end && ((p + 4) >= end)) goto fail;
89
90 /*
91 * Allow for \1f in single quoted strings
92 */
93 if ((quote == '\'') && isxdigit((uint8_t) p[1]) && isxdigit((uint8_t) p[2])) {
94 p += 3;
95 continue;
96 }
97
98 if (!isdigit((uint8_t) p[2]) || !isdigit((uint8_t) p[3])) {
99 fr_strerror_const("Invalid octal escape");
100 return -(p - start);
101 }
102
103 p += 4;
104 }
105
106 /*
107 * Unexpected end of string.
108 */
109 fr_strerror_const("Unexpected end of string");
110 return -(p - start);
111}
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#define SKIP_MAX_DEPTH 64
120
121static ssize_t skip_brackets(char const *start, char const *end, char end_quote, unsigned int depth);
122static ssize_t skip_xlat(char const *start, char const *end, unsigned int depth);
123
124static ssize_t skip_brackets(char const *start, char const *end, char end_quote, unsigned int depth)
125{
126 ssize_t slen;
127 char const *p = start;
128
129 if (depth >= SKIP_MAX_DEPTH) {
130 fr_strerror_const("Nesting too deep");
131 return -(p - start);
132 }
133
134 while ((end && (p < end)) || *p) {
135 if (*p == end_quote) {
136 p++;
137 return p - start;
138 }
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 if (*p == '(') {
147 p++;
148 slen = skip_brackets(p, end, ')', depth + 1);
149
150 next:
151 if (slen <= 0) return slen - (p - start);
152
153 fr_assert((size_t) slen <= (size_t) (end - p));
154 p += slen;
155 continue;
156 }
157
158 /*
159 * A quoted string.
160 */
161 if ((*p == '"') || (*p == '\'') || (*p == '`')) {
162 slen = fr_skip_string(p, end);
163 goto next;
164 }
165
166 /*
167 * Nested expansion.
168 */
169 if ((p[0] == '$') || (p[0] == '%')) {
170 if (end && (p + 2) >= end) break;
171
172 /*
173 * %% inside of an xlat
174 */
175 if ((p[0] == '%') && (p[1] == '%')) {
176 p += 2;
177 continue;
178 }
179
180 if ((p[1] == '{') || (p[1] == '(')) {
181 slen = skip_xlat(p, end, depth + 1);
182 goto next;
183 }
184
185 /*
186 * Bare $ or %, just leave it alone.
187 */
188 p++;
189 continue;
190 }
191
192 /*
193 * Escapes are special.
194 */
195 if (*p != '\\') {
196 p++;
197 continue;
198 }
199
200 if (end && ((p + 2) >= end)) break;
201
202 /*
203 * Escapes here are only one-character escapes.
204 */
205 if (p[1] < ' ') break;
206 p += 2;
207 }
208
209 /*
210 * Unexpected end of xlat
211 */
212 fr_strerror_const("Unexpected end of expansion");
213 return -(p - start);
214}
215
216static ssize_t skip_xlat(char const *start, char const *end, unsigned int depth)
217{
218 ssize_t slen;
219 char const *p = start;
220
221 if (depth >= SKIP_MAX_DEPTH) {
222 fr_strerror_const("Nesting too deep");
223 return -(p - start);
224 }
225
226 /*
227 * At least %{1} or $(.)
228 */
229 if (end && ((end - start) < 4)) {
230 fr_strerror_const("Invalid expansion");
231 return 0;
232 }
233
234 if (!((memcmp(p, "%{", 2) == 0) || /* xlat */
235 (memcmp(p, "${", 2) == 0) || /* config file macro */
236 (memcmp(p, "$(", 2) == 0))) { /* shell expansion in an back-ticks argument */
237 fr_strerror_const("Invalid expansion");
238 return 0;
239 }
240 p++;
241
242 if (*p == '(') {
243 p++; /* skip the '(' */
244 slen = skip_brackets(p, end, ')', depth + 1);
245
246 } else if (*p == '{') {
247 p++; /* skip the '{' */
248 slen = skip_brackets(p, end, '}', depth + 1);
249
250 } else {
251 char const *q = p;
252
253 /*
254 * New xlat syntax: %foo(...)
255 */
256 while (isalnum((int) *q) || (*q == '.') || (*q == '_') || (*q == '-')) {
257 q++;
258 }
259
260 if (*q != '(') {
261 fr_strerror_const("Invalid character after '%'");
262 return -(p - start);
263 }
264
265 p = q + 1;
266
267 slen = skip_brackets(p, end, ')', depth + 1);
268 }
269
270 if (slen <= 0) return slen - (p - start);
271 return slen + (p - start);
272}
273
274/** Skip a generic {...} or (...) arguments
275 *
276 */
277ssize_t fr_skip_brackets(char const *start, char const *end, char end_quote)
278{
279 return skip_brackets(start, end, end_quote, 0);
280}
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 */
296ssize_t fr_skip_xlat(char const *start, char const *end)
297{
298 return skip_xlat(start, end, 0);
299}
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 */
317ssize_t fr_skip_condition(char const *start, char const *end, bool const terminal[static SBUFF_CHAR_CLASS], bool *eol)
318{
319 char const *p = start;
320 bool was_regex = false;
321 int depth = 0;
322 ssize_t slen;
323
324 if (eol) *eol = false;
325
326 /*
327 * Keep parsing the condition until we hit EOS or EOL.
328 */
329 while ((end && (p < end)) || *p) {
330 if (isspace((uint8_t) *p)) {
331 p++;
332 continue;
333 }
334
335 /*
336 * In the configuration files, conditions end with ") {" or just "{"
337 */
338 if ((depth == 0) && terminal[(uint8_t) *p]) {
339 return p - start;
340 }
341
342 /*
343 * "recurse" to get more conditions.
344 */
345 if (*p == '(') {
346 p++;
347 depth++;
348 was_regex = false;
349 continue;
350 }
351
352 if (*p == ')') {
353 if (!depth) {
354 fr_strerror_const("Too many ')'");
355 return -(p - start);
356 }
357
358 p++;
359 depth--;
360 was_regex = false;
361 continue;
362 }
363
364 /*
365 * Parse xlats. They cannot span EOL.
366 */
367 if ((*p == '$') || (*p == '%')) {
368 if (end && ((p + 2) >= end)) {
369 fr_strerror_const("Expansions cannot extend across end of line");
370 return -(p - start);
371 }
372
373 if ((p[1] == '{') || ((p[0] == '$') && (p[1] == '('))) {
374 slen = fr_skip_xlat(p, end);
375
376 check:
377 if (slen <= 0) return -(p - start) + slen;
378
379 p += slen;
380 continue;
381 }
382
383 /*
384 * Bare $ or %, just leave it alone.
385 */
386 p++;
387 was_regex = false;
388 continue;
389 }
390
391 /*
392 * Parse quoted strings. They cannot span EOL.
393 */
394 if ((*p == '"') || (*p == '\'') || (*p == '`') || (was_regex && (*p == '/'))) {
395 was_regex = false;
396
397 slen = fr_skip_string((char const *) p, end);
398 goto check;
399 }
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 if ((p[0] == '=') || (p[0] == '!')) {
409 if (end && ((p + 2) >= end)) {
410 fr_strerror_const("Operators cannot extend across end of line");
411 return -(p - start);
412 }
413
414 if (p[1] == '~') {
415 was_regex = true;
416 p += 2;
417 continue;
418 }
419
420 /*
421 * Some other '==' or '!=', just leave it alone.
422 */
423 p++;
424 was_regex = false;
425 continue;
426 }
427
428 /*
429 * Any control characters (other than \t) cause an error.
430 */
431 if (*p < ' ') break;
432
433 was_regex = false;
434
435 /*
436 * Normal characters just get skipped.
437 */
438 if (*p != '\\') {
439 p++;
440 continue;
441 }
442
443 /*
444 * Backslashes at EOL are ignored.
445 */
446 if (end && ((p + 2) >= end)) break;
447
448 /*
449 * Escapes here are only one-character escapes.
450 */
451 if (p[1] < ' ') break;
452 p += 2;
453 }
454
455 /*
456 * We've fallen off of the end of a string. It may be OK?
457 */
458 if (eol) *eol = (depth > 0);
459
460 if (terminal[(uint8_t) *p]) return p - start;
461
462 fr_strerror_const("Unexpected end of condition");
463 return -(p - start);
464}
465
#define RCSID(id)
Definition build.h:512
long int ssize_t
unsigned char uint8_t
static uint8_t depth(fr_minmax_heap_index_t i)
Definition minmax_heap.c:83
#define fr_assert(_expr)
Definition rad_assert.h:37
#define SBUFF_CHAR_CLASS
Definition sbuff.h:203
ssize_t fr_skip_brackets(char const *start, char const *end, char end_quote)
Skip a generic {...} or (...) arguments.
Definition skip.c:277
ssize_t fr_skip_string(char const *start, char const *end)
Skip a quoted string.
Definition skip.c:36
ssize_t fr_skip_condition(char const *start, char const *end, bool const terminal[static SBUFF_CHAR_CLASS], bool *eol)
Skip a conditional expression.
Definition skip.c:317
static ssize_t skip_xlat(char const *start, char const *end, unsigned int depth)
Definition skip.c:216
#define SKIP_MAX_DEPTH
Definition skip.c:119
static ssize_t skip_brackets(char const *start, char const *end, char end_quote, unsigned int depth)
Definition skip.c:124
ssize_t fr_skip_xlat(char const *start, char const *end)
Skip an xlat expression.
Definition skip.c:296
#define fr_strerror_const(_msg)
Definition strerror.h:223