Back to Blog
medium SEVERITY8 min read

Command Injection in Firejail's netfilter.c: How Environment Variables Can Lead to Root Compromise

A critical command injection vulnerability was discovered and patched in Firejail's `netfilter.c`, where attacker-controlled environment variables could be used to inject shell metacharacters into a command string executed with elevated privileges. This type of vulnerability is particularly dangerous in security-focused tools like Firejail, which often run with root or elevated permissions, potentially allowing a local attacker to achieve full system compromise. The fix removes the unsafe `exec(

O
By orbisai0security
May 13, 2026

Command Injection in Firejail's netfilter.c: How Environment Variables Can Lead to Root Compromise

Introduction

There's a certain irony when a security tool becomes the source of a security vulnerability. Firejail — the widely-used Linux sandboxing utility designed to restrict what programs can do — was found to contain a command injection flaw in its network filtering code. This vulnerability, tracked as V-004, existed in src/firejail/netfilter.c and could allow a local attacker to execute arbitrary commands with Firejail's elevated (potentially root) privileges.

This post breaks down what went wrong, how it could be exploited, and what developers working on privileged C applications can learn from it.


The Vulnerability Explained

What Is Command Injection?

Command injection occurs when an application constructs a shell command using untrusted input without proper sanitization. If an attacker can influence the content of that command string, they can append or inject additional shell instructions — potentially running any command they choose.

This is distinct from SQL injection or XSS, but the root cause is the same: mixing untrusted data with trusted instructions.

What Happened in netfilter.c?

In src/firejail/netfilter.c around line 70, the code constructed a shell command string using asprintf(). The command was assembled from three components:

  1. terminal — a value sourced from the TERMINAL environment variable (attacker-controlled)
  2. LIBDIR — a compile-time constant (safe)
  3. flog — derived from getpid() (safe, but predictable)

The resulting string looked something like this:

// Simplified representation of the vulnerable pattern
char *cmd;
char *terminal = getenv("TERMINAL");  // ← Attacker-controlled!

asprintf(&cmd, "%s %s/firejail/some-helper %s", terminal, LIBDIR, flog);
// Then passed to exec() or a shell function...

Because terminal came directly from the environment without any validation or sanitization, an attacker could set:

export TERMINAL="xterm; malicious-command #"

When Firejail constructed and executed the command, it would become:

xterm; malicious-command # /usr/lib/firejail/some-helper /tmp/firejail-1234.log

The semicolon terminates the first command, malicious-command runs independently, and the # comments out the rest. Shell metacharacters like ;, |, &&, ||, `, $(), and newlines all enable this kind of manipulation.

Why Is This Particularly Dangerous?

Firejail frequently runs with elevated privileges — often as a setuid-root binary or invoked with sudo. This means the injected command doesn't just run as the current user; it runs as root.

A local attacker (or a compromised process) that can set environment variables before Firejail executes could:

  • Read or exfiltrate sensitive files (/etc/shadow, private keys)
  • Create new privileged user accounts
  • Install persistent backdoors
  • Escape the very sandbox Firejail is meant to enforce

Real-World Attack Scenario

Consider this scenario:

  1. A developer uses Firejail to sandbox an untrusted application.
  2. That application, before being sandboxed, sets TERMINAL to include a malicious payload.
  3. Firejail's netfilter code executes during sandbox setup with root privileges.
  4. The injected command runs as root, fully compromising the host system.

Alternatively, a local attacker on a shared system (e.g., a university server or cloud VM with multiple users) could set the environment variable in their shell session and trigger Firejail execution to escalate privileges.


The Fix

What Changed?

The fix removes the unsafe exec() call that relied on shell interpretation of the constructed command string. The core principle of the fix is:

Never pass attacker-controlled data to a shell. If you must execute subprocesses, use execv()/execve() with argument arrays, not shell strings.

The key improvements are:

  1. Elimination of the TERMINAL environment variable dependency — The code no longer reads getenv("TERMINAL") for constructing privileged commands.
  2. Removal of shell-interpreted execution — Instead of constructing a string and passing it to a shell, the fix uses direct execution paths that don't invoke /bin/sh.
  3. Input validation — Any remaining external input is validated before use.

Before and After (Conceptual)

Before (vulnerable pattern):

// DANGEROUS: Environment variable used directly in shell command
char *terminal = getenv("TERMINAL");
char *cmd = NULL;
char *flog = /* derived from getpid() */;

