[v3] intl: Restrict path traversal when using LANGUAGE env var [BZ #17142, CVE-2026-84243]

Message ID 20260901133902.184341-1-avinal.xlvii@gmail.com (mailing list archive)
State Superseded
Headers
Series [v3] intl: Restrict path traversal when using LANGUAGE env var [BZ #17142, CVE-2026-84243] |

Checks

Context Check Description
redhat-pt-bot/TryBot-apply_patch success Patch applied to master at the time it was sent
linaro-tcwg-bot/tcwg_glibc_build--master-aarch64 success Build passed
linaro-tcwg-bot/tcwg_glibc_build--master-arm success Build passed
redhat-pt-bot/TryBot-32bit fail Patch caused testsuite regressions
linaro-tcwg-bot/tcwg_glibc_check--master-aarch64 fail Test failed
linaro-tcwg-bot/tcwg_glibc_check--master-arm pending Patch applied

Commit Message

Avinal Kumar Sept. 1, 2026, 1:39 p.m. UTC
  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 <adhemerval.zanella@linaro.org>
Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
---
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
  

Comments

Florian Weimer Sept. 1, 2026, 2:01 p.m. UTC | #1
* Avinal Kumar:

> 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;
>  	}
>  

Is there an impact on SUID binaries because ".." was not skipped before?

Thanks,
Florian
  
Avinal Kumar Sept. 1, 2026, 2:19 p.m. UTC | #2
On Tue, Sep 1, 2026 at 7:31 PM Florian Weimer <fweimer@redhat.com> wrote:
>
> * Avinal Kumar:
>
> > 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;
> >       }
> >
>
> Is there an impact on SUID binaries because ".." was not skipped before?
>
> Thanks,
> Florian
>
Now that I think, yes, there may be some impact. IS_PATH_WITH_DIR
checks for '/' but a bare '..' has no slash, so it was never caught,
not even for SUID binaries where ENABLE_SECURE is true.

But any practical impact should be limited by the fact that
LANGUAGE=.. is just one level up, that path is generally not writable
by normal users. So it will reduces the possibility that a malicious
file could be there.
  

Patch

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>