aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Weimer <fweimer@redhat.com>2014-05-12 15:24:12 +0200
committerAdhemerval Zanella <azanella@linux.vnet.ibm.com>2015-01-15 15:12:03 -0500
commitc7a91d241b095855e06e0bd00287968df2f6d87e (patch)
tree8d081e736e9ae164f9b1ec3a8df1d77967bd2657
parent588b214bc7fa3e54d6b679ed4b755e6d1310e61d (diff)
downloadglibc-c7a91d241b095855e06e0bd00287968df2f6d87e.tar
glibc-c7a91d241b095855e06e0bd00287968df2f6d87e.tar.gz
glibc-c7a91d241b095855e06e0bd00287968df2f6d87e.tar.bz2
glibc-c7a91d241b095855e06e0bd00287968df2f6d87e.zip
_nl_find_locale: Improve handling of crafted locale names [BZ #17137]
Prevent directory traversal in locale-related environment variables (CVE-2014-0475).
-rw-r--r--ChangeLog9
-rw-r--r--NEWS11
-rw-r--r--locale/findlocale.c74
-rw-r--r--localedata/ChangeLog6
-rw-r--r--localedata/Makefile2
-rw-r--r--localedata/tst-setlocale3.c203
6 files changed, 290 insertions, 15 deletions
diff --git a/ChangeLog b/ChangeLog
index a1cc122ce9..3f3665074a 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,12 @@
+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-08-26 Florian Weimer <fweimer@redhat.com>
[BZ #17187]
diff --git a/NEWS b/NEWS
index 3665933311..fb66b4da23 100644
--- a/NEWS
+++ b/NEWS
@@ -10,7 +10,16 @@ Version 2.16.1
* The following bugs are resolved with this release:
6530, 14195, 14547, 14459, 14476, 14562, 14621, 14648, 14699, 14756, 14831,
- 15078, 15754, 15755, 16072, 17187, 17325.
+ 15078, 15754, 15755, 16072, 17137, 17187, 17325.
+
+* 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.
* Support for loadable gconv transliteration modules has been removed.
The support for transliteration modules has been non-functional for
diff --git a/locale/findlocale.c b/locale/findlocale.c
index eb712fc452..a16077d6eb 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 248b0092a5..3c9a37c64d 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.
+
2012-06-20 Petr Baudis <pasky@ucw.cz>
* locales/mag_IN: Fix comment character.
diff --git a/localedata/Makefile b/localedata/Makefile
index 0873a5524f..214671e119 100644
--- a/localedata/Makefile
+++ b/localedata/Makefile
@@ -78,7 +78,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
ifeq (yes,$(build-shared))
ifneq (no,$(PERL))
tests: $(objpfx)mtrace-tst-leaks
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"