xref: /freebsd/crypto/openssh/match.c (revision bb5c77e9d281d6def6835d48249898764bc6a5fe)
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