/* * Author: Tatu Ylonen * Copyright (c) 1995 Tatu Ylonen , 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 #include #include #ifndef _WIN32 #include #include #include #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; }