Coverage Report

Created: 2026-06-30 07:16

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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