asprintf(&cmd, "%s %s/firejail/helper %s", terminal, LIBDIR, flog);

// Shell interprets the entire string — metacharacters execute!
system(cmd);  // or popen(cmd, ...) or similar
free(cmd);

After (safe pattern):

// SAFE: Use execv() with explicit argument array — no shell involved
char helper_path[PATH_MAX];
snprintf(helper_path, sizeof(helper_path), "%s/firejail/helper", LIBDIR);

// flog is derived from getpid(), validated to be numeric
char *args[] = {
    helper_path,
    flog,
    NULL
};

// execv does NOT invoke a shell; metacharacters have no special meaning
execv(helper_path, args);

With execv(), there is no shell to interpret metacharacters. The arguments are passed directly to the program as discrete strings. Even if an attacker somehow influenced flog, a ; or | would be treated as a literal character, not a shell operator.

Why This Solves the Problem

The fundamental issue was shell interpolation — the act of passing a constructed string to a shell (/bin/sh -c "..." or equivalent) that then parses it for special characters. By removing the shell from the equation entirely:

  • Metacharacters lose their power
  • Each argument is isolated and cannot "escape" into another command
  • The attack surface is dramatically reduced

Prevention & Best Practices

1. Never Trust Environment Variables in Privileged Code

Environment variables are user-controlled. In any setuid binary, privileged daemon, or code that runs with elevated permissions, treat all environment variables as hostile input.

// BAD: Using getenv() output directly in a privileged operation
char *path = getenv("PATH");
execl("/bin/sh", "sh", "-c", path, NULL);

// BETTER: Use hardcoded paths or validate strictly
// In setuid programs, consider clearing the environment entirely

In fact, the Linux dynamic linker (ld.so) automatically ignores certain environment variables (like LD_PRELOAD) for setuid binaries. Your application-level code should apply the same skepticism.

2. Prefer execv()/execve() Over system() or popen()

Function Shell Involved? Safe for Untrusted Input?
system(cmd) ✅ Yes ❌ No
popen(cmd, mode) ✅ Yes ❌ No
execl(path, ...) ❌ No ✅ With validation
execv(path, argv[]) ❌ No ✅ With validation
execve(path, argv[], envp[]) ❌ No ✅ Best option

execve() is the gold standard — it lets you explicitly control both the argument array and the environment passed to the child process.

3. Validate and Sanitize All External Input

If you absolutely must use external input in a command, validate it strictly:

#include <ctype.h>
#include <string.h>

// Only allow alphanumeric characters and specific safe chars
int is_safe_terminal_name(const char *name) {
    if (!name || strlen(name) == 0 || strlen(name) > 64)
        return 0;

    for (const char *p = name; *p; p++) {
        if (!isalnum(*p) && *p != '-' && *p != '_' && *p != '/')
            return 0;  // Reject anything suspicious
    }
    return 1;
}

Better yet, use an allowlist of known-good values rather than trying to blocklist bad ones.

4. Drop Privileges as Early as Possible

If your application needs elevated privileges for only part of its operation, drop them immediately after:

// Perform privileged operation
setup_network_filter();

// Drop root privileges immediately
if (setuid(getuid()) != 0) {
    perror("Failed to drop privileges");
    exit(EXIT_FAILURE);
}

// Rest of the code runs as normal user

This limits the blast radius of any exploitation — even if a vulnerability exists, the attacker gains fewer privileges.

5. Clear the Environment in Setuid Binaries

For setuid programs, consider clearing the environment and rebuilding only what's needed:

#include <unistd.h>

// Clear all environment variables
clearenv();

// Set only what you need, with known-safe values
setenv("PATH", "/usr/local/bin:/usr/bin:/bin", 1);

6. Use Static Analysis Tools

Several tools can catch this class of vulnerability automatically:

  • Semgrep — Has rules for detecting getenv() usage in dangerous contexts
  • CodeQL — Can trace taint flow from environment variables to exec functions
  • Coverity — Commercial tool with strong command injection detection
  • Flawfinder — Lightweight C/C++ security scanner
  • Compiler warnings-Wformat-security and -D_FORTIFY_SOURCE=2 catch some unsafe patterns

7. Relevant Security Standards

This vulnerability maps to well-known security weaknesses:


A Note on the Severity Discrepancy

