Coverage Report

Created: 2026-09-01 06:21

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libssh/src/match.c
Line
Count
Source
1
/*
2
 * Author: Tatu Ylonen <ylo@cs.hut.fi>
3
 * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
4
 *                    All rights reserved
5
 * Simple pattern matching, with '*' and '?' as wildcards.
6
 *
7
 * As far as I am concerned, the code I have written for this software
8
 * can be used freely for any purpose.  Any derived versions of this
9
 * software must be clearly marked as such, and if the derived work is
10
 * incompatible with the protocol description in the RFC file, it must be
11
 * called by a name other than "ssh" or "Secure Shell".
12
 */
13
14
/*
15
 * Copyright (c) 2000 Markus Friedl.  All rights reserved.
16
 *
17
 * Redistribution and use in source and binary forms, with or without
18
 * modification, are permitted provided that the following conditions
19
 * are met:
20
 * 1. Redistributions of source code must retain the above copyright
21
 *    notice, this list of conditions and the following disclaimer.
22
 * 2. Redistributions in binary form must reproduce the above copyright
23
 *    notice, this list of conditions and the following disclaimer in the
24
 *    documentation and/or other materials provided with the distribution.
25
 *
26
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
27
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
28
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
29
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
30
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
31
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
32
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
33
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
34
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
35
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36
 */
37
38
#include "config.h"
39
40
#include <ctype.h>
41
#include <stdbool.h>
42
#include <sys/types.h>
43
#ifndef _WIN32
44
#include <arpa/inet.h>
45
#include <netinet/in.h>
46
#include <sys/socket.h>
47
#endif
48
49
/* for systems without IPv6 support matching should still work */
50
#ifndef INET6_ADDRSTRLEN
51
#define INET6_ADDRSTRLEN 46
52
#endif
53
54
#include "libssh/priv.h"
55
56
/**
57
 * @brief Compare a string with a pattern containing wildcards `*` and `?`
58
 *
59
 * This function is an iterative replacement for the previously recursive
60
 * implementation to avoid exponential complexity (DoS) with specific patterns.
61
 *
62
 * @param[in]  s        The string to match.
63
 * @param[in]  pattern  The pattern to match against.
64
 *
65
 * @return              1 if the pattern matches, 0 otherwise.
66
 */
67
static int match_pattern(const char *s, const char *pattern)
68
0
{
69
0
    const char *s_star = NULL; /* Position in s when last `*` was met */
70
0
    const char *p_star = NULL; /* Position in pattern after last `*` */
71
72
0
    if (s == NULL || pattern == NULL) {
73
0
        return 0;
74
0
    }
75
76
0
    while (*s) {
77
        /* Case 1: Exact match or '?' wildcard */
78
0
        if (*pattern == *s || *pattern == '?') {
79
0
            s++;
80
0
            pattern++;
81
0
            continue;
82
0
        }
83
84
        /* Case 2: '*' wildcard */
85
0
        if (*pattern == '*') {
86
            /* Record the position of the star and the current string position.
87
             * We optimistically assume * matches 0 characters first.
88
             */
89
0
            p_star = ++pattern;
90
0
            s_star = s;
91
0
            continue;
92
0
        }
93
94
        /* Case 3: Mismatch */
95
0
        if (p_star) {
96
            /* If we have seen a star previously, backtrack.
97
             * We restore the pattern to just after the star,
98
             * but advance the string position (consume one more char for the
99
             * star).
100
             * No need to backtrack to previous stars as any match of the last
101
             * star could be eaten the same way by the previous star.
102
             */
103
0
            pattern = p_star;
104
0
            s = ++s_star;
105
0
            continue;
106
0
        }
107
108
        /* Case 4: Mismatch and no star to backtrack to */
109
0
        return 0;
110
0
    }
111
112
    /* Handle trailing stars in the pattern
113
     * (e.g., pattern "abc*" matching "abc") */
114
0
    while (*pattern == '*') {
115
0
        pattern++;
116
0
    }
117
118
    /* If we reached the end of the pattern, it's a match */
119
0
    return (*pattern == '\0');
120
0
}
121
122
/*
123
 * Tries to match the string against the comma-separated sequence of subpatterns
124
 * (each possibly preceded by ! to indicate negation).
125
 * Returns -1 if negation matches, 1 if there is a positive match, 0 if there is
126
 * no match at all.
127
 */
