[PATCH v3] intl: Restrict path traversal when using LANGUAGE env var [BZ #17142, CVE-2026-84243]
Avinal Kumar <[email protected]>
| Newsgroups | gmane.comp.lib.glibc.alpha |
|---|---|
| Message-ID | <[email protected]> |
The fix for CVE-2014-0475 (Bug 17137) added valid_locale_name() in locale/findlocale.c to reject locale names containing ".." path components. However, the LANGUAGE environment variable processing in intl/dcigettext.c was not covered by that fix. An attacker who can set LANGUAGE (e.g. via SSH AcceptEnv) can force any gettext-using program to load a crafted .mo file from an arbitrary filesystem location via directory traversal. Remove the ENABLE_SECURE gate from the IS_PATH_WITH_DIR check so it applies to all binaries, not just SUID/SGID ones, and extend it to also reject the bare ".." entry which contains no directory separator but still names the parent directory. Add tst-gettext-path-traversal to verify that locale names containing path separators or ".." are rejected by the LANGUAGE processing loop. This fixes bug 17142 and CVE-2026-84243. Suggested-by: Adhemerval Zanella <[email protected]> Signed-off-by: Avinal Kumar <[email protected]> --- Changes from v3: - added CVE details - moved to libc-alpha intl/Makefile | 4 ++ intl/dcigettext.c | 8 +-- intl/tst-gettext-path-traversal.c | 83 +++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 intl/tst-gettext-path-traversal.c diff --git a/intl/Makefile b/intl/Makefile index a8b41a1993..869b2eeebe 100644 --- a/intl/Makefile +++ b/intl/Makefile @@ -43,6 +43,7 @@ endif tests = \ tst-gettext-c-utf8 \ tst-ngettext \ + tst-gettext-path-traversal \ # tests before-compile += $(objpfx)msgs.h @@ -125,6 +126,7 @@ $(objpfx)tst-plural-eval.out: tst-plural-eval.sh $(objpfx)tst-plural-eval $(objpfx)tst-codeset.out: $(codeset_mo) $(objpfx)tst-gettext3.out: $(codeset_mo) $(objpfx)tst-gettext5.out: $(codeset_mo) +$(objpfx)tst-gettext-path-traversal.out: $(codeset_mo) endif LOCALES := de_DE.ISO-8859-1 de_DE.UTF-8 en_US.ANSI_X3.4-1968 fr_FR.ISO-8859-1 \ @@ -139,6 +141,7 @@ $(objpfx)tst-gettext4.out: $(gen-locales) $(objpfx)tst-gettext5.out: $(gen-locales) $(objpfx)tst-gettext6.out: $(gen-locales) $(objpfx)tst-gettext-c-utf8.out: $(gen-locales) +$(objpfx)tst-gettext-path-traversal.out: $(gen-locales) $(objpfx)tst-translit.out: $(gen-locales) endif @@ -159,6 +162,7 @@ CFLAGS-tst-gettext4.c += -DOBJPFX=\"$(objpfx)\" CFLAGS-tst-gettext5.c += -DOBJPFX=\"$(objpfx)\" CFLAGS-tst-gettext6.c += -DOBJPFX=\"$(objpfx)\" CFLAGS-tst-plural-eval.c += -DOBJPFX=\"$(objpfx)\" +CFLAGS-tst-gettext-path-traversal.c += -DOBJPFX=\"$(objpfx)\" ifeq ($(have-thread-library),yes) ifeq (yes,$(build-shared)) diff --git a/intl/dcigettext.c b/intl/dcigettext.c index 43b99c2dea..e9611dae7a 100644 --- a/intl/dcigettext.c +++ b/intl/dcigettext.c @@ -591,10 +591,10 @@ DCIGETTEXT (const char *domainname, const char *msgid1, const char *msgid2, *cp++ = *categoryvalue++; *cp = '\0'; - /* When this is a SUID binary we must not allow accessing files - outside the dedicated directories. */ - if (ENABLE_SECURE && IS_PATH_WITH_DIR (single_locale)) - /* Ignore this entry. */ + /* Do not allow accessing files outside the dedicated + directories. */ + if (IS_PATH_WITH_DIR (single_locale) + || strcmp (single_locale, "..") == 0) continue; } diff --git a/intl/tst-gettext-path-traversal.c b/intl/tst-gettext-path-traversal.c new file mode 100644 index 0000000000..0ddc0c8f70 --- /dev/null +++ b/intl/tst-gettext-path-traversal.c @@ -0,0 +1,83 @@ +/* Test that LANGUAGE values with path traversal are rejected [BZ #17142]. + Copyright (C) 2026 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 + <https://www.gnu.org/licenses/>. */ + +#include <libintl.h> +#include <locale.h> +#include <stdlib.h> +#include <support/check.h> +#include <support/support.h> + +static const char *const domaindir = OBJPFX "domaindir"; +static const char *const localedir = OBJPFX "domaindir/de_DE"; +static const char *const msgdir = OBJPFX "domaindir/de_DE/LC_MESSAGES"; + +static int +do_test (void) +{ + unsetenv ("OUTPUT_CHARSET"); + /* LANGUAGE is only consulted if the locale is not "C" or "C.<codeset>". + The catalog is in ISO-8859-1, as is the locale, so no conversion of + the translation takes place. */ + xsetlocale (LC_ALL, "de_DE.ISO-8859-1"); + textdomain ("codeset"); + + /* Verify that a legitimate LANGUAGE value produces the expected + translation. This exercises the normal lookup path and confirms + that the test .mo catalog is in place. */ + bindtextdomain ("codeset", domaindir); + setenv ("LANGUAGE", "de_DE", 1); + TEST_COMPARE_STRING (gettext ("cheese"), "K\344se"); + + /* A bare ".." must be rejected. Without the fix this would resolve + to msgdir/../LC_MESSAGES/codeset.mo, which is the real catalog, + so the translation would succeed. */ + bindtextdomain ("codeset", msgdir); + setenv ("LANGUAGE", "..", 1); + TEST_COMPARE_STRING (gettext ("cheese"), "cheese"); + + /* A relative path that leaves and re-enters the catalog directory + must be rejected. Without the fix this would resolve to + localedir/../de_DE/LC_MESSAGES/codeset.mo. */ + bindtextdomain ("codeset", localedir); + setenv ("LANGUAGE", "../de_DE", 1); + TEST_COMPARE_STRING (gettext ("cheese"), "cheese"); + + /* Multiple levels of directory traversal must be rejected. Without + the fix this would resolve to + msgdir/../../de_DE/LC_MESSAGES/codeset.mo. */ + bindtextdomain ("codeset", msgdir); + setenv ("LANGUAGE", "../../de_DE", 1); + TEST_COMPARE_STRING (gettext ("cheese"), "cheese"); + + /* Invalid entries in a colon-separated LANGUAGE list must be + skipped individually; valid entries that follow are still + used. */ + bindtextdomain ("codeset", domaindir); + setenv ("LANGUAGE", "..:../de_DE:de_DE", 1); + TEST_COMPARE_STRING (gettext ("cheese"), "K\344se"); + + /* Trailing "/.." must be rejected. Use localedir so the binding + changes, which invalidates the DCIGETTEXT result cache. */ + bindtextdomain ("codeset", localedir); + setenv ("LANGUAGE", "de_DE/..", 1); + TEST_COMPARE_STRING (gettext ("cheese"), "cheese"); + + return 0; +} + +#include <support/test-driver.c> -- 2.55.0