]> git.ipfire.org Git - thirdparty/glibc.git/commitdiff
_nl_find_locale: Improve handling of crafted locale names [BZ #17137]
authorFlorian Weimer <fweimer@redhat.com>
Mon, 12 May 2014 13:24:12 +0000 (15:24 +0200)
committerAdhemerval Zanella <azanella@linux.vnet.ibm.com>
Thu, 15 Jan 2015 20:12:03 +0000 (15:12 -0500)
Prevent directory traversal in locale-related environment variables
(CVE-2014-0475).

ChangeLog
NEWS
locale/findlocale.c
localedata/ChangeLog
localedata/Makefile
localedata/tst-setlocale3.c [new file with mode: 0644]

index a1cc122ce9559de15feb8d9315945a9f07e15556..3f3665074a0edf1d6a20effb61bebbcc26541efd 100644 (file)
--- 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 3665933311b945349b4823eac07479cf7feb6dcb..fb66b4da231881ff7756033dafbe8bc07d3725aa 100644 (file)
--- 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
index eb712fc452e19deeab7d46c9cb1a5829bc4bdb40..a16077d6ebc87f4db34ef418f6adf0c4bb79387f 100644 (file)
@@ -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.  */
index 248b0092a5456f7fab00fa243c19fb5426ba7cb0..3c9a37c64d1e426472dcfdd066b120b3913ef595 100644 (file)
@@ -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.
index 0873a5524f051a0bfab9e88c73f7731354c30b57..214671e11981dc09d77857a6a8a6b38587e92997 100644 (file)
@@ -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 (file)
index 0000000..e3b21a9
--- /dev/null
@@ -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"