mirror of
https://git.libssh.org/projects/libssh.git
synced 2026-02-04 20:30:38 +09:00
This commit also initializes the pointers in match_group() to NULL in order to follow libssh coding guidelines. Signed-off-by: Eshan Kelkar <eshankelkar@galorithm.com> Reviewed-by: Jakub Jelen <jjelen@redhat.com> Reviewed-by: Sahana Prasad <sahana@redhat.com>
624 lines
19 KiB
C
624 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
|
|
|
|
#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, unsigned int 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
|
|
|
|
/**
|
|
* @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;
|
|
}
|