128
int match_pattern_list(const char *string, const char *pattern,
129
0
    size_t len, int dolower) {
130
0
  char sub[1024];
131
0
  int negated;
132
0
  int got_positive;
133
0
  size_t i, subi;
134
135
0
  got_positive = 0;
136
0
  for (i = 0; i < len;) {
137
    /* Check if the subpattern is negated. */
138
0
    if (pattern[i] == '!') {
139
0
      negated = 1;
140
0
      i++;
141
0
    } else {
142
0
      negated = 0;
143
0
    }
144
145
    /*
146
     * Extract the subpattern up to a comma or end.  Convert the
147
     * subpattern to lowercase.
148
     */
149
0
    for (subi = 0;
150
0
        i < len && subi < sizeof(sub) - 1 && pattern[i] != ',';
151
0
        subi++, i++) {
152
0
      sub[subi] = dolower && isupper(pattern[i]) ?
153
0
        (char)tolower(pattern[i]) : pattern[i];
154
0
    }
155
156
    /* If subpattern too long, return failure (no match). */
157
0
    if (subi >= sizeof(sub) - 1) {
158
0
      return 0;
159
0
    }
160
161
    /* If the subpattern was terminated by a comma, skip the comma. */
162
0
    if (i < len && pattern[i] == ',') {
163
0
      i++;
164
0
    }
165
166
    /* Null-terminate the subpattern. */
167
0
    sub[subi] = '\0';
168
169
    /* Try to match the subpattern against the string. */
170
0
    if (match_pattern(string, sub)) {
171
0
      if (negated) {
172
0
        return -1;        /* Negative */
173
0
      } else {
174
0
        got_positive = 1; /* Positive */
175
0
      }
176
0
    }
177
0
  }
178
179
  /*
180
   * Return success if got a positive match.  If there was a negative
181
   * match, we have already returned -1 and never get here.
182
   */
183
0
  return got_positive;
184
0
}
185
186
/*
187
 * Tries to match the host name (which must be in all lowercase) against the
188
 * comma-separated sequence of subpatterns (each possibly preceded by ! to
189
 * indicate negation).
190
 * Returns -1 if negation matches, 1 if there is a positive match, 0 if there
191
 * is no match at all.
192
 */
193
int
194
match_hostname(const char *host, const char *pattern, size_t len)
195
0
{
196
0
    return match_pattern_list(host, pattern, len, 1);
197
0
}
198
199
#ifndef _WIN32
200
/**
201
 * @brief Tries to match the host IPv6 address against a given network address
202
 * with specified prefix length in CIDR notation.
203
 *
204
 * @param[in] host_addr     The host address to verify.
205
 *
206
 * @param[in] net_addr      The network id address against which the match is
207
 *                          being verified
208
 *
209
 * @param[in] bits          The prefix length
210
 *
211
 * @return 0 on a negative match.
212
 * @return 1 on a positive match.
213
 */
214
static int
215
cidr_match_6(struct in6_addr *host_addr,
216
             struct in6_addr *net_addr,
217
             unsigned int bits)
218
0
{
219
0
    const uint8_t *a = host_addr->s6_addr;
220
0
    const uint8_t *b = net_addr->s6_addr;
221
222
0
    unsigned int byte_whole, bits_left;
223
224
    /* The number of a complete byte covered by the prefix */
225
0
    byte_whole = bits / 8;
226
227
    /*
228
     * The number of bits remaining in the incomplete (last) byte
229
     * covered by the prefix
230
     */
231
0
    bits_left = bits % 8;
232
233
0
    if (byte_whole) {
234
0
        if (memcmp(a, b, byte_whole) != 0) {
235
0
            return 0;
236
0
        }
237
0
    }
238
239
0
    if (bits_left) {
240
0
        if ((a[byte_whole] ^ b[byte_whole]) & (0xFFu << (8 - bits_left))) {
241
0
            return 0;
242
0
        }
243
0
    }
244
245
0
    return 1;
246
0
}
247
248
/**
249
 * @brief Tries to match the host IPv4 address against a given network address
250
 * with specified prefix length in CIDR notation.
251
 *
252
 * @param[in] host_addr     The host address to verify.
253
 *
254
 * @param[in] net_addr      The network id address against which the match is
255
 *                          being verified
256
 *
257
 * @param[in] bits          The prefix length
258
 *
259
 * @return 0 on a negative match.
260
 * @return 1 on a positive match.
261
 */
262
static int
263
cidr_match_4(struct in_addr *host_addr,
264
             struct in_addr *net_addr,
265
             unsigned int bits)
