[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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.