Coverage Report

Created: 2026-06-07 06:51

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/openssh/match.c
Line
Count
Source
1
/* $OpenBSD: match.c,v 1.46 2026/05/31 04:19:16 djm Exp $ */
2
/*
3
 * Author: Tatu Ylonen <ylo@cs.hut.fi>
4
 * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
5
 *                    All rights reserved
6
 * Simple pattern matching, with '*' and '?' as wildcards.
7
 *
8
 * As far as I am concerned, the code I have written for this software
9
 * can be used freely for any purpose.  Any derived versions of this
10
 * software must be clearly marked as such, and if the derived work is
11
 * incompatible with the protocol description in the RFC file, it must be
12
 * called by a name other than "ssh" or "Secure Shell".
13
 */
14
/*
15
 * Copyright (c) 2000 Markus Friedl.  All rights reserved.
16
 * Copyright (c) 2026 Damien Miller.  All rights reserved.
17
 *
18
 * Redistribution and use in source and binary forms, with or without
19
 * modification, are permitted provided that the following conditions
20
 * are met:
21
 * 1. Redistributions of source code must retain the above copyright
22
 *    notice, this list of conditions and the following disclaimer.
23
 * 2. Redistributions in binary form must reproduce the above copyright
24
 *    notice, this list of conditions and the following disclaimer in the
25
 *    documentation and/or other materials provided with the distribution.
26
 *
27
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
28
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
29
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
30
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
31
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
32
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
33
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
34
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
35
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
36
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37
 */
38
39
#include "includes.h"
40
41
#include <sys/types.h>
42
43
#include <ctype.h>
44
#include <stdlib.h>
45
#include <string.h>
46
#include <stdarg.h>
47
#include <stdio.h>
48
49
#include "xmalloc.h"
50
#include "match.h"
51
#include "misc.h"
52
53
/*
54
 * Computes the epsilon closure of an NFA set.
55
 * In our wildcard grammar, epsilon transitions only exist for '*' wildcards,
56
 * allowing us to transition from state i to i+1 without consuming input.
57
 *
58
 * This function modifies 'states' in place.
59
 */
60
static void
61
epsilon_closure(char *states, const char *pattern, size_t M)
62
0
{
63
0
  size_t i;
64
65
  /* only need a forward pass as there are no back jumps in our grammar */
66
0
  for (i = 0; i < M; i++) {
67
0
    if (!states[i] || pattern[i] != '*')
68
0
      continue;
69
    /*
70
     * State i is active, and pattern[i] is '*', so we can
71
     * epsilon-transition to i+1.
72
     */
73
0
    states[i + 1] = 1;
74
0
  }
75
0
}
76
77
/*
78
 * Returns true if the given string matches the pattern (which may contain ?
79
 * and * as wildcards), and zero if it does not match. Uses an NFA internally.
80
 */
81
int
82
match_pattern(const char *s, const char *pattern)
83
0
{
84
0
  size_t M;
85
0
  size_t i;
86
0
  char *states, *next_states, *tmp;
87
0
  int active, matched = 0;
88
89
  /* trivial case: empty pattern vs empty input */
90
0
  if ((M = strlen(pattern)) == 0)
91
0
    return *s == '\0';
92
93
  /* A state for each pattern character, plus one final accepting state */
94
0
  states = xcalloc(M + 1, sizeof(*states));
95
0
  next_states = xcalloc(M + 1, sizeof(*next_states));
96
97
  /* Initial state: state 0 is active */
98
0
  states[0] = 1;
99
  /* Other states might be reachable now if the pattern starts with '*' */
100
0
  epsilon_closure(states, pattern, M);
101
102
0
  for (; *s; s++) {
103
0
    memset(next_states, 0, M + 1);
104
105
    /* Calculate the reachable next states given the input char */
106
0
    for (i = 0; i < M; i++) {
107
0
      if (!states[i])
108
0
        continue;
109
0
      if (pattern[i] == '*') {
110
        /*
111
         * '*' matches any character, so we can
112
         * stay in state i
113
         */
114
0
        next_states[i] = 1;
115
0
      } else if (pattern[i] == '?' || pattern[i] == *s) {
116
        /*
117
         * '?' matches any character, or we have
118
         * a literal match.
119
         */
120
0
        next_states[i + 1] = 1;
121
0
      }
122
0
    }
123
124
    /* Expand the reachable next states with epsilon transitions */
125
0
    epsilon_closure(next_states, pattern, M);
126
127
    /* Swap states and next_states */
128
0
    tmp = states;
129
0
    states = next_states;
130
0
    next_states = tmp;
131
132
    /* Check if we have any active pattern states left */
133
0
    active = 0;
134
0
    for (i = 0; i <= M; i++) {
135
0
      if (states[i]) {
136
0
        active = 1;
137
0
        break;
138
0
      }
139
0
    }
140
0
    if (!active)
141
0
      goto out; /* No active states, fail early */
142
0
  }
143
  /*
144
   * We matched only if we ended up in the final, accepting state
145
   * after consuming all the input.
146
   */
147
0
  matched = states[M];
148
0
 out:
149
0
  free(states);
150
0
  free(next_states);
151
0
  return matched;
152
0
}
153
154
/*
155
 * Tries to match the string against the
156
 * comma-separated sequence of subpatterns (each possibly preceded by ! to
157
 * indicate negation).  Returns -1 if negation matches, 1 if there is
158
 * a positive match, 0 if there is no match at all.
159
 */
