diff options
author | Florian Weimer <fweimer@redhat.com> | 2014-05-12 15:24:12 +0200 |
---|---|---|
committer | Allan McRae <allan@archlinux.org> | 2014-09-05 22:44:11 +1000 |
commit | ac39af9f195138a01b836fb4a30bd971de4aa163 (patch) | |
tree | f6895009f9662672c232aa9857364a2532af07b2 | |
parent | 2da15d05c54738ed2c53aaf555c7cf51a9057844 (diff) | |
download | glibc-ac39af9f195138a01b836fb4a30bd971de4aa163.tar.gz glibc-ac39af9f195138a01b836fb4a30bd971de4aa163.tar.xz glibc-ac39af9f195138a01b836fb4a30bd971de4aa163.zip |
_nl_find_locale: Improve handling of crafted locale names [BZ #17137]
Prevent directory traversal in locale-related environment variables (CVE-2014-0475). (cherry picked from commit 4e8f95a0df7c2300b830ec12c0ae1e161bc8a8a3) Addiational backporting fixes: Added tst-setlocale3-ENV to localedata/Makefile Conflicts: NEWS localedata/Makefile
-rw-r--r-- | ChangeLog | 9 | ||||
-rw-r--r-- | NEWS | 11 | ||||
-rw-r--r-- | locale/findlocale.c | 74 | ||||
-rw-r--r-- | localedata/ChangeLog | 6 | ||||
-rw-r--r-- | localedata/Makefile | 5 | ||||
-rw-r--r-- | localedata/tst-setlocale3.c | 203 |
6 files changed, 292 insertions, 16 deletions
diff --git a/ChangeLog b/ChangeLog index 38efe3d650..7733db3375 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,14 @@ 2014-07-02 Florian Weimer <fweimer@redhat.com> + [BZ #17137] + * locale/findlocale.c (name_present, valid_locale_name): New + functions. + (_nl_find_locale): Use the loc_name variable to store name + candidates. Call name_present and valid_locale_name to check and + validate locale names. Return an error if the locale is invalid. + +2014-07-02 Florian Weimer <fweimer@redhat.com> + * locale/setlocale.c (setlocale): Use strdup for allocating composite name copy. diff --git a/NEWS b/NEWS index e84bae538f..71b6ad5c88 100644 --- a/NEWS +++ b/NEWS @@ -10,13 +10,22 @@ Version 2.19.1 * The following bugs are resolved with this release: 15946, 16545, 16574, 16623, 16695, 16878, 16882, 16885, 16916, 16932, - 16943, 16958, 17048, 17069. + 16943, 16958, 17048, 17069, 17137. * CVE-2014-4043 The posix_spawn_file_actions_addopen implementation did not copy the path argument. This allowed programs to cause posix_spawn to deference a dangling pointer, or use an unexpected pathname argument if the string was modified after the posix_spawn_file_actions_addopen invocation. + +* Locale names, including those obtained from environment variables (LANG + and the LC_* variables), are more tightly checked for proper syntax. + setlocale will now fail (with EINVAL) for locale names that are overly + long, contain slashes without starting with a slash, or contain ".." path + components. (CVE-2014-0475) Previously, some valid locale names were + silently replaced with the "C" locale when running in AT_SECURE mode + (e.g., in a SUID program). This is no longer necessary because of the + additional checks. Version 2.19 diff --git a/locale/findlocale.c b/locale/findlocale.c index 0c42b99251..faeee61bcb 100644 --- a/locale/findlocale.c +++ b/locale/findlocale.c @@ -17,6 +17,7 @@ <http://www.gnu.org/licenses/>. */ #include <assert.h> +#include <errno.h> #include <locale.h> #include <stdlib.h> #include <string.h> @@ -57,6 +58,45 @@ struct loaded_l10nfile *_nl_locale_file_list[__LC_LAST]; const char _nl_default_locale_path[] attribute_hidden = LOCALEDIR; +/* Checks if the name is actually present, that is, not NULL and not + empty. */ +static inline int +name_present (const char *name) +{ + return name != NULL && name[0] != '\0'; +} + +/* Checks that the locale name neither extremely long, nor contains a + ".." path component (to prevent directory traversal). */ +static inline int +valid_locale_name (const char *name) +{ + /* Not set. */ + size_t namelen = strlen (name); + /* Name too long. The limit is arbitrary and prevents stack overflow + issues later. */ + if (__glibc_unlikely (namelen > 255)) + return 0; + /* Directory traversal attempt. */ + static const char slashdot[4] = {'/', '.', '.', '/'}; + if (__glibc_unlikely (memmem (name, namelen, + slashdot, sizeof (slashdot)) != NULL)) + return 0; + if (namelen == 2 && __glibc_unlikely (name[0] == '.' && name [1] == '.')) + return 0; + if (namelen >= 3 + && __glibc_unlikely (((name[0] == '.' + && name[1] == '.' + && name[2] == '/') + || (name[namelen - 3] == '/' + && name[namelen - 2] == '.' + && name[namelen - 1] == '.')))) + return 0; + /* If there is a slash in the name, it must start with one. */ + if (__glibc_unlikely (memchr (name, '/', namelen) != NULL) && name[0] != '/') + return 0; + return 1; +} struct __locale_data * internal_function @@ -65,7 +105,7 @@ _nl_find_locale (const char *locale_path, size_t locale_path_len, { int mask; /* Name of the locale for this category. */ - char *loc_name; + char *loc_name = (char *) *name; const char *language; const char *modifier; const char *territory; @@ -73,31 +113,39 @@ _nl_find_locale (const char *locale_path, size_t locale_path_len, const char *normalized_codeset; struct loaded_l10nfile *locale_file; - if ((*name)[0] == '\0') + if (loc_name[0] == '\0') { /* The user decides which locale to use by setting environment variables. */ - *name = getenv ("LC_ALL"); - if (*name == NULL || (*name)[0] == '\0') - *name = getenv (_nl_category_names.str + loc_name = getenv ("LC_ALL"); + if (!name_present (loc_name)) + loc_name = getenv (_nl_category_names.str + _nl_category_name_idxs[category]); - if (*name == NULL || (*name)[0] == '\0') - *name = getenv ("LANG"); + if (!name_present (loc_name)) + loc_name = getenv ("LANG"); + if (!name_present (loc_name)) + loc_name = (char *) _nl_C_name; } - if (*name == NULL || (*name)[0] == '\0' - || (__builtin_expect (__libc_enable_secure, 0) - && strchr (*name, '/') != NULL)) - *name = (char *) _nl_C_name; + /* We used to fall back to the C locale if the name contains a slash + character '/', but we now check for directory traversal in + valid_locale_name, so this is no longer necessary. */ - if (__builtin_expect (strcmp (*name, _nl_C_name), 1) == 0 - || __builtin_expect (strcmp (*name, _nl_POSIX_name), 1) == 0) + if (__builtin_expect (strcmp (loc_name, _nl_C_name), 1) == 0 + || __builtin_expect (strcmp (loc_name, _nl_POSIX_name), 1) == 0) { /* We need not load anything. The needed data is contained in the library itself. */ *name = (char *) _nl_C_name; return _nl_C[category]; } + else if (!valid_locale_name (loc_name)) + { + __set_errno (EINVAL); + return NULL; + } + + *name = loc_name; /* We really have to load some data. First we try the archive, but only if there was no LOCPATH environment variable specified. */ diff --git a/localedata/ChangeLog b/localedata/ChangeLog index a5707677b5..ff7ecb6181 100644 --- a/localedata/ChangeLog +++ b/localedata/ChangeLog @@ -1,3 +1,9 @@ +2014-07-02 Florian Weimer <fweimer@redhat.com> + + * tst-setlocale3.c: New file. + * Makefile (tests): Add tst-setlocale3. + (tst-setlocale3-ENV): New variable. + 2013-12-26 Chris Leonard <cjl@sugarlabs.org> * locales/sa_IN: Add lang_name. diff --git a/localedata/Makefile b/localedata/Makefile index 7d157bff42..d179765684 100644 --- a/localedata/Makefile +++ b/localedata/Makefile @@ -77,7 +77,7 @@ locale_test_suite := tst_iswalnum tst_iswalpha tst_iswcntrl \ tests = $(locale_test_suite) tst-digits tst-setlocale bug-iconv-trans \ tst-leaks tst-mbswcs6 tst-xlocale1 tst-xlocale2 bug-usesetlocale \ - tst-strfmon1 tst-sscanf bug-setlocale1 tst-setlocale2 + tst-strfmon1 tst-sscanf bug-setlocale1 tst-setlocale2 tst-setlocale3 tests-static = bug-setlocale1-static tests += $(tests-static) ifeq (yes,$(build-shared)) @@ -276,7 +276,7 @@ tst-xlocale2-ENV = $(TEST_MBWC_ENV) tst-strfmon1-ENV = $(TEST_MBWC_ENV) tst-strptime-ENV = $(TEST_MBWC_ENV) -tst-setlocale-ENV = LOCPATH=$(common-objpfx)localedata LC_ALL=ja_JP.EUC-JP +tst-setlocale-ENV = LOCPATH=$(common-objpfx)localedata LC_ALL=ja_JP.EUC- bug-iconv-trans-ENV = LOCPATH=$(common-objpfx)localedata @@ -292,6 +292,7 @@ bug-setlocale1-ARGS = -- $(host-test-program-cmd) bug-setlocale1-static-ENV = $(bug-setlocale1-ENV) bug-setlocale1-static-ARGS = $(bug-setlocale1-ARGS) tst-setlocale2-ENV = LOCPATH=$(common-objpfx)localedata +tst-setlocale3-ENV = LOCPATH=$(common-objpfx)localedata $(objdir)/iconvdata/gconv-modules: $(MAKE) -C ../iconvdata subdir=iconvdata $@ diff --git a/localedata/tst-setlocale3.c b/localedata/tst-setlocale3.c new file mode 100644 index 0000000000..e3b21a9170 --- /dev/null +++ b/localedata/tst-setlocale3.c @@ -0,0 +1,203 @@ +/* Regression test for setlocale invalid environment variable handling. + Copyright (C) 2014 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + <http://www.gnu.org/licenses/>. */ + +#include <locale.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +/* The result of setlocale may be overwritten by subsequent calls, so + this wrapper makes a copy. */ +static char * +setlocale_copy (int category, const char *locale) +{ + const char *result = setlocale (category, locale); + if (result == NULL) + return NULL; + return strdup (result); +} + +static char *de_locale; + +static void +setlocale_fail (const char *envstring) +{ + setenv ("LC_CTYPE", envstring, 1); + if (setlocale (LC_CTYPE, "") != NULL) + { + printf ("unexpected setlocale success for \"%s\" locale\n", envstring); + exit (1); + } + const char *newloc = setlocale (LC_CTYPE, NULL); + if (strcmp (newloc, de_locale) != 0) + { + printf ("failed setlocale call \"%s\" changed locale to \"%s\"\n", + envstring, newloc); + exit (1); + } +} + +static void +setlocale_success (const char *envstring) +{ + setenv ("LC_CTYPE", envstring, 1); + char *newloc = setlocale_copy (LC_CTYPE, ""); + if (newloc == NULL) + { + printf ("setlocale for \"%s\": %m\n", envstring); + exit (1); + } + if (strcmp (newloc, de_locale) == 0) + { + printf ("setlocale with LC_CTYPE=\"%s\" left locale at \"%s\"\n", + envstring, de_locale); + exit (1); + } + if (setlocale (LC_CTYPE, de_locale) == NULL) + { + printf ("restoring locale \"%s\" with LC_CTYPE=\"%s\": %m\n", + de_locale, envstring); + exit (1); + } + char *newloc2 = setlocale_copy (LC_CTYPE, newloc); + if (newloc2 == NULL) + { + printf ("restoring locale \"%s\" following \"%s\": %m\n", + newloc, envstring); + exit (1); + } + if (strcmp (newloc, newloc2) != 0) + { + printf ("representation of locale \"%s\" changed from \"%s\" to \"%s\"", + envstring, newloc, newloc2); + exit (1); + } + free (newloc); + free (newloc2); + + if (setlocale (LC_CTYPE, de_locale) == NULL) + { + printf ("restoring locale \"%s\" with LC_CTYPE=\"%s\": %m\n", + de_locale, envstring); + exit (1); + } +} + +/* Checks that a known-good locale still works if LC_ALL contains a + value which should be ignored. */ +static void +setlocale_ignore (const char *to_ignore) +{ + const char *fr_locale = "fr_FR.UTF-8"; + setenv ("LC_CTYPE", fr_locale, 1); + char *expected_locale = setlocale_copy (LC_CTYPE, ""); + if (expected_locale == NULL) + { + printf ("setlocale with LC_CTYPE=\"%s\" failed: %m\n", fr_locale); + exit (1); + } + if (setlocale (LC_CTYPE, de_locale) == NULL) + { + printf ("failed to restore locale: %m\n"); + exit (1); + } + unsetenv ("LC_CTYPE"); + + setenv ("LC_ALL", to_ignore, 1); + setenv ("LC_CTYPE", fr_locale, 1); + const char *actual_locale = setlocale (LC_CTYPE, ""); + if (actual_locale == NULL) + { + printf ("setlocale with LC_ALL, LC_CTYPE=\"%s\" failed: %m\n", + fr_locale); + exit (1); + } + if (strcmp (actual_locale, expected_locale) != 0) + { + printf ("setlocale under LC_ALL failed: got \"%s\", expected \"%s\"\n", + actual_locale, expected_locale); + exit (1); + } + unsetenv ("LC_CTYPE"); + setlocale_success (fr_locale); + unsetenv ("LC_ALL"); + free (expected_locale); +} + +static int +do_test (void) +{ + /* The glibc test harness sets this environment variable + uncondionally. */ + unsetenv ("LC_ALL"); + + de_locale = setlocale_copy (LC_CTYPE, "de_DE.UTF-8"); + if (de_locale == NULL) + { + printf ("setlocale (LC_CTYPE, \"de_DE.UTF-8\"): %m\n"); + return 1; + } + setlocale_success ("C"); + setlocale_success ("en_US.UTF-8"); + setlocale_success ("/en_US.UTF-8"); + setlocale_success ("//en_US.UTF-8"); + setlocale_ignore (""); + + setlocale_fail ("does-not-exist"); + setlocale_fail ("/"); + setlocale_fail ("/../localedata/en_US.UTF-8"); + setlocale_fail ("en_US.UTF-8/"); + setlocale_fail ("en_US.UTF-8/.."); + setlocale_fail ("en_US.UTF-8/../en_US.UTF-8"); + setlocale_fail ("../localedata/en_US.UTF-8"); + { + size_t large_length = 1024; + char *large_name = malloc (large_length + 1); + if (large_name == NULL) + { + puts ("malloc failure"); + return 1; + } + memset (large_name, '/', large_length); + const char *suffix = "en_US.UTF-8"; + strcpy (large_name + large_length - strlen (suffix), suffix); + setlocale_fail (large_name); + free (large_name); + } + { + size_t huge_length = 64 * 1024 * 1024; + char *huge_name = malloc (huge_length + 1); + if (huge_name == NULL) + { + puts ("malloc failure"); + return 1; + } + memset (huge_name, 'X', huge_length); + huge_name[huge_length] = '\0'; + /* Construct a composite locale specification. */ + const char *prefix = "LC_CTYPE=de_DE.UTF-8;LC_TIME="; + memcpy (huge_name, prefix, strlen (prefix)); + setlocale_fail (huge_name); + free (huge_name); + } + + return 0; +} + +#define TEST_FUNCTION do_test () +#include "../test-skeleton.c" |