[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
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
* 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
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.
@@ -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))
@@ -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;
}
new file mode 100644
@@ -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>