160
int
161
match_pattern_list(const char *string, const char *pattern, int dolower)
162
0
{
163
0
  char sub[1024];
164
0
  int negated;
165
0
  int got_positive;
166
0
  u_int i, subi, len = strlen(pattern);
167
168
0
  got_positive = 0;
169
0
  for (i = 0; i < len;) {
170
    /* Check if the subpattern is negated. */
171
0
    if (pattern[i] == '!') {
172
0
      negated = 1;
173
0
      i++;
174
0
    } else
175
0
      negated = 0;
176
177
    /*
178
     * Extract the subpattern up to a comma or end.  Convert the
179
     * subpattern to lowercase.
180
     */
181
0
    for (subi = 0;
182
0
        i < len && subi < sizeof(sub) - 1 && pattern[i] != ',';
183
0
        subi++, i++)
184
0
      sub[subi] = dolower && isupper((u_char)pattern[i]) ?
185
0
          tolower((u_char)pattern[i]) : pattern[i];
186
    /* If subpattern too long, return failure (no match). */
187
0
    if (subi >= sizeof(sub) - 1)
188
0
      return 0;
189
190
    /* If the subpattern was terminated by a comma, then skip it. */
191
0
    if (i < len && pattern[i] == ',')
192
0
      i++;
193
194
    /* Null-terminate the subpattern. */
195
0
    sub[subi] = '\0';
196
197
    /* Try to match the subpattern against the string. */
198
0
    if (match_pattern(string, sub)) {
199
0
      if (negated)
200
0
        return -1;   /* Negative */
201
0
      else
202
0
        got_positive = 1; /* Positive */
203
0
    }
204
0
  }
205
206
  /*
207
   * Return success if got a positive match.  If there was a negative
208
   * match, we have already returned -1 and never get here.
209
   */
210
0
  return got_positive;
211
0
}
212
213
/* Match a list representing users or groups. */
214
int
215
match_usergroup_pattern_list(const char *string, const char *pattern)
216
0
{
217
#ifdef HAVE_CYGWIN
218
  /* Windows usernames may be Unicode and are not case sensitive */
219
  return cygwin_ug_match_pattern_list(string, pattern);
220
#else
221
  /* Case sensitive match */
222
0
  return match_pattern_list(string, pattern, 0);
223
0
#endif
224
0
}
225
226
/*
227
 * Tries to match the host name (which must be in all lowercase) against the
228
 * comma-separated sequence of subpatterns (each possibly preceded by ! to
229
 * indicate negation).  Returns -1 if negation matches, 1 if there is
230
 * a positive match, 0 if there is no match at all.
231
 */
232
int
233
match_hostname(const char *host, const char *pattern)
234
0
{
235
0
  char *hostcopy = xstrdup(host);
236
0
  int r;
237
238
0
  lowercase(hostcopy);
239
0
  r = match_pattern_list(hostcopy, pattern, 1);
240
0
  free(hostcopy);
241
0
  return r;
242
0
}
243
244
/*
245
 * returns 0 if we get a negative match for the hostname or the ip
246
 * or if we get no match at all.  returns -1 on error, or 1 on
247
 * successful match.
248
 */
249
int
250
match_host_and_ip(const char *host, const char *ipaddr,
251
    const char *patterns)
252
0
{
253
0
  int mhost, mip;
254
255
0
  if ((mip = addr_match_list(ipaddr, patterns)) == -2)
256
0
    return -1; /* error in ipaddr match */
257
0
  else if (host == NULL || ipaddr == NULL || mip == -1)
258
0
    return 0; /* negative ip address match, or testing pattern */
259
260
  /* negative hostname match */
261
0
  if ((mhost = match_hostname(host, patterns)) == -1)
262
0
    return 0;
263
  /* no match at all */
264
0
  if (mhost == 0 && mip == 0)
265
0
    return 0;
266
0
  return 1;
267
0
}
268
269
/*
270
 * Match user, user@host_or_ip, user@host_or_ip_list against pattern.
271
 * If user, host and ipaddr are all NULL then validate pattern/
272
 * Returns -1 on invalid pattern, 0 on no match, 1 on match.
273
 */
