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