oss-sec mailing list archives

Re: Telnetd Vulnerability Report


From: Justin Swartz <justin.swartz () risingedge co za>
Date: Sun, 8 Mar 2026 09:34:22 +0200

On Sun, 08 Mar 2026 06:05:45 +0200, Justin Swartz wrote:
I'll submit a third version of this patch set later.

Based on the feedback provided, the third version of the patch set [1]:

- Leaves the inherited environment intact.
- Implements a default whitelist and whitelisted variable value
  sanitization.
- Places the strings of the allowed environment variables array into
  the .rodata section.
- Eliminates duplicated setenv/unsetenv logic in "telnetd/state.c".
- Discards the --accept-env feature [3], as an inetutils maintainer [2]
  is working on an implementation to extend the allowed environment
  using Gnulib instead.

Find the patch included below.

Regards,
Justin

---

[1] https://lists.gnu.org/archive/html/bug-inetutils/2026-03/msg00020.html
[2] https://lists.gnu.org/archive/html/bug-inetutils/2026-03/msg00017.html 
[3] https://lists.gnu.org/archive/html/bug-inetutils/2026-03/msg00018.html 


From 1b9dc91cfd3c730317aa3bb6ec58ff1beb5dcc15 Mon Sep 17 00:00:00 2001
From: Justin Swartz <justin.swartz () risingedge co za>
Date: Sun, 8 Mar 2026 06:55:23 +0200
Subject: [PATCH v3 1/1] telnetd: replace environment blacklist with a
 whitelist.

The previous method of scrubbing environment variables, scrub_env(),
and targeted calls to unsetenv() were insufficient to protect against
glibc-based injection attacks, such as the recently reported
CVE-1999-0073 regression.

To fix this issue, the approach suggested by Simon Josefsson in
<https://lists.gnu.org/archive/html/bug-inetutils/2026-02/msg00002.html>
has been taken to replace the reactive blacklist with a fairly strict
default whitelist of the following allowed environment variables:

  USER LOGNAME TERM LANG LC_*

And as suggested by Solar Designer, all whitelisted variables will be
subject to sanitization, and the inherited environment will be left
intact.

Any negotiated variable will be dropped if its value contains a path
separator ('/'), or an explicit reference to the current working
directory (".") or its parent ("..").

* telnetd/utility.c (allowed_env_vars): New whitelist array.
(is_env_var_allowed): New function.
(set_env_var_if_allowed): New helper function.
(getterminaltype): Apply final whitelist validation to terminaltype.
(terminaltypeok): Validate terminal type against the whitelist.
* telnetd/state.c (suboption): Filter NEW_ENVIRON during parsing using
set_env_var_if_allowed().
* telnetd/pty.c (start_login): Remove the obsolete scrubbing logic.
* telnetd/telnetd.h: Add prototypes for new functions.
---
 telnetd/pty.c     | 32 ------------------
 telnetd/state.c   | 10 ++----
 telnetd/telnetd.h |  3 ++
 telnetd/utility.c | 83 +++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 88 insertions(+), 40 deletions(-)

diff --git a/telnetd/pty.c b/telnetd/pty.c
index f3518049..4bf407ad 100644
--- a/telnetd/pty.c
+++ b/telnetd/pty.c
@@ -83,29 +83,6 @@ startslave (char *host, int autologin, char *autoname)
   return master;
 }
 
-/*
- * scrub_env()
- *
- * Remove a few things from the environment that
- * don't need to be there.
- *
- * Security fix included in telnet-95.10.23.NE of David Borman <deb () cray com>.
- */
-static void
-scrub_env (void)
-{
-  char **cpp, **cpp2;
-
-  for (cpp2 = cpp = environ; *cpp; cpp++)
-    {
-      if (strncmp (*cpp, "LD_", 3)
-         && strncmp (*cpp, "_RLD_", 5)
-         && strncmp (*cpp, "LIBPATH=", 8) && strncmp (*cpp, "IFS=", 4))
-       *cpp2++ = *cpp;
-    }
-  *cpp2 = 0;
-}
-
 void
 start_login (char *host, int autologin, char *name)
 {
@@ -117,8 +94,6 @@ start_login (char *host, int autologin, char *name)
   (void) autologin;
   (void) name;
 
-  scrub_env ();
-
   /* Set the environment variable "LINEMODE" to indicate our linemode */
   if (lmodetype == REAL_LINEMODE)
     setenv ("LINEMODE", "real", 1);
@@ -130,13 +105,6 @@ start_login (char *host, int autologin, char *name)
     fatal (net, "can't expand login command line");
   argcv_get (cmd, "", &argc, &argv);
 
-  /* util-linux's "login" introduced an authentication bypass method
-   * via environment variable "CREDENTIALS_DIRECTORY" in version 2.40.
-   * Clear it from the environment before executing "login" to prevent
-   * abuse via Telnet.
-   */
-  unsetenv ("CREDENTIALS_DIRECTORY");
-
   execv (argv[0], argv);
   syslog (LOG_ERR, "%s: %m\n", cmd);
   fatalperror (net, cmd);
diff --git a/telnetd/state.c b/telnetd/state.c
index a9a51e00..ab6bfb11 100644
--- a/telnetd/state.c
+++ b/telnetd/state.c
@@ -1495,10 +1495,7 @@ suboption (void)
              case NEW_ENV_VAR:
              case ENV_USERVAR:
                *cp = '\0';
-               if (valp)
-                 setenv (varp, valp, 1);
-               else
-                 unsetenv (varp);
+               set_env_var_if_allowed (varp, valp);
                cp = varp = (char *) subpointer;
                valp = 0;
                break;
@@ -1514,10 +1511,7 @@ suboption (void)
              }
          }
        *cp = '\0';