274
int
275
match_user(const char *user, const char *host, const char *ipaddr,
276
    const char *pattern)
277
0
{
278
0
  char *p, *pat;
279
0
  int ret;
280
281
  /* test mode */
282
0
  if (user == NULL && host == NULL && ipaddr == NULL) {
283
0
    if ((p = strrchr(pattern, '@')) != NULL &&
284
0
        match_host_and_ip(NULL, NULL, p + 1) < 0)
285
0
      return -1;
286
0
    return 0;
287
0
  }
288
289
0
  if (user == NULL)
290
0
    return 0; /* shouldn't happen */
291
292
0
  if (strrchr(pattern, '@') == NULL)
293
0
    return match_pattern(user, pattern);
294
295
0
  pat = xstrdup(pattern);
296
0
  p = strrchr(pat, '@');
297
0
  *p++ = '\0';
298
299
0
  if ((ret = match_pattern(user, pat)) == 1)
300
0
    ret = match_host_and_ip(host, ipaddr, p);
301
0
  free(pat);
302
303
0
  return ret;
304
0
}
305
306
/*
307
 * Returns first item from client-list that is also supported by server-list,
308
 * caller must free the returned string.
309
 */
310
0
#define MAX_PROP  40
311
0
#define SEP ","
312
char *
313
match_list(const char *client, const char *server, u_int *next)
314
0
{
315
0
  char *sproposals[MAX_PROP];
316
0
  char *c, *s, *p, *ret, *cp, *sp;
317
0
  int i, j, nproposals;
318
319
0
  c = cp = xstrdup(client);
320
0
  s = sp = xstrdup(server);
321
322
0
  for ((p = strsep(&sp, SEP)), i=0; p && *p != '\0';
323
0
      (p = strsep(&sp, SEP)), i++) {
324
0
    if (i < MAX_PROP)
325
0
      sproposals[i] = p;
326
0
    else
327
0
      break;
328
0
  }
329
0
  nproposals = i;
330
331
0
  for ((p = strsep(&cp, SEP)), i=0; p && *p != '\0';
332
0
      (p = strsep(&cp, SEP)), i++) {
333
0
    for (j = 0; j < nproposals; j++) {
334
0
      if (strcmp(p, sproposals[j]) == 0) {
335
0
        ret = xstrdup(p);
336
0
        if (next != NULL)
337
0
          *next = (cp == NULL) ?
338
0
              strlen(c) : (u_int)(cp - c);
339
0
        free(c);
340
0
        free(s);
341
0
        return ret;
342
0
      }
343
0
    }
344
0
  }
345
0
  if (next != NULL)
346
0
    *next = strlen(c);
347
0
  free(c);
348
0
  free(s);
349
0
  return NULL;
350
0
}
351
352
/*
353
 * Filter proposal using pattern-list filter.
354
 * "denylist" determines sense of filter:
355
 * non-zero indicates that items matching filter should be excluded.
356
 * zero indicates that only items matching filter should be included.
357
 * returns NULL on allocation error, otherwise caller must free result.
358
 */
359
static char *
360
filter_list(const char *proposal, const char *filter, int denylist)
361
0
{
362
0
  size_t len = strlen(proposal) + 1;
363
0
  char *fix_prop = malloc(len);
364
0
  char *orig_prop = strdup(proposal);
365
0
  char *cp, *tmp;
366
0
  int r;
367
368
0
  if (fix_prop == NULL || orig_prop == NULL) {
369
0
    free(orig_prop);
370
0
    free(fix_prop);
371
0
    return NULL;
372
0
  }
373
374
0
  tmp = orig_prop;
375
0
  *fix_prop = '\0';
376
0
  while ((cp = strsep(&tmp, ",")) != NULL) {
377
0
    r = match_pattern_list(cp, filter, 0);
378
0
    if ((denylist && r != 1) || (!denylist && r == 1)) {
379
0
      if (*fix_prop != '\0')
380
0
        strlcat(fix_prop, ",", len);
381
0
      strlcat(fix_prop, cp, len);
382
0
    }
383
0
  }
384
0
  free(orig_prop);
385
0
  return fix_prop;
386
0
}
387
388
/*
389
 * Filters a comma-separated list of strings, excluding any entry matching
390
 * the 'filter' pattern list. Caller must free returned string.
391
 */
392
char *
393
match_filter_denylist(const char *proposal, const char *filter)
394
0
{
395
0
  return filter_list(proposal, filter, 1);
396
0
}
397
398
/*
399
 * Filters a comma-separated list of strings, including only entries matching
400
 * the 'filter' pattern list. Caller must free returned string.
401
 */
402
char *
403
match_filter_allowlist(const char *proposal, const char *filter)
404
0
{
405
0
  return filter_list(proposal, filter, 0);
406
0
}