How Environment Variables Become a Linux Attack Surface

Checking an executable’s path does not tell you every piece of code it will run. Its environment can influence which libraries load, which helper programs execute and which settings the application trusts.

Shellshock, PwnKit and Looney Tunables illustrate different failures along that path. Understanding the differences makes an environment review more useful than searching for a list of suspicious variable names.

Start with the value and its reader

An environment variable is a named value supplied to a program when it starts. For example:

PATH=/usr/bin:/bin

Here, PATH lists two directories to search when resolving a command name. Child processes normally inherit their parent’s environment, although a launcher can supply a different one. Changing your shell’s environment does not rewrite an already running service’s environment. 1, 3

A value becomes security-sensitive when it reaches code that uses it to select behavior. The attacker needs a route to influence that value. Examples include launching a local program, modifying a service’s startup configuration or reaching an application that maps request data into an environment variable.

Control over your own process is not automatically privilege escalation. The concern is crossing a trust boundary: making another component act with permissions or access you should not control.

Where environment values are consumedA local launcher or service manager supplies the environment at execution. For a dynamically linked glibc program, the loader reads startup values before main. Application code and dependencies may later read other values, load a library in the current process or execute a helper with its own launch environment. Executing a helper repeats startup; a runtime library load stays inside the process. Where environment values are consumed Linux / glibc · simplified execution path Local launcher Caller supplies values Service manager Configured launch values Environment passed at execution Dynamic linker + library initialization Startup reads before main() Loader restrictions depend on secure-execution mode Application + dependencies Further environment reads during execution Load a library For example: plugin selection Code stays inside this process Execute a helper For example: PATH-based lookup Helper startup reads its environment Every arrow carries a review question: who controls the next input?
Figure 1. A simplified execution path. main() is the application’s main entry point. Libraries can read values during initialization or later calls; executing a helper begins that executable’s startup sequence. Sources: 1, 2, 3, 9, 10.

Set-user-ID programs and services have different boundaries

The set-user-ID permission bit, often called SUID, can make an executable run with its owner’s effective user ID. That identity governs many permission checks. A root-owned SUID program can therefore perform privileged work for an ordinary caller; its real user ID still identifies the caller. 3

The service approach starts a process under a configured account and accepts requests through an interface, such as a local socket. The comparison matters because the caller does not necessarily control the same inputs.

Question SUID executable System service managed by systemd
Who supplies startup values? The launching process supplies an environment, subject to filtering. The service manager assembles it from configuration and other configured sources.
What establishes authority? An eligible execution changes the effective user ID to the file owner’s. The manager starts the service under its configured account.
What needs review? Caller-controlled inputs across the privilege transition. Startup configuration, request authorization and the service’s existing permissions.

glibc, the GNU C Library, enables its loader’s secure-execution mode when the kernel supplies a nonzero AT_SECURE flag. A difference between real and effective IDs is a common trigger. In this mode, LD_LIBRARY_PATH is ignored and LD_PRELOAD is restricted. Running a service as root does not, by itself, activate this protection. 2

A systemd system service does not simply inherit the shell environment of whoever runs systemctl start. Review its unit configuration, including Environment= and EnvironmentFile=. A service can make startup inputs easier to control, but its request interface still needs authentication and authorization. 4

The dynamic linker reads values before application code

For a dynamically linked program, the dynamic linker loads shared libraries: reusable code the executable depends on. Some of this work happens before main(), the application’s main entry point.

Outside secure-execution restrictions, LD_LIBRARY_PATH influences library searches and LD_PRELOAD requests additional libraries loaded ahead of others. These have legitimate uses. They become dangerous when an attacker can influence a more privileged program’s environment and get it to load attacker-controlled code. Removing a variable inside main() cannot undo library initialization that already ran. 2, 10

Looney Tunables — CVE-2023-4911, disclosed in October 2023 — involved a different loader failure. A buffer overflow, a write beyond an allocated buffer, affected glibc’s handling of GLIBC_TUNABLES, a runtime tuning variable. A local user could exploit it to gain root privileges on affected systems. Qualys demonstrated the flaw through SUID programs. The failure was in the loader’s parsing and sanitization code. 5

That distinction affects remediation: a vulnerable loader needs a vendor fix, even when the application validates its own settings.

News coverage: BleepingComputer, 3 October 2023.

External programs add another decision point

Consider a backup application that invokes tar by name using execvp(), a function that searches PATH. If an attacker controls the search path and can place an executable in a directory searched first, the application may run the wrong program. No shell metacharacters are needed for this form of substitution. 6

An absolute path avoids that name search. It does not make the executable’s environment, arguments or dependencies trustworthy. A shell adds another interpreter: C’s system() passes a command to /bin/sh, which is not necessarily Bash. 3, 7

Shellshock — including CVE-2014-6271, disclosed in September 2014 — showed why the receiving program matters. Vulnerable Bash versions could execute trailing commands while importing shell functions, or named blocks of commands, from environment variables.

One remote route involved CGI, an interface through which a web server passes request information to a program using environment variables. If attacker-controlled request data reached a vulnerable Bash invocation, commands could run with that process’s permissions. Root access was not automatic, and an HTTP request alone was insufficient without the vulnerable execution path. Red Hat also documented follow-up flaws after the initial fix. 8

