Files
libssh/src/match.c
Jakub Jelen b7018c17c7 Fix implicit type conversions and warnings on windows builds
The visual studio windows builds spit dozens of lines of warnings
on these.

Signed-off-by: Jakub Jelen <jjelen@redhat.com>
Reviewed-by: Andreas Schneider <asn@cryptomilk.org>
2025-01-03 15:18:53 +01:00

631 lines
19 KiB
C

/*
* Author: Tatu Ylonen <ylo@cs.hut.fi>
* Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
* All rights reserved
* Simple pattern matching, with '*' and '?' as wildcards.
*
* As far as I am concerned, the code I have written for this software
* can be used freely for any purpose. Any derived versions of this
* software must be clearly marked as such, and if the derived work is
* incompatible with the protocol description in the RFC file, it must be
* called by a name other than "ssh" or "Secure Shell".
*/
/*
* Copyright (c) 2000 Markus Friedl. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include <ctype.h>
#include <stdbool.h>
#include <sys/types.h>
#ifndef _WIN32
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#endif
/* for systems without IPv6 support matching should still work */
#ifndef INET6_ADDRSTRLEN
#define INET6_ADDRSTRLEN 46
#endif
#include "libssh/priv.h"
#define MAX_MATCH_RECURSION 16
/*
* Returns true if the given string matches the pattern (which may contain ?
* and * as wildcards), and zero if it does not match.
*/
static int match_pattern(const char *s, const char *pattern, size_t limit)
{
bool had_asterisk = false;
if (s == NULL || pattern == NULL || limit <= 0) {
return 0;
}
for (;;) {
/* If at end of pattern, accept if also at end of string. */
if (*pattern == '\0') {
return (*s == '\0');
}
/* Skip all the asterisks and adjacent question marks */
while (*pattern == '*' || (had_asterisk && *pattern == '?')) {
if (*pattern == '*') {
had_asterisk = true;
}
pattern++;
}
if (had_asterisk) {
/* If at end of pattern, accept immediately. */
if (!*pattern)
return 1;
/* If next character in pattern is known, optimize. */
if (*pattern != '?') {
/*
* Look instances of the next character in
* pattern, and try to match starting from
* those.
*/
for (; *s; s++)
if (*s == *pattern && match_pattern(s + 1, pattern + 1, limit - 1)) {
return 1;
}
/* Failed. */
return 0;
}
/*
* Move ahead one character at a time and try to
* match at each position.
*/
for (; *s; s++) {
if (match_pattern(s, pattern, limit - 1)) {
return 1;
}
}
/* Failed. */
return 0;
}
/*
* There must be at least one more character in the string.
* If we are at the end, fail.
*/
if (!*s) {
return 0;
}
/* Check if the next character of the string is acceptable. */
if (*pattern != '?' && *pattern != *s) {
return 0;
}
/* Move to the next character, both in string and in pattern. */
s++;
pattern++;
}
/* NOTREACHED */
return 0;
}
/*
* Tries to match the string against the comma-separated sequence of subpatterns
* (each possibly preceded by ! to indicate negation).
* Returns -1 if negation matches, 1 if there is a positive match, 0 if there is
* no match at all.
*/
int match_pattern_list(const char *string, const char *pattern,
size_t len, int dolower) {
char sub[1024];
int negated;
int got_positive;
size_t i, subi;
got_positive = 0;
for (i = 0; i < len;) {
/* Check if the subpattern is negated. */
if (pattern[i] == '!') {
negated = 1;
i++;
} else {
negated = 0;
}
/*
* Extract the subpattern up to a comma or end. Convert the
* subpattern to lowercase.
*/
for (subi = 0;
i < len && subi < sizeof(sub) - 1 && pattern[i] != ',';
subi++, i++) {
sub[subi] = dolower && isupper(pattern[i]) ?
(char)tolower(pattern[i]) : pattern[i];
}
/* If subpattern too long, return failure (no match). */
if (subi >= sizeof(sub) - 1) {
return 0;
}
/* If the subpattern was terminated by a comma, skip the comma. */
if (i < len && pattern[i] == ',') {
i++;
}
/* Null-terminate the subpattern. */
sub[subi] = '\0';
/* Try to match the subpattern against the string. */
if (match_pattern(string, sub, MAX_MATCH_RECURSION)) {
if (negated) {
return -1; /* Negative */
} else {
got_positive = 1; /* Positive */
}
}
}
/*
* Return success if got a positive match. If there was a negative
* match, we have already returned -1 and never get here.
*/
return got_positive;
}
/*
* Tries to match the host name (which must be in all lowercase) against the
* comma-separated sequence of subpatterns (each possibly preceded by ! to
* indicate negation).
* Returns -1 if negation matches, 1 if there is a positive match, 0 if there
* is no match at all.
*/
int
match_hostname(const char *host, const char *pattern, size_t len)
{
return match_pattern_list(host, pattern, len, 1);
}
#ifndef _WIN32
/**
* @brief Tries to match the host IPv6 address against a given network address
* with specified prefix length in CIDR notation.
*
* @param[in] host_addr The host address to verify.
*
* @param[in] net_addr The network id address against which the match is
* being verified
*
* @param[in] bits The prefix length
*
* @return 0 on a negative match.
* @return 1 on a positive match.
*/
static int
cidr_match_6(struct in6_addr *host_addr,
struct in6_addr *net_addr,
unsigned int bits)
{
const uint8_t *a = host_addr->s6_addr;
const uint8_t *b = net_addr->s6_addr;
unsigned int byte_whole, bits_left;
/* The number of a complete byte covered by the prefix */
byte_whole = bits / 8;
/*
* The number of bits remaining in the incomplete (last) byte
* covered by the prefix
*/
bits_left = bits % 8;
if (byte_whole) {
if (memcmp(a, b, byte_whole) != 0) {
return 0;
}
}
if (bits_left) {
if ((a[byte_whole] ^ b[byte_whole]) & (0xFFu << (8 - bits_left))) {
return 0;
}
}
return 1;
}
/**
* @brief Tries to match the host IPv4 address against a given network address
* with specified prefix length in CIDR notation.
*
* @param[in] host_addr The host address to verify.
*
* @param[in] net_addr The network id address against which the match is
* being verified
*
* @param[in] bits The prefix length
*
* @return 0 on a negative match.
* @return 1 on a positive match.
*/
static int
cidr_match_4(struct in_addr *host_addr,
struct in_addr *net_addr,
unsigned int bits)
{
if (bits == 0) {
/* C99 6.5.7 (3): u32 << 32 is undefined behaviour */
return 1;
}
return !((host_addr->s_addr ^ net_addr->s_addr) &
htonl((0xFFFFFFFFu << (32 - bits)) & 0xFFFFFFFFu));
}
/**
* @brief Checks if the mask length is valid according to the address family
* (IPv4 or IPv6).
*
* @param[in] family The address family (e.g. AF_INET or AF_INET6)
*
* @param[in] mask The subnet mask (prefix)
*
* @return true if the mask length does not exceed the maximum valid length
* according to the address family (IPv4 or IPv6).
* @return false if the mask length exceeds the maximum valid length
* or there is no match with IPv4 or IPv6 address family.
*/
static bool
masklen_valid(int family, unsigned int mask)
{
switch (family) {
case AF_INET:
return mask <= 32;
case AF_INET6:
return mask <= 128;
default:
return false;
}
}
/**
* @brief Extracts address family given a network address.
*
* @param[in] address The network address.
*
* @return The value of the address family if no errors.
* @return -1 in case of errors.
*/
static int
get_address_family(const char *address)
{
struct addrinfo hints, *ai = NULL;
int rc = -1, rv;
ZERO_STRUCT(hints);
if (address == NULL) {
SSH_LOG(SSH_LOG_TRACE, "Bad arguments");
goto out;
}
hints.ai_flags = AI_NUMERICHOST;
rv = getaddrinfo(address, NULL, &hints, &ai);
if (rv != 0) {
SSH_LOG(SSH_LOG_TRACE,
"Couldn't get address information - getaddrinfo() failed: %s",
gai_strerror(rv));
goto out;
}
rc = ai->ai_family;
freeaddrinfo(ai);
out:
return rc;
}
/**
* @brief Tries to match the host address against a CIDR list provided
* by the user. If the host address family is unknown, it can be derived by
* passing -1 as sa_family argument.
*
* It can be also used to validate a CIDR list when the passed address is NULL
* and sa_family is -1.
*
* @param[in] address The host address to verify (NULL to validate CIDR list).
*
* @param[in] addrlist The CIDR list against which the match is being verified.
* The CIDR list can contain both IPv4 and IPv6 addresses
* and has to be comma separated
* (',' only, space after comma not allowed).
*
* @param[in] sa_family The socket address family (e.g. AF_INET or AF_INET6,
* -1 to validate CIDR list or unknown address family).
*
* @usage To validate CIDR list: match_cidr_address_list(NULL, addrlist, -1).
* @usage To verify a match with unknown address family:
* match_cidr_address_list(address, addrlist, -1).
* @return 1 only on positive match.
* @return 0 on negative match or valid CIDR list.
* @return -1 on errors or invalid CIDR list.
*/
int
match_cidr_address_list(const char *address,
const char *addrlist,
int sa_family)
{
char *list = NULL, *cp = NULL, *a = NULL, *b = NULL, *sp = NULL;
char addr_buffer[64], addr[NI_MAXHOST];
struct in_addr try_addr, match_addr;
struct in6_addr try_addr6, match_addr6;
unsigned long mask_len;
size_t addr_len, tmp_len;
int rc = 0, r, ai_family;
ZERO_STRUCT(try_addr);
ZERO_STRUCT(try_addr6);
ZERO_STRUCT(match_addr);
ZERO_STRUCT(match_addr6);
if (sa_family != AF_INET && sa_family != AF_INET6 && sa_family != -1) {
SSH_LOG(SSH_LOG_TRACE,
"Invalid argument: sa_family %d is not valid",
sa_family);
return -1;
}
if (address != NULL) {
strncpy(addr, address, NI_MAXHOST - 1);
/* Remove interface in case of IPv6 address: addr%interface */
a = strchr(addr, '%');
if (a != NULL) {
*a = '\0';
}
/*
* If sa_family is set to -1 and address is not NULL then
* the socket address family should be derived
*/
if (sa_family == -1) {
r = get_address_family(addr);
if (r == -1) {
SSH_LOG(SSH_LOG_TRACE,
"Failed to derive address family for address "
"\"%.100s\"",
addr);
return -1;
}
sa_family = r;
}
/*
* Translate host address from dot notation to binary network format
* according to family type,
* i.e. IPv4 (store in in_addr) or IPv6 (store in in6_addr)
*/
if (sa_family == AF_INET) {
if (inet_pton(AF_INET, addr, &try_addr) == 0) {
SSH_LOG(SSH_LOG_TRACE,
"Couldn't parse IPv4 address \"%.100s\"",
addr);
return -1;
}
} else if (sa_family == AF_INET6) {
if (inet_pton(AF_INET6, addr, &try_addr6) == 0) {
SSH_LOG(SSH_LOG_TRACE,
"Couldn't parse IPv6 address \"%.100s\"",
addr);
return -1;
}
} else {
SSH_LOG(SSH_LOG_TRACE,
"Address family %d for address \"%.100s\" "
"is not recognized",
sa_family,
addr);
return -1;
}
}
b = list = strdup(addrlist);
if (b == NULL) {
return -1;
}
while ((cp = strsep(&list, ",")) != NULL) {
if (*cp == '\0') {
SSH_LOG(SSH_LOG_TRACE, "Empty entry in list \"%.100s\"", b);
rc = -1;
break;
}
/*
* Stop junk from reaching address translation. +3 for the "/prefix".
* INET6_ADDRSTRLEN is 46 and includes space for '\0' terminator. The
* maximum IPv6 address printable is the one that carries IPv4 too.
* E.g. ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255 is 46 chars
* long ('\0' included) and the maximum prefix length possible is 96.
* This explains why +3. All the other IPv6 addresses with maximum /127
* prefix length (39 + 4) are covered just by INET6_ADDRSTRLEN itself
*/
addr_len = strlen(cp);
if (addr_len > INET6_ADDRSTRLEN + 3) {
SSH_LOG(SSH_LOG_TRACE,
"List entry \"%.100s\" too long: %zu > %d (MAX ALLOWED)",
cp,
addr_len,
INET6_ADDRSTRLEN + 3);
rc = -1;
break;
}
#define VALID_CIDR_CHARS "0123456789abcdefABCDEF.:/"
tmp_len = strspn(cp, VALID_CIDR_CHARS);
if (tmp_len != addr_len) {
SSH_LOG(SSH_LOG_TRACE,
"List entry \"%.100s\" contains invalid characters "
"-> \"%c\" is an invalid character",
cp,
cp[tmp_len]);
rc = -1;
break;
}
#undef VALID_CIDR_CHARS
strncpy(addr_buffer, cp, sizeof(addr_buffer) - 1);
sp = strchr(addr_buffer, '/');
if (sp != NULL) {
*sp = '\0';
sp++;
mask_len = strtoul(sp, &cp, 10);
if (*sp < '0' || *sp > '9' || *cp != '\0') {
SSH_LOG(SSH_LOG_TRACE, "Error while parsing prefix: %s", sp);
rc = -1;
break;
}
if (mask_len > 128) {
SSH_LOG(SSH_LOG_TRACE,
"Invalid prefix: %lu exceeds the maximum allowed "
"(>128)",
mask_len);
rc = -1;
break;
}
} else {
SSH_LOG(SSH_LOG_TRACE,
"Missing prefix length for list entry \"%.100s\"",
addr_buffer);
rc = -1;
break;
}
ai_family = get_address_family(addr_buffer);
if (ai_family == -1) {
SSH_LOG(SSH_LOG_TRACE,
"Couldn't get address family for \"%.100s\"",
addr_buffer);
rc = -1;
break;
}
if (ai_family == AF_INET) {
if (inet_pton(AF_INET, addr_buffer, &match_addr) == 0) {
SSH_LOG(SSH_LOG_TRACE,
"Couldn't parse IPv4 address \"%.100s\"",
addr_buffer);
rc = -1;
break;
}
} else if (ai_family == AF_INET6) {
if (inet_pton(AF_INET6, addr_buffer, &match_addr6) == 0) {
SSH_LOG(SSH_LOG_TRACE,
"Couldn't parse IPv6 address \"%.100s\"",
addr_buffer);
rc = -1;
break;
}
} else {
SSH_LOG(SSH_LOG_TRACE,
"Address family %d for address \"%.100s\" "
"is not recognized",
ai_family,
addr_buffer);
rc = -1;
break;
}
if (masklen_valid(ai_family, mask_len) != true) {
SSH_LOG(SSH_LOG_TRACE,
"Invalid mask length %lu for list entry \"%.100s\"",
mask_len,
addr_buffer);
rc = -1;
break;
}
/* Verify match between host address and network address*/
if (((ai_family == AF_INET && sa_family == AF_INET) &&
cidr_match_4(&try_addr, &match_addr, mask_len)) ||
((ai_family == AF_INET6 && sa_family == AF_INET6) &&
cidr_match_6(&try_addr6, &match_addr6, mask_len))) {
rc = 1;
break;
}
}
SAFE_FREE(b);
return rc;
}
#endif /* _WIN32 */
/**
* @brief Tries to match an object against a comma separated group of objects
*
* The characters '*' and '?' are NOT considered wildcards and an object in the
* group preceded by a ! does NOT indicate negation. The characters '*', '?'
* and '!' are treated normally like other characters, only ',' (comma) is
* treated specially and is considered as a delimiter that separates objects in
* the group.
*
* @param[in] group Group of objects (comma separated) to match against.
*
* @param[in] object Object to match.
*
* @returns 1 if there is a match, 0 if there is no match at all.
*/
int match_group(const char *group, const char *object)
{
const char *a = NULL;
const char *z = NULL;
if (group == NULL || object == NULL) {
return 0;
}
z = group;
do {
a = strchr(z, ',');
if (a == NULL) {
if (strcmp(z, object) == 0) {
return 1;
}
return 0;
} else {
if (strncmp(z, object, a - z) == 0) {
return 1;
}
}
z = a + 1;
} while (1);
/* not reached */
return 0;
}