-       if (valp)
-         setenv (varp, valp, 1);
-       else
-         unsetenv (varp);
+       set_env_var_if_allowed (varp, valp);
        break;
       }                                /* end of case TELOPT_NEW_ENVIRON */
 #if defined AUTHENTICATION
diff --git a/telnetd/telnetd.h b/telnetd/telnetd.h
index df31a819..8b14d9dd 100644
--- a/telnetd/telnetd.h
+++ b/telnetd/telnetd.h
@@ -316,6 +316,9 @@ extern void tty_setsofttab (int);
 extern void tty_tspeed (int);
 
 extern char *expand_line (const char *fmt);
+extern int is_env_var_allowed (const char *var, const char *val);
+extern void set_env_var_if_allowed (const char *var, const char *val);
+
 
 /*  FIXME */
 extern void _termstat (void);
diff --git a/telnetd/utility.c b/telnetd/utility.c
index 2fe6730c..085065ea 100644
--- a/telnetd/utility.c
+++ b/telnetd/utility.c
@@ -17,6 +17,16 @@
   along with this program.  If not, see `http://www.gnu.org/licenses/&apos;. */
 
 #include <config.h>
+#include <fnmatch.h>
+#include <string.h>
+
+#ifdef HAVE_PATHS_H
+# include <paths.h>
+#else
+# ifndef _PATH_DEFPATH
+#  define _PATH_DEFPATH "/usr/bin:/bin"
+# endif
+#endif
 
 #define TELOPTS
 #define TELCMDS
@@ -65,6 +75,66 @@ static int pcc;
 
 extern int not42;
 
+/* A default whitelist for environment variables. */
+static const char * const allowed_env_vars[] = {
+  "USER",
+  "LOGNAME",
+  "TERM",
+  "LANG",
+  "LC_*",
+  NULL
+};
+
+int
+is_env_var_allowed (const char *var, const char *val)
+{
+  const char * const *p;
+  int allowed = 0;
+
+  for (p = allowed_env_vars; *p; p++)
+    {
+      if (fnmatch (*p, var, FNM_NOESCAPE) == 0)
+        {
+          allowed = 1;
+          break;
+        }
+    }
+
+  if (!allowed)
+    return 0;
+
+  if (val != NULL)
+    {
+      if (strchr (val, '/') != NULL)
+        return 0;
+
+      if (strcmp (val, "..") == 0)
+        return 0;
+
+      if (strcmp (val, ".") == 0)
+        return 0;
+    }
+
+  return 1;
+}
+
+void
+set_env_var_if_allowed (const char *var, const char *val)
+{
+  if (is_env_var_allowed (var, val))
+    {
+      if (val)
+        {
+          if (*val != 0)
+            setenv (var, val, 1);
+        }
+      else
+        {
+          unsetenv (var);
+        }
+    }
+}
+
 static int
 readstream (int p, char *ibuf, int bufsize)
 {
@@ -863,6 +933,16 @@ getterminaltype (char *uname, size_t len)
        }
       free (first);
       free (last);
+
+      /* Does TERM appear to be illogical? */
+      if (terminaltype)
+       {
+          if (!is_env_var_allowed ("TERM", terminaltype))
+            {
+               free (terminaltype);
+               terminaltype = NULL;
+            }
+       }
     }
   return retval;
 }
@@ -876,6 +956,9 @@ getterminaltype (char *uname, size_t len)
 int
 terminaltypeok (char *s)
 {
+  if (!is_env_var_allowed ("TERM", s))
+    return 0;
+
 #ifdef HAVE_TGETENT
   char buf[2048];
 
-- 


Current thread: