_nl_find_locale: Improve handling of crafted locale names [BZ #17137]

Prevent directory traversal in locale-related environment variables
(CVE-2014-0475).
This commit is contained in:
Florian Weimer 2014-05-12 15:24:12 +02:00
parent d183645616
commit 4e8f95a0df
6 changed files with 292 additions and 15 deletions

View File

@ -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-07-02 Florian Weimer <fweimer@redhat.com>
* locale/setlocale.c (setlocale): Use strdup for allocating

12
NEWS
View File

@ -21,7 +21,8 @@ Version 2.20
16882, 16885, 16888, 16890, 16912, 16915, 16916, 16917, 16918, 16922,
16927, 16928, 16932, 16943, 16958, 16965, 16966, 16967, 16977, 16978,
16984, 16990, 16996, 17009, 17022, 17031, 17042, 17048, 17050, 17058,
17061, 17062, 17069, 17075, 17079, 17084, 17086, 17092, 17097, 17125.
17061, 17062, 17069, 17075, 17079, 17084, 17086, 17092, 17097, 17125,
17137.
* Optimized strchr implementation for AArch64. Contributed by ARM Ltd.
@ -70,6 +71,15 @@ Version 2.20
On configurations that support it (all Linux configurations), it's now
used regardless of the --enable-add-ons switch to configure. It is no
longer possible to build such configurations without pthreads support.
* 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

View 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. */

View 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.
2014-06-20 Stefan Liebler <stli@linux.vnet.ibm.com>
* Makefile (LOCALES): Add en_GB.UTF-8.

View File

@ -74,7 +74,8 @@ locale_test_suite := tst_iswalnum tst_iswalpha tst_iswcntrl \
tests = $(locale_test_suite) tst-digits tst-setlocale bug-iconv-trans \
tst-leaks tst-mbswcs1 tst-mbswcs2 tst-mbswcs3 tst-mbswcs4 tst-mbswcs5 \
tst-mbswcs6 tst-xlocale1 tst-xlocale2 bug-usesetlocale \
tst-strfmon1 tst-sscanf bug-setlocale1 tst-setlocale2 tst-wctype
tst-strfmon1 tst-sscanf bug-setlocale1 tst-setlocale2 tst-setlocale3 \
tst-wctype
tests-static = bug-setlocale1-static
tests += $(tests-static)
ifeq (yes,$(build-shared))

203
localedata/tst-setlocale3.c Normal file
View File

@ -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"