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

Adhemerval Zanella Netto adhemerval.zanella@linaro.org
Tue Sep 8 17:38:17 GMT 2026



On 07/09/26 07:41, Avinal Kumar wrote:
> 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.  Also reject bare "." because it
> is not a legitimate locale name even if the resulting path is a
> real catalog.
> 
> 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 v4:
> - added check and test for bare "."
> - move tests to conditional block
> - added compatibility notes
> - cc'ed bug-gettext
> 
> The check for "." is a no-op but I am adding it for consistency sake. It will
> make the behavior clear that "." is not a legitimate locale name, even if it
> is practically the same path.
> 
> Question: Should we also add the "." check in valid_locale_name(), preferebly
> in a followup patch?
> 
>  NEWS                              |  4 +-
>  intl/Makefile                     |  4 ++
>  intl/dcigettext.c                 |  9 ++--
>  intl/tst-gettext-path-traversal.c | 90 +++++++++++++++++++++++++++++++
>  4 files changed, 102 insertions(+), 5 deletions(-)
>  create mode 100644 intl/tst-gettext-path-traversal.c
> 
> diff --git a/NEWS b/NEWS
> index 56c6b581df..a034e599d1 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -13,7 +13,9 @@ Major new features:
>  
>  Deprecated and removed features, and other changes affecting compatibility:
>  
> -  [Add deprecations, removals and changes affecting compatibility here]
> +* LANGUAGE entries containing '/' or equal to '..' or '.' are now rejected

I am not sure about restricting '.', currently a LANGUAGE=. bound at domaindir/de_DE
resolves to that directory's own LC_MESSAGES and translates.  It should be safe, since
it never leaves the bound directory.

(On my last review I was really not sure, not really asking for the change)

> +  unconditionally (previously only rejected for SUID/SGID binaries).  This
> +  is a deviation from upstream gettext.

This sounds oddly, maybe something like:

* The gettext functions now ignore entries in the LANGUAGE environment variable 
  that contain '/' or are '.' or '..' in all programs, not only SUID/SGID ones.

The rest look ok.

>  
>  Changes to build and runtime requirements:
>  
> diff --git a/intl/Makefile b/intl/Makefile
> index a8b41a1993..2547de2744 100644
> --- a/intl/Makefile
> +++ b/intl/Makefile
> @@ -61,6 +61,7 @@ $(objpfx)plural.o: $(objpfx)plural.c
>  ifeq ($(run-built-tests),yes)
>  ifeq (yes,$(build-shared))
>  ifneq ($(strip $(MSGFMT)),:)
> +tests += tst-gettext-path-traversal
>  tests-special += \
>  	$(objpfx)tst-translit.out \
>  	$(objpfx)tst-gettext.out \
> @@ -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..6f062bbd7a 100644
> --- a/intl/dcigettext.c
> +++ b/intl/dcigettext.c
> @@ -591,10 +591,11 @@ 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
> +	      || 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..49696cbb94
> --- /dev/null
> +++ b/intl/tst-gettext-path-traversal.c
> @@ -0,0 +1,90 @@
> +/* 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");
> +
> +  /* A bare "." is not a valid locale name.  Without the fix this
> +     would resolve to localedir/./LC_MESSAGES/codeset.mo, which is the
> +     real catalog.  */
> +  bindtextdomain ("codeset", localedir);
> +  setenv ("LANGUAGE", ".", 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");
> +
> +  /* A path containing "/.." must be rejected.  Use localedir so the
> +     binding changes, which invalidates the DCIGETTEXT result cache.  */
> +  bindtextdomain ("codeset", localedir);
> +  setenv ("LANGUAGE", "LC_MESSAGES/..", 1);
> +  TEST_COMPARE_STRING (gettext ("cheese"), "cheese");
> +
> +  return 0;
> +}
> +
> +#include <support/test-driver.c>



More information about the Libc-alpha mailing list