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
epsilon_closure(char * states,const char * pattern,size_t M)61 epsilon_closure(char *states, const char *pattern, size_t M)
62 {
63 size_t i;
64
65 /* only need a forward pass as there are no back jumps in our grammar */
66 for (i = 0; i < M; i++) {
67 if (!states[i] || pattern[i] != '*')
68 continue;
69 /*
70 * State i is active, and pattern[i] is '*', so we can
71 * epsilon-transition to i+1.
72 */
73 states[i + 1] = 1;
74 }
75 }
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
match_pattern(const char * s,const char * pattern)82 match_pattern(const char *s, const char *pattern)
83 {
84 size_t M;
85 size_t i;
86 char *states, *next_states, *tmp;
87 int active, matched = 0;
88
89 /* trivial case: empty pattern vs empty input */
90 if ((M = strlen(pattern)) == 0)
91 return *s == '\0';
92
93 /* A state for each pattern character, plus one final accepting state */
94 states = xcalloc(M + 1, sizeof(*states));
95 next_states = xcalloc(M + 1, sizeof(*next_states));
96
97 /* Initial state: state 0 is active */
98 states[0] = 1;
99 /* Other states might be reachable now if the pattern starts with '*' */
100 epsilon_closure(states, pattern, M);
101
102 for (; *s; s++) {
103 memset(next_states, 0, M + 1);
104
105 /* Calculate the reachable next states given the input char */
106 for (i = 0; i < M; i++) {
107 if (!states[i])
108 continue;
109 if (pattern[i] == '*') {
110 /*
111 * '*' matches any character, so we can
112 * stay in state i
113 */
114 next_states[i] = 1;
115 } else if (pattern[i] == '?' || pattern[i] == *s) {
116 /*
117 * '?' matches any character, or we have
118 * a literal match.
119 */
120 next_states[i + 1] = 1;
121 }
122 }
123
124 /* Expand the reachable next states with epsilon transitions */
125 epsilon_closure(next_states, pattern, M);
126
127 /* Swap states and next_states */
128 tmp = states;
129 states = next_states;
130 next_states = tmp;
131
132 /* Check if we have any active pattern states left */
133 active = 0;
134 for (i = 0; i <= M; i++) {
135 if (states[i]) {
136 active = 1;
137 break;
138 }
139 }
140 if (!active)
141 goto out; /* No active states, fail early */
142 }
143 /*
144 * We matched only if we ended up in the final, accepting state
145 * after consuming all the input.
146 */
147 matched = states[M];
148 out:
149 free(states);
150 free(next_states);
151 return matched;
152 }
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
match_pattern_list(const char * string,const char * pattern,int dolower)161 match_pattern_list(const char *string, const char *pattern, int dolower)
162 {
163 char sub[1024];
164 int negated;
165 int got_positive;
166 u_int i, subi, len = strlen(pattern);
167
168 got_positive = 0;
169 for (i = 0; i < len;) {
170 /* Check if the subpattern is negated. */
171 if (pattern[i] == '!') {
172 negated = 1;
173 i++;
174 } else
175 negated = 0;
176
177 /*
178 * Extract the subpattern up to a comma or end. Convert the
179 * subpattern to lowercase.
180 */
181 for (subi = 0;
182 i < len && subi < sizeof(sub) - 1 && pattern[i] != ',';
183 subi++, i++)
184 sub[subi] = dolower && isupper((u_char)pattern[i]) ?
185 tolower((u_char)pattern[i]) : pattern[i];
186 /* If subpattern too long, return failure (no match). */
187 if (subi >= sizeof(sub) - 1)
188 return 0;
189
190 /* If the subpattern was terminated by a comma, then skip it. */
191 if (i < len && pattern[i] == ',')
192 i++;
193
194 /* Null-terminate the subpattern. */
195 sub[subi] = '\0';
196
197 /* Try to match the subpattern against the string. */
198 if (match_pattern(string, sub)) {
199 if (negated)
200 return -1; /* Negative */
201 else
202 got_positive = 1; /* Positive */
203 }
204 }
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 return got_positive;
211 }
212
213 /* Match a list representing users or groups. */
214 int
match_usergroup_pattern_list(const char * string,const char * pattern)215 match_usergroup_pattern_list(const char *string, const char *pattern)
216 {
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 return match_pattern_list(string, pattern, 0);
223 #endif
224 }
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
match_hostname(const char * host,const char * pattern)233 match_hostname(const char *host, const char *pattern)
234 {
235 char *hostcopy = xstrdup(host);
236 int r;
237
238 lowercase(hostcopy);
239 r = match_pattern_list(hostcopy, pattern, 1);
240 free(hostcopy);
241 return r;
242 }
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
match_host_and_ip(const char * host,const char * ipaddr,const char * patterns)250 match_host_and_ip(const char *host, const char *ipaddr,
251 const char *patterns)
252 {
253 int mhost, mip;
254
255 if ((mip = addr_match_list(ipaddr, patterns)) == -2)
256 return -1; /* error in ipaddr match */
257 else if (host == NULL || ipaddr == NULL || mip == -1)
258 return 0; /* negative ip address match, or testing pattern */
259
260 /* negative hostname match */
261 if ((mhost = match_hostname(host, patterns)) == -1)
262 return 0;
263 /* no match at all */
264 if (mhost == 0 && mip == 0)
265 return 0;
266 return 1;
267 }
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
match_user(const char * user,const char * host,const char * ipaddr,const char * pattern)275 match_user(const char *user, const char *host, const char *ipaddr,
276 const char *pattern)
277 {
278 char *p, *pat;
279 int ret;
280
281 /* test mode */
282 if (user == NULL && host == NULL && ipaddr == NULL) {
283 if ((p = strrchr(pattern, '@')) != NULL &&
284 match_host_and_ip(NULL, NULL, p + 1) < 0)
285 return -1;
286 return 0;
287 }
288
289 if (user == NULL)
290 return 0; /* shouldn't happen */
291
292 if (strrchr(pattern, '@') == NULL)
293 return match_pattern(user, pattern);
294
295 pat = xstrdup(pattern);
296 p = strrchr(pat, '@');
297 *p++ = '\0';
298
299 if ((ret = match_pattern(user, pat)) == 1)
300 ret = match_host_and_ip(host, ipaddr, p);
301 free(pat);
302
303 return ret;
304 }
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 #define MAX_PROP 40
311 #define SEP ","
312 char *
match_list(const char * client,const char * server,u_int * next)313 match_list(const char *client, const char *server, u_int *next)
314 {
315 char *sproposals[MAX_PROP];
316 char *c, *s, *p, *ret, *cp, *sp;
317 int i, j, nproposals;
318
319 c = cp = xstrdup(client);
320 s = sp = xstrdup(server);
321
322 for ((p = strsep(&sp, SEP)), i=0; p && *p != '\0';
323 (p = strsep(&sp, SEP)), i++) {
324 if (i < MAX_PROP)
325 sproposals[i] = p;
326 else
327 break;
328 }
329 nproposals = i;
330
331 for ((p = strsep(&cp, SEP)), i=0; p && *p != '\0';
332 (p = strsep(&cp, SEP)), i++) {
333 for (j = 0; j < nproposals; j++) {
334 if (strcmp(p, sproposals[j]) == 0) {
335 ret = xstrdup(p);
336 if (next != NULL)
337 *next = (cp == NULL) ?
338 strlen(c) : (u_int)(cp - c);
339 free(c);
340 free(s);
341 return ret;
342 }
343 }
344 }
345 if (next != NULL)
346 *next = strlen(c);
347 free(c);
348 free(s);
349 return NULL;
350 }
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 *
filter_list(const char * proposal,const char * filter,int denylist)360 filter_list(const char *proposal, const char *filter, int denylist)
361 {
362 size_t len = strlen(proposal) + 1;
363 char *fix_prop = malloc(len);
364 char *orig_prop = strdup(proposal);
365 char *cp, *tmp;
366 int r;
367
368 if (fix_prop == NULL || orig_prop == NULL) {
369 free(orig_prop);
370 free(fix_prop);
371 return NULL;
372 }
373
374 tmp = orig_prop;
375 *fix_prop = '\0';
376 while ((cp = strsep(&tmp, ",")) != NULL) {
377 r = match_pattern_list(cp, filter, 0);
378 if ((denylist && r != 1) || (!denylist && r == 1)) {
379 if (*fix_prop != '\0')
380 strlcat(fix_prop, ",", len);
381 strlcat(fix_prop, cp, len);
382 }
383 }
384 free(orig_prop);
385 return fix_prop;
386 }
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 *
match_filter_denylist(const char * proposal,const char * filter)393 match_filter_denylist(const char *proposal, const char *filter)
394 {
395 return filter_list(proposal, filter, 1);
396 }
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 *
match_filter_allowlist(const char * proposal,const char * filter)403 match_filter_allowlist(const char *proposal, const char *filter)
404 {
405 return filter_list(proposal, filter, 0);
406 }
407