You may notice that the vulnerability description labels this as medium severity in one place and critical in another. This discrepancy likely reflects different scoring methodologies:

  • A CVSS base score calculation considering local-only access and specific preconditions might yield a medium score
  • A contextual/environmental score accounting for Firejail's setuid-root nature and the realistic exploitation scenario pushes this firmly into critical territory

When a security tool that runs as root is vulnerable to privilege escalation via trivially-set environment variables, treat it as critical regardless of the base score. Context matters.


Conclusion

The command injection vulnerability in Firejail's netfilter.c is a textbook example of a dangerous but preventable mistake: trusting environment variables in a privileged context and using shell-interpreted execution. The fix — removing the unsafe exec() call and eliminating the TERMINAL environment variable dependency — is the right approach and serves as a model for how to handle subprocess execution securely.

Key takeaways for developers:

  1. 🔴 Environment variables are attacker-controlled — never use them directly in privileged command construction
  2. 🔴 system() and popen() invoke a shell — prefer execv()/execve() for subprocess execution
  3. 🟡 Drop privileges as early as possible — minimize the window of elevated execution
  4. 🟢 Use static analysis — tools like CodeQL and Semgrep can catch taint-flow issues automatically
  5. 🟢 Apply defense in depth — validate inputs, use allowlists, and clear the environment in setuid binaries

Security tools deserve extra scrutiny precisely because they run with elevated privileges and are trusted by users to protect them. When writing or reviewing security-sensitive C code, approach every getenv() call, every asprintf(), and every exec*() with healthy paranoia.

Stay secure, and keep your shells clean. 🔒


This vulnerability was identified and fixed by OrbisAI Security. Responsible disclosure and timely patching make the open-source ecosystem safer for everyone.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #7159

Related Articles

high

Shell Injection via Unsafe String Concatenation in gRPCurl Command Generation

A high-severity vulnerability was discovered in PaddleOCR's deployment configuration where model download URLs were specified using unencrypted `http://`, exposing users to man-in-the-middle attacks that could allow an attacker to intercept and replace model files with malicious ones. The fix upgrades all model download URLs to use `https://`, ensuring encrypted transmission and integrity of the downloaded files. This change is a critical security baseline for any application that downloads bina

critical

Shell Injection via os.system(): How a Single Line of Code Can Compromise Your System

A critical OS command injection vulnerability (CWE-78) was discovered and patched in `voice.py`, where user-controlled input was interpolated directly into a shell command string passed to `os.system()`. An attacker who could influence the `device` variable — through a config file, environment variable, or any external input — could execute arbitrary system commands with the full privileges of the running process. The fix replaces the dangerous `os.system()` calls with Python's `subprocess.run()

critical

Command Injection via os.system() in DeepSpeed's Data Analyzer: A Critical Fix

A critical command injection vulnerability was discovered in DeepSpeed's `data_analyzer.py`, where an `os.system()` call directly interpolated an unsanitized file path variable into a shell command string. An attacker who could influence dataset configuration or file paths could execute arbitrary shell commands on the host machine. The fix replaces the dangerous shell invocation with safe, Python-native file operations that never touch a shell interpreter.

high

Shell Injection via Unsafe String Concatenation in gRPC Command Generation

A high-severity shell injection vulnerability was discovered in `src/RtlJaguarDevice.cpp`, where user-controlled values from API responses were directly interpolated into gRPCurl command strings without proper shell escaping. An attacker who controls API response data could inject shell metacharacters, causing arbitrary command execution when a user pastes and runs the generated command. The fix applies proper shell escaping to all user-controlled values before they are included in command strin

high

Shell Injection via Unsafe String Concatenation in gRPCurl Command Generation

A high-severity shell injection vulnerability was discovered and patched in a distributed server's gRPCurl command generation logic, where user-controlled values from API responses were directly interpolated into shell command strings without proper escaping. An attacker who can influence API response data — such as headers, endpoints, or payloads — could inject shell metacharacters that execute arbitrary commands when a user pastes and runs the generated command. This fix eliminates the risk by

high

Shell Injection via gRPCurl Command Generation: A Hidden Android Threat

A high-severity shell injection vulnerability was discovered and fixed in the HeadUnit Revived Android project, where user-controlled API response values were unsafely interpolated into gRPCurl command strings. An attacker could craft malicious headers, endpoints, or data payloads containing shell metacharacters that, when the generated command is pasted and executed, would run arbitrary commands on the victim's machine. The fix introduces proper shell escaping and broadcast intent protection to