News coverage: KrebsOnSecurity, 25 September 2014.

Libraries and application code can read the environment later

Application code can read a value with getenv(). A hypothetical REPORT_PLUGIN setting might select a plugin through dlopen(), which loads a shared library into the current process. The plugin then runs with that process’s authority. Loader filtering does not validate every application-defined setting. 9, 10

glibc’s secure_getenv() withholds values in secure-execution mode. It does not validate values for an ordinary service. Code still needs to check allowed values and handle missing settings explicitly. 9

PwnKit — CVE-2021-4034, disclosed in January 2022 — connected an application bug to library behavior. Memory corruption in polkit’s SUID-root pkexec utility allowed an unsafe environment variable to be reintroduced after startup filtering. Qualys described a route using GCONV_PATH to influence character-conversion module loading and execute code as root. The vulnerability was in pkexec; library loading completed the chain. 11

The lesson extends beyond explicit plugin systems: dependencies may read environment values during error handling or other operations that look unrelated to loading code.

News coverage: Computer Weekly, 26 January 2022.

Three vulnerabilities, three different failuresHistorical case comparison, not experimental results. Looney Tunables: a local attacker launches an affected privileged executable; GLIBC_TUNABLES parsing in the dynamic loader overflows; exploitation can elevate privileges. Shellshock: request data mapped into environment variables reaches vulnerable Bash; function import executes commands as the Bash process identity. PwnKit: a local attacker reaches vulnerable SUID-root pkexec; memory corruption reintroduces an unsafe environment variable; the documented GCONV_PATH module-loading route executes code as root. Three vulnerabilities, three different failures Documented mechanisms · affected versions and reachable paths required Looney Tunables CVE-2023-4911 · October 2023 Local execution Privileged executable Loader parser GLIBC_TUNABLES Privilege escalation Through buffer overflow Failed boundary: privileged startup processed attacker-controlled tuning data. Shellshock CVE-2014-6271 · September 2014 Request → environment CGI route shown Vulnerable Bash Function import Command execution As the Bash process Remote exposure requires request mapping and a vulnerable Bash invocation. PwnKit CVE-2021-4034 · January 2022 Local execution SUID-root pkexec Memory corruption Unsafe variable restored Code execution as root GCONV_PATH module route Failed boundary: application corruption undid startup environment filtering. Disclosure dates shown. PwnKit was disclosed in 2022 despite its 2021 CVE identifier.
Figure 2. Three historical failure paths from the published advisories. The Shellshock row shows one CGI route; the PwnKit row shows the documented conversion-module route. These are explanatory diagrams, not reproduced exploit traces. Sources: 5, 8, 11.

Match the control to the point of use

These historical CVEs have published fixes. Confirm installed packages against your distribution’s advisories; vendors may backport fixes without adopting the newest upstream version number. 5, 8, 11, 13

For application and service reviews, I would check five things:

  • Control the launch environment. Pass only required, approved values before execution. Protect service units, environment files and their parent directories from unauthorized changes.
  • Make helper execution explicit. Use trusted absolute paths, separate arguments and a deliberate child environment. Validate arguments too; avoiding a shell does not prevent a helper from interpreting an unsafe option.
  • Constrain library selection. Map approved plugin names to protected files instead of accepting arbitrary paths. Include dependencies and directory permissions in that review.
  • Limit authority. Use a dedicated service account with only the access it needs. Where compatible, NoNewPrivileges=yes prevents execution from granting additional privileges; it does not remove permissions the service already has. 4, 12
  • Monitor decisions, not just names. Investigate unexpected helper paths, library loads and startup-configuration changes where telemetry supports them. A variable’s presence alone is not evidence of compromise. Avoid collecting whole environments into logs; they can contain secrets. 4

For each sensitive value, record who can supply it, which component reads it, when it is read and what authority that component holds. That record tells you whether the fix belongs in the launcher, a dependency, application code or the service’s permission model.

Technical references

  1. Linux man-pages: environ(7) — process environments and inheritance.
  2. Linux man-pages: ld.so(8) — loader behavior and secure execution.
  3. Linux man-pages: execve(2) — execution, environment and identity changes.
  4. systemd: systemd.exec — service environments, identity and restrictions.
  5. Qualys: Looney Tunables advisory, CVE-2023-4911 — original disclosure, 3 October 2023.
  6. Linux man-pages: exec(3) — executable-name resolution.
  7. Linux man-pages: system(3) — shell invocation and privileged-program cautions.
  8. Red Hat: Shellshock vulnerability — Bash behavior, CGI exposure and follow-up fixes.
  9. Linux man-pages: getenv(3) — environment reads and secure_getenv().
  10. Linux man-pages: dlopen(3) — library loading and initialization.
  11. Qualys: PwnKit advisory, CVE-2021-4034 — original disclosure, 25 January 2022.
  12. Linux kernel: No New Privileges Flag — protection and limits.
  13. Red Hat: Security Backporting Practice — interpreting vendor package versions.
Next
Next

How I’m Using ChatGPT to Learn Regex