266
0
{
267
0
    if (bits == 0) {
268
        /* C99 6.5.7 (3): u32 << 32 is undefined behaviour */
269
0
        return 1;
270
0
    }
271
272
0
    return !((host_addr->s_addr ^ net_addr->s_addr) &
273
0
             htonl((0xFFFFFFFFu << (32 - bits)) & 0xFFFFFFFFu));
274
0
}
275
276
/**
277
 * @brief Checks if the mask length is valid according to the address family
278
 * (IPv4 or IPv6).
279
 *
280
 * @param[in] family    The address family (e.g. AF_INET or AF_INET6)
281
 *
282
 * @param[in] mask      The subnet mask (prefix)
283
 *
284
 * @return true if the mask length does not exceed the maximum valid length
285
 * according to the address family (IPv4 or IPv6).
286
 * @return false if the mask length exceeds the maximum valid length
287
 * or there is no match with IPv4 or IPv6 address family.
288
 */
289
static bool
290
masklen_valid(int family, unsigned int mask)
291
0
{
292
0
    switch (family) {
293
0
    case AF_INET:
294
0
        return mask <= 32;
295
0
    case AF_INET6:
296
0
        return mask <= 128;
297
0
    default:
298
0
        return false;
299
0
    }
300
0
}
301
302
/**
303
 * @brief Extracts address family given a network address.
304
 *
305
 * @param[in] address   The network address.
306
 *
307
 * @return The value of the address family if no errors.
308
 * @return -1 in case of errors.
309
 */
310
static int
311
get_address_family(const char *address)
312
0
{
313
0
    struct addrinfo hints, *ai = NULL;
314
0
    int rc = -1, rv;
315
316
0
    ZERO_STRUCT(hints);
317
0
    if (address == NULL) {
318
0
        SSH_LOG(SSH_LOG_TRACE, "Bad arguments");
319
0
        goto out;
320
0
    }
321
322
0
    hints.ai_flags = AI_NUMERICHOST;
323
0
    rv = getaddrinfo(address, NULL, &hints, &ai);
324
0
    if (rv != 0) {
325
0
        SSH_LOG(SSH_LOG_TRACE,
326
0
                "Couldn't get address information - getaddrinfo() failed: %s",
327
0
                gai_strerror(rv));
328
0
        goto out;
329
0
    }
330
331
0
    rc = ai->ai_family;
332
0
    freeaddrinfo(ai);
333
334
0
out:
335
0
    return rc;
336
0
}
337
338
/**
339
 * @brief Tries to match the host address against a CIDR list provided
340
 * by the user. If the host address family is unknown, it can be derived by
341
 * passing -1 as sa_family argument.
342
 *
343
 * It can be also used to validate a CIDR list when the passed address is NULL
344
 * and sa_family is -1.
345
 *
346
 * @param[in] address   The host address to verify (NULL to validate CIDR list).
347
 *
348
 * @param[in] addrlist  The CIDR list against which the match is being verified.
349
 *                      The CIDR list can contain both IPv4 and IPv6 addresses
350
 *                      and has to be comma separated
351
 *                      (',' only, space after comma not allowed).
352
 *
353
 * @param[in] sa_family The socket address family (e.g. AF_INET or AF_INET6,
354
 *                      -1 to validate CIDR list or unknown address family).
355
 *
356
 * @usage To validate CIDR list: match_cidr_address_list(NULL, addrlist, -1).
357
 * @usage To verify a match with unknown address family:
358
 *        match_cidr_address_list(address, addrlist, -1).
359
 * @return  1 only on positive match.
360
 * @return  0 on negative match or valid CIDR list.
361
 * @return  -1 on errors or invalid CIDR list.
362
 */
363
int
364
match_cidr_address_list(const char *address,
365
                        const char *addrlist,
366
                        int sa_family)
367
0
{
368
0
    char *list = NULL, *cp = NULL, *a = NULL, *b = NULL, *sp = NULL;
369
0
    char addr_buffer[64], addr[NI_MAXHOST];
370
0
    struct in_addr try_addr, match_addr;
371
0
    struct in6_addr try_addr6, match_addr6;
372
0
    unsigned long mask_len;
373
0
    size_t addr_len, tmp_len;
374
0
    int rc = 0, r, ai_family;
375
376
0
    ZERO_STRUCT(try_addr);
377
0
    ZERO_STRUCT(try_addr6);
378
0
    ZERO_STRUCT(match_addr);
379
0
    ZERO_STRUCT(match_addr6);
380
381
0
    if (sa_family != AF_INET && sa_family != AF_INET6 && sa_family != -1) {
382
0
        SSH_LOG(SSH_LOG_TRACE,
383
0
                "Invalid argument: sa_family %d is not valid",
384
0
                sa_family);
385
0
        return -1;
386
0
    }
387
388
0
    if (address != NULL) {
389
0
        strlcpy(addr, address, sizeof(addr));
390
391
        /* Remove interface in case of IPv6 address: addr%interface */
392
0
        a = strchr(addr, '%');
393
0
        if (a != NULL) {
394
0
            *a = '\0';
395
0
        }
396
397
        /*
398
         * If sa_family is set to -1 and address is not NULL then
399
         * the socket address family should be derived
400
         */
401
0
        if (sa_family == -1) {
402
0
            r = get_address_family(addr);
403
0
            if (r == -1) {
404
0
                SSH_LOG(SSH_LOG_TRACE,
405
0
                        "Failed to derive address family for address "
406
0
                        "\"%.100s\"",
407
0
                        addr);
408
0
                return -1;
409
0
            }
410
0
            sa_family = r;
411
0
        }
412
413
        /*
414
         * Translate host address from dot notation to binary network format
415
         * according to family type,
416
         * i.e. IPv4 (store in in_addr) or IPv6 (store in in6_addr)
417
         */
418
0
        if (sa_family == AF_INET) {
419
0
            if (inet_pton(AF_INET, addr, &try_addr) == 0) {
420
0
                SSH_LOG(SSH_LOG_TRACE,
421
0
                        "Couldn't parse IPv4 address \"%.100s\"",
422
0
                        addr);
423
0
                return -1;
424
0
            }
425
0
        } else if (sa_family == AF_INET6) {
426
0
            if (inet_pton(AF_INET6, addr, &try_addr6) == 0) {
427
0
                SSH_LOG(SSH_LOG_TRACE,
428
0
                        "Couldn't parse IPv6 address \"%.100s\"",
429
0
                        addr);
430
0
                return -1;
431
0
            }
432
0
        } else {
433
0
            SSH_LOG(SSH_LOG_TRACE,
434
0
                    "Address family %d for address \"%.100s\" "
435
0
                    "is not recognized",
436
0
                    sa_family,
437
0
                    addr);
438
0
            return -1;
439
0
        }
440
0
    }
441
442
0
    b = list = strdup(addrlist);
443
0
    if (b == NULL) {
444
0
        return -1;
445
0
    }
446
447
0
    while ((cp = strsep(&list, ",")) != NULL) {
448
0
        if (*cp == '\0') {
449
0
            SSH_LOG(SSH_LOG_TRACE, "Empty entry in list \"%.100s\"", b);
450
0
            rc = -1;
451
0
            break;
452
0
        }
453
454
        /*
455
         * Stop junk from reaching address translation. +3 for the "/prefix".
456
         * INET6_ADDRSTRLEN is 46 and includes space for '\0' terminator. The
457
         * maximum IPv6 address printable is the one that carries IPv4 too.
458
         * E.g. ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255 is 46 chars
459
         * long ('\0' included) and the maximum prefix length possible is 96.
460
         * This explains why +3. All the other IPv6 addresses with maximum /127
461
         * prefix length (39 + 4) are covered just by INET6_ADDRSTRLEN itself
462
         */
463
0
        addr_len = strlen(cp);
464
0
        if (addr_len > INET6_ADDRSTRLEN + 3) {
465
0
            SSH_LOG(SSH_LOG_TRACE,
466
0
                    "List entry \"%.100s\" too long: %zu > %d (MAX ALLOWED)",
467
0
                    cp,
468
0
                    addr_len,
469
0
                    INET6_ADDRSTRLEN + 3);
470
0
            rc = -1;
471
0
            break;
472
0
        }
473
474
0
#define VALID_CIDR_CHARS "0123456789abcdefABCDEF.:/"
475
0
        tmp_len = strspn(cp, VALID_CIDR_CHARS);
476
0
        if (tmp_len != addr_len) {
477
0
            SSH_LOG(SSH_LOG_TRACE,
478
0
                    "List entry \"%.100s\" contains invalid characters "
479
0
                    "-> \"%c\" is an invalid character",
480
0
                    cp,
481
0
                    cp[tmp_len]);
482
0
            rc = -1;
483
0
            break;
484
0
        }
485
0
#undef VALID_CIDR_CHARS
486
487
0
        strlcpy(addr_buffer, cp, sizeof(addr_buffer));
488
0
        sp = strchr(addr_buffer, '/');
489
0
        if (sp != NULL) {
490
0
            *sp = '\0';
491
0
            sp++;
492
0
            mask_len = strtoul(sp, &cp, 10);
493
0
            if (*sp < '0' || *sp > '9' || *cp != '\0') {
494
0
                SSH_LOG(SSH_LOG_TRACE, "Error while parsing prefix: %s", sp);
495
0
                rc = -1;
496
0
                break;
497
0
            }
498
0
            if (mask_len > 128) {
499
0
                SSH_LOG(SSH_LOG_TRACE,
500
0
                        "Invalid prefix: %lu exceeds the maximum allowed "
501
0
                        "(>128)",
502
0
                        mask_len);
503
0
                rc = -1;
504
0
                break;
505
0
            }
506
0
        } else {
507
0
            SSH_LOG(SSH_LOG_TRACE,
508
0
                    "Missing prefix length for list entry \"%.100s\"",
509
0
                    addr_buffer);
510
0
            rc = -1;
511
0
            break;
512
0
        }
513
514
0
        ai_family = get_address_family(addr_buffer);
515
0
        if (ai_family == -1) {
516
0
            SSH_LOG(SSH_LOG_TRACE,
517
0
                    "Couldn't get address family for \"%.100s\"",
518
0
                    addr_buffer);
519
0
            rc = -1;
520
0
            break;
521
0
        }
522
523
0
        if (ai_family == AF_INET) {
524
0
            if (inet_pton(AF_INET, addr_buffer, &match_addr) == 0) {
525
0
                SSH_LOG(SSH_LOG_TRACE,
526
0
                        "Couldn't parse IPv4 address \"%.100s\"",
527
0
                        addr_buffer);
528
0
                rc = -1;
529
0
                break;
530
0
            }
531
0
        } else if (ai_family == AF_INET6) {
532
0
            if (inet_pton(AF_INET6, addr_buffer, &match_addr6) == 0) {
533
0
                SSH_LOG(SSH_LOG_TRACE,
534
0
                        "Couldn't parse IPv6 address \"%.100s\"",
535
0
                        addr_buffer);
536
0
                rc = -1;
537
0
                break;
538
0
            }
539
0
        } else {
540
0
            SSH_LOG(SSH_LOG_TRACE,
541
0
                    "Address family %d for address \"%.100s\" "
542
0
                    "is not recognized",
543
0
                    ai_family,
544
0
                    addr_buffer);
545
0
            rc = -1;
546
0
            break;
547
0
        }
548
549
0
        if (masklen_valid(ai_family, mask_len) != true) {
550
0
            SSH_LOG(SSH_LOG_TRACE,
551
0
                    "Invalid mask length %lu for list entry \"%.100s\"",
552
0
                    mask_len,
553
0
                    addr_buffer);
554
0
            rc = -1;
555
0
            break;
556
0
        }
557
558
        /* Verify match between host address and network address*/
559
0
        if (((ai_family == AF_INET && sa_family == AF_INET) &&
560
0
             cidr_match_4(&try_addr, &match_addr, mask_len)) ||
561
0
            ((ai_family == AF_INET6 && sa_family == AF_INET6) &&
562
0
             cidr_match_6(&try_addr6, &match_addr6, mask_len))) {
563
0
            rc = 1;
564
0
            break;
565
0
        }
566
0
    }
567
0
    SAFE_FREE(b);
568
569
0
    return rc;
570
0
}
571
#endif /* _WIN32 */
572
573
/**
574
 * @brief Tries to match an object against a comma separated group of objects
575
 *
576
 * The characters '*' and '?' are NOT considered wildcards and an object in the
577
 * group preceded by a ! does NOT indicate negation. The characters '*', '?'
578
 * and '!' are treated normally like other characters, only ',' (comma) is
579
 * treated specially and is considered as a delimiter that separates objects in
580
 * the group.
581
 *
582
 * @param[in] group     Group of objects (comma separated) to match against.
583
 *
584
 * @param[in] object    Object to match.
585
 *
586
 * @returns             1 if there is a match, 0 if there is no match at all.
587
 */
588
int match_group(const char *group, const char *object)
589
0
{
590
0
    const char *a = NULL;
591
0
    const char *z = NULL;
592
593
0
    if (group == NULL || object == NULL) {
594
0
        return 0;
595
0
    }
596
597
0
    z = group;
598
0
    do {
599
0
        a = strchr(z, ',');
600
0
        if (a == NULL) {
601
0
            if (strcmp(z, object) == 0) {
602
0
                return 1;
603
0
            }
604
0
            return 0;
605
0
        } else {
606
0
            if (strncmp(z, object, a - z) == 0) {
607
0
                return 1;
608
0
            }
609
0
        }
610
0
        z = a + 1;
611
0
    } while (1);
612
613
    /* not reached */
614
0
    return 0;
615
0
}