From 1c15fa8248fdb72ba16119ea45046383b8fe4f12 Mon Sep 17 00:00:00 2001 From: wrowe Date: Sun, 1 May 2011 12:24:12 +0000 Subject: Security: CVE-2011-0419 Reported by: Maksymilian Arciemowicz Excessive CPU consumption was possible due to the unconstrained, recursive invocation of apr_fnmatch, as apr_fnmatch processed '*' wildcards. Introduce new apr_fnmatch implementation. This delivers optimizations in some common cases, without the underlying weakness of recursion present in older implementations. Submitted by: William Rowe Forward port: r1098289 git-svn-id: http://svn.apache.org/repos/asf/apr/apr/trunk@1098292 13f79535-47bb-0310-9956-ffa450edef68 --- strings/apr_fnmatch.c | 588 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 377 insertions(+), 211 deletions(-) (limited to 'strings') diff --git a/strings/apr_fnmatch.c b/strings/apr_fnmatch.c index aa250ecdc..1de725189 100644 --- a/strings/apr_fnmatch.c +++ b/strings/apr_fnmatch.c @@ -1,50 +1,58 @@ -/* - * Copyright (c) 1989, 1993, 1994 - * The Regents of the University of California. All rights reserved. +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * - * This code is derived from software contributed to Berkeley by - * Guido van Rossum. + * http://www.apache.org/licenses/LICENSE-2.0 * - * 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. - * 3. All advertising materials mentioning features or use of this software - * must display the following acknowledgement: - * This product includes software developed by the University of - * California, Berkeley and its contributors. - * 4. Neither the name of the University nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``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 REGENTS OR CONTRIBUTORS 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. - */ + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -#if defined(LIBC_SCCS) && !defined(lint) -static char sccsid[] = "@(#)fnmatch.c 8.2 (Berkeley) 4/16/94"; -#endif /* LIBC_SCCS and not lint */ -/* - * Function fnmatch() as specified in POSIX 1003.2-1992, section B.6. - * Compares a filename or pathname to a pattern. +/* Derived from The Open Group Base Specifications Issue 7, IEEE Std 1003.1-2008 + * as described in; + * http://pubs.opengroup.org/onlinepubs/9699919799/functions/fnmatch.html + * + * Filename pattern matches defined in section 2.13, "Pattern Matching Notation" + * from chapter 2. "Shell Command Language" + * http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_13 + * where; 1. A bracket expression starting with an unquoted '^' + * character CONTINUES to specify a non-matching list; 2. an explicit '.' + * in a bracket expression matching list, e.g. "[.abc]" does NOT match a leading + * in a filename; 3. a '[' which does not introduce + * a valid bracket expression is treated as an ordinary character; 4. a differing + * number of consecutive slashes within pattern and string will NOT match; + * 5. a trailing '\' in FNM_ESCAPE mode is treated as an ordinary '\' character. + * + * Bracket expansion defined in section 9.3.5, "RE Bracket Expression", + * from chapter 9, "Regular Expressions" + * http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_03_05 + * with no support for collating symbols, equivalence class expressions or + * character class expressions. A partial range expression with a leading + * hyphen following a valid range expression will match only the ordinary + * and the ending character (e.g. "[a-m-z]" will match characters + * 'a' through 'm', a '-', or a 'z'). + * + * NOTE: Only POSIX/C single byte locales are correctly supported at this time. + * Notably, non-POSIX locales with FNM_CASEFOLD produce undefined results, + * particularly in ranges of mixed case (e.g. "[A-z]") or spanning alpha and + * nonalpha characters within a range. + * + * XXX comments below indicate porting required for multi-byte character sets + * and non-POSIX locale collation orders; requires mbr* APIs to track shift + * state of pattern and string (rewinding pattern and string repeatedly). + * + * Certain parts of the code assume 0x00-0x3F are unique with any MBCS (e.g. + * UTF-8, SHIFT-JIS, etc). Any implementation allowing '\' as an alternate + * path delimiter must be aware that 0x5C is NOT unique within SHIFT-JIS. */ -#ifndef WIN32 -#include "apr_private.h" -#endif + #include "apr_file_info.h" #include "apr_fnmatch.h" #include "apr_tables.h" @@ -55,196 +63,354 @@ static char sccsid[] = "@(#)fnmatch.c 8.2 (Berkeley) 4/16/94"; # include #endif -#define EOS '\0' - -static const char *rangematch(const char *, int, int); -APR_DECLARE(apr_status_t) apr_fnmatch(const char *pattern, const char *string, int flags) +/* Most MBCS/collation/case issues handled here. Wildcard '*' is not handled. + * EOS '\0' and the FNM_PATHNAME '/' delimiters are not advanced over, + * however the "\/" sequence is advanced to '/'. + * + * Both pattern and string are **char to support pointer increment of arbitrary + * multibyte characters for the given locale, in a later iteration of this code + */ +static __inline int fnmatch_ch(const char **pattern, const char **string, int flags) { - const char *stringstart; - char c, test; - - for (stringstart = string;;) { - switch (c = *pattern++) { - case EOS: - return (*string == EOS ? APR_SUCCESS : APR_FNM_NOMATCH); - case '?': - if (*string == EOS) { - return (APR_FNM_NOMATCH); - } - if (*string == '/' && (flags & APR_FNM_PATHNAME)) { - return (APR_FNM_NOMATCH); - } - if (*string == '.' && (flags & APR_FNM_PERIOD) && - (string == stringstart || - ((flags & APR_FNM_PATHNAME) && *(string - 1) == '/'))) { - return (APR_FNM_NOMATCH); - } - ++string; - break; - case '*': - c = *pattern; - /* Collapse multiple stars. */ - while (c == '*') { - c = *++pattern; - } - - if (*string == '.' && (flags & APR_FNM_PERIOD) && - (string == stringstart || - ((flags & APR_FNM_PATHNAME) && *(string - 1) == '/'))) { - return (APR_FNM_NOMATCH); - } - - /* Optimize for pattern with * at end or before /. */ - if (c == EOS) { - if (flags & APR_FNM_PATHNAME) { - return (strchr(string, '/') == NULL ? APR_SUCCESS : APR_FNM_NOMATCH); - } - else { - return (APR_SUCCESS); - } - } - else if (c == '/' && flags & APR_FNM_PATHNAME) { - if ((string = strchr(string, '/')) == NULL) { - return (APR_FNM_NOMATCH); - } - break; - } - - /* General case, use recursion. */ - while ((test = *string) != EOS) { - if (!apr_fnmatch(pattern, string, flags & ~APR_FNM_PERIOD)) { - return (APR_SUCCESS); - } - if (test == '/' && flags & APR_FNM_PATHNAME) { - break; - } - ++string; - } - return (APR_FNM_NOMATCH); - case '[': - if (*string == EOS) { - return (APR_FNM_NOMATCH); - } - if (*string == '/' && flags & APR_FNM_PATHNAME) { - return (APR_FNM_NOMATCH); - } - if (*string == '.' && (flags & APR_FNM_PERIOD) && - (string == stringstart || - ((flags & APR_FNM_PATHNAME) && *(string - 1) == '/'))) { - return (APR_FNM_NOMATCH); - } - if ((pattern = rangematch(pattern, *string, flags)) == NULL) { - return (APR_FNM_NOMATCH); - } - ++string; - break; - case '\\': - if (!(flags & APR_FNM_NOESCAPE)) { - if ((c = *pattern++) == EOS) { - c = '\\'; - --pattern; - } - } - /* FALLTHROUGH */ - default: - if (flags & APR_FNM_CASE_BLIND) { - if (apr_tolower(c) != apr_tolower(*string)) { - return (APR_FNM_NOMATCH); - } - } - else if (c != *string) { - return (APR_FNM_NOMATCH); - } - string++; - break; - } - /* NOTREACHED */ + const char * const mismatch = *pattern; + const int nocase = !!(flags & APR_FNM_CASE_BLIND); + const int escape = !(flags & APR_FNM_NOESCAPE); + const int slash = !!(flags & APR_FNM_PATHNAME); + int result = APR_FNM_NOMATCH; + const char *startch; + int negate; + + if (**pattern == '[') + { + ++*pattern; + + /* Handle negation, either leading ! or ^ operators (never both) */ + negate = ((**pattern == '!') || (**pattern == '^')); + if (negate) + ++*pattern; + + while (**pattern) + { + /* ']' is an ordinary character at the start of the range pattern */ + if ((**pattern == ']') && (*pattern > mismatch)) { + ++*pattern; + /* XXX: Fix for MBCS character width */ + ++*string; + return (result ^ negate); + } + + if (escape && (**pattern == '\\')) { + ++*pattern; + + /* Patterns must be terminated with ']', not EOS */ + if (!**pattern) + break; + } + + /* Patterns must be terminated with ']' not '/' */ + if (slash && (**pattern == '/')) + break; + + /* Look at only well-formed range patterns; ']' is allowed only if escaped, + * while '/' is not allowed at all in FNM_PATHNAME mode. + */ + /* XXX: Fix for locale/MBCS character width */ + if (((*pattern)[1] == '-') && (*pattern)[2] + && ((escape && ((*pattern)[2] != '\\')) + ? (((*pattern)[2] != ']') && (!slash || ((*pattern)[2] != '/'))) + : (((*pattern)[3]) && (!slash || ((*pattern)[3] != '/'))))) { + startch = *pattern; + *pattern += (escape && ((*pattern)[2] == '\\')) ? 3 : 2; + + /* XXX: handle locale/MBCS comparison, advance by MBCS char width */ + if ((**string >= *startch) && (**string <= **pattern)) + result = 0; + else if (nocase && (isupper(**string) || isupper(*startch) + || isupper(**pattern)) + && (tolower(**string) >= tolower(*startch)) + && (tolower(**string) <= tolower(**pattern))) + result = 0; + + ++*pattern; + continue; + } + + /* XXX: handle locale/MBCS comparison, advance by MBCS char width */ + if ((**string == **pattern)) + result = 0; + else if (nocase && (isupper(**string) || isupper(**pattern)) + && (tolower(**string) == tolower(**pattern))) + result = 0; + + ++*pattern; + } + + /* NOT a properly balanced [expr] pattern; Rewind to test '[' literal */ + *pattern = mismatch; + result = APR_FNM_NOMATCH; + } + else if (**pattern == '?') { + /* Optimize '?' match before unescaping **pattern */ + if (!**string || (!slash || (**string != '/'))) + return APR_FNM_NOMATCH; + result = 0; + goto fnmatch_ch_success; } + else if (escape && (**pattern == '\\') && (*pattern)[1]) { + ++*pattern; + } + + /* XXX: handle locale/MBCS comparison, advance by the MBCS char width */ + if (**string == **pattern) + result = 0; + else if (nocase && (isupper(**string) || isupper(**pattern)) + && (tolower(**string) == tolower(**pattern))) + result = 0; + + /* Refuse to advance over trailing slash or nulls + */ + if (!**string || !**pattern || (slash && ((**string == '/') || (**pattern == '/')))) + return result; + +fnmatch_ch_success: + ++*pattern; + ++*string; + return result; } -static const char *rangematch(const char *pattern, int test, int flags) + +APR_DECLARE(int) apr_fnmatch(const char *pattern, const char *string, int flags) { - int negate, ok; - char c, c2; - - /* - * A bracket expression starting with an unquoted circumflex - * character produces unspecified results (IEEE 1003.2-1992, - * 3.13.2). This implementation treats it like '!', for - * consistency with the regular expression syntax. - * J.T. Conklin (conklin@ngai.kaleida.com) + static const char dummystring[2] = {' ', 0}; + const int escape = !(flags & APR_FNM_NOESCAPE); + const int slash = !!(flags & APR_FNM_PATHNAME); + const char *strendseg; + const char *dummyptr; + const char *matchptr; + int wild; + /* For '*' wild processing only; surpress 'used before initialization' + * warnings with dummy initialization values; */ - if ((negate = (*pattern == '!' || *pattern == '^'))) { - ++pattern; - } + const char *strstartseg = NULL; + const char *mismatch = NULL; + int matchlen = 0; + + while (*pattern) + { + /* Match balanced slashes, starting a new segment pattern + */ + if (slash && escape && (*pattern == '\\') && (pattern[1] == '/')) + ++pattern; + if (slash && (*pattern == '/') && (*string == '/')) { + ++pattern; + ++string; + } + + /* At the beginning of each segment, validate leading period behavior. + */ + if ((flags & APR_FNM_PERIOD) && (*string == '.')) + { + if (*pattern == '.') + ++pattern; + else if (escape && (*pattern == '\\') && (pattern[1] == '.')) + pattern += 2; + else + return APR_FNM_NOMATCH; + ++string; + } + + /* Determine the end of string segment + * + * Presumes '/' character is unique, not composite in any MBCS encoding + */ + if (slash) { + if (!(strendseg = strchr(string, '/'))) + strendseg = strchr(string, '\0'); + } + else { + strendseg = strchr(string, '\0'); + } + + /* Allow pattern '*' to be consumed even with no remaining string to match + */ + while (*pattern && !(slash && ((*pattern == '/') + || (escape && (*pattern == '\\') + && (pattern[1] == '/')))) + && ((string < strendseg) + || ((*pattern == '*') && (string == strendseg)))) + { + /* Reduce groups of '*' and '?' to n '?' matches + * followed by one '*' test for simplicity + */ + for (wild = 0; ((*pattern == '*') || (*pattern == '?')); ++pattern) + { + if (*pattern == '*') { + wild = 1; + } + else if (string < strendseg) { /* && (*pattern == '?') */ + /* XXX: Advance 1 char for MBCS locale */ + ++string; + } + else { /* (string >= strendseg) && (*pattern == '?') */ + return APR_FNM_NOMATCH; + } + } + + if (wild) + { + strstartseg = string; + mismatch = pattern; + + /* Count fixed (non '*') char matches remaining in pattern + * excluding '/' (or "\/") and '*' + */ + for (matchptr = pattern, matchlen = 0; 1; ++matchlen) + { + if ((*matchptr == '\0') + || (slash && ((*matchptr == '/') + || (escape && (*matchptr == '\\') + && (matchptr[1] == '/'))))) + { + /* Compare precisely this many trailing string chars, + * the resulting match needs no wildcard loop + */ + /* XXX: Adjust for MBCS */ + if (string + matchlen > strendseg) + return APR_FNM_NOMATCH; + + string = strendseg - matchlen; + wild = 0; + break; + } + + if (*matchptr == '*') + { + /* Ensure at least this many trailing string chars remain + * for the first comparison + */ + /* XXX: Adjust for MBCS */ + if (string + matchlen > strendseg) + return APR_FNM_NOMATCH; + + /* Begin first wild comparison at the current position */ + break; + } + + /* Skip forward in pattern by a single character match + * Use a dummy fnmatch_ch() test to count one "[range]" escape + */ + /* XXX: Adjust for MBCS */ + if (escape && (*matchptr == '\\') && matchptr[1]) { + matchptr += 2; + } + else if (*matchptr == '[') { + dummyptr = dummystring; + fnmatch_ch(&matchptr, &dummyptr, flags); + } + else { + ++matchptr; + } + } + } + + /* Incrementally match string against the pattern + */ + while (*pattern && (string < strendseg)) + { + /* Success; begin a new wild pattern search + */ + if (*pattern == '*') + break; + + if (slash && ((*string == '/') || (*pattern == '/') + || (escape && (*pattern == '\\') + && (pattern[1] == '/')))) + break; - for (ok = 0; (c = *pattern++) != ']';) { - if (c == '\\' && !(flags & APR_FNM_NOESCAPE)) { - c = *pattern++; - } - if (c == EOS) { - return (NULL); - } - if (*pattern == '-' && (c2 = *(pattern + 1)) != EOS && c2 != ']') { - pattern += 2; - if (c2 == '\\' && !(flags & APR_FNM_NOESCAPE)) { - c2 = *pattern++; - } - if (c2 == EOS) { - return (NULL); - } - if ((c <= test && test <= c2) - || ((flags & APR_FNM_CASE_BLIND) - && ((apr_tolower(c) <= apr_tolower(test)) - && (apr_tolower(test) <= apr_tolower(c2))))) { - ok = 1; - } - } - else if ((c == test) - || ((flags & APR_FNM_CASE_BLIND) - && (apr_tolower(c) == apr_tolower(test)))) { - ok = 1; - } + /* Compare ch's (the pattern is advanced over "\/" to the '/', + * but slashes will mismatch, and are not consumed) + */ + if (!fnmatch_ch(&pattern, &string, flags)) + continue; + + /* Failed to match, loop against next char offset of string segment + * until not enough string chars remain to match the fixed pattern + */ + if (wild) { + /* XXX: Advance 1 char for MBCS locale */ + string = ++strstartseg; + if (string + matchlen > strendseg) + return APR_FNM_NOMATCH; + + pattern = mismatch; + continue; + } + else + return APR_FNM_NOMATCH; + } + } + + if (*string && (!slash || (*string != '/'))) + return APR_FNM_NOMATCH; + + if (*pattern && (!slash || ((*pattern != '/') + && (!escape || (*pattern != '\\') + || (pattern[1] != '/'))))) + return APR_FNM_NOMATCH; } - return (ok == negate ? NULL : pattern); + + /* pattern is at EOS; if string is also, declare success + */ + if (!*string) + return 0; + + /* pattern didn't match to the end of string */ + return APR_FNM_NOMATCH; } -/* This function is an Apache addition */ -/* return non-zero if pattern has any glob chars in it */ +/* This function is an Apache addition + * return non-zero if pattern has any glob chars in it + * @bug Function does not distinguish for FNM_PATHNAME mode, which renders + * a false positive for test[/]this (which is not a range, but + * seperate test[ and ]this segments and no glob.) + * @bug Function does not distinguish for non-FNM_ESCAPE mode. + * @bug Function does not parse []] correctly + * Solution may be to use fnmatch_ch() to walk the patterns? + */ APR_DECLARE(int) apr_fnmatch_test(const char *pattern) { int nesting; nesting = 0; while (*pattern) { - switch (*pattern) { - case '?': - case '*': - return 1; - - case '\\': - if (*++pattern == '\0') { - return 0; - } - break; - - case '[': /* '[' is only a glob if it has a matching ']' */ - ++nesting; - break; - - case ']': - if (nesting) { - return 1; - } - break; - } - ++pattern; - } + switch (*pattern) { + case '?': + case '*': + return 1; + + case '\\': + if (*++pattern == '\0') { + return 0; + } + break; + + case '[': /* '[' is only a glob if it has a matching ']' */ + ++nesting; + break; + + case ']': + if (nesting) { + return 1; + } + break; + } + ++pattern; } return 0; } + /* Find all files matching the specified pattern */ APR_DECLARE(apr_status_t) apr_match_glob(const char *pattern, apr_array_header_t **result, -- cgit v1.2.1