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 Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published May 13, 2026•Reviewed June 3, 2026

Answer Summary

This is a command injection vulnerability (CWE-78) in Firejail's `netfilter.c`, written in C. Attacker-controlled environment variables were passed unsanitized into a command string executed with elevated privileges, allowing shell metacharacters to inject arbitrary commands. The fix removes the unsafe `exec()`-with-shell pattern and replaces it with a direct, argument-array-based execution that bypasses shell interpretation entirely, eliminating the injection surface.

Vulnerability at a Glance

cweCWE-78
fixReplace shell-based exec() call with direct argument-array execution that does not invoke a shell interpreter
riskLocal attacker can execute arbitrary commands as root via shell metacharacter injection
languageC
root causeAttacker-controlled environment variable interpolated into a shell command string passed to exec()
vulnerabilityCommand Injection via environment variable in privileged exec() call

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

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.

Prevention and further reading

Related Articles

critical

webapp-testing Server Script: Shell Injection via Unquoted Command

A critical command injection vulnerability in a web server testing utility allowed attackers to execute arbitrary shell commands by injecting metacharacters into command-line arguments. The fix removes shell interpretation and safely tokenizes user-supplied commands using `shlex.split()`, while preserving support for legitimate directory-change workflows.

high

THiNX Device Platform: Command Injection via source.owner in Git Fetch

The THiNX device management platform fixed a critical vulnerability where the `source.owner` parameter flowed directly into shell commands during git repository operations. An attacker with source configuration access could inject arbitrary shell commands by providing malicious owner values containing metacharacters like `; curl`.

critical

backtest.js CLI args: Unvalidated --days and --start Options

The backtest.js command-line tool accepted --days and --start arguments without validation, creating a command injection vector when the script is invoked by upstream processes with untrusted input. The fix adds strict input validation: --days must be a positive integer ≤ 3650, and --start must match the YYYY-MM-DD date format.

critical

Tauri type_text() Command Injection via Control Characters in

The type_text() system command handler in Tauri accepted arbitrary text up to 2000 UTF-16 characters without validating content, allowing injection of control characters that SendInput's Unicode path interprets as real key presses. An attacker could inject newline, escape, or tab characters to trigger actions in the focused window beyond mere text typing.

high

runStreaming() Command Injection: Defense-in-Depth for Electron Child

An Electron application's `runStreaming()` utility accepted a command string and argument array without validating either, creating a latent command injection vector. The fix adds strict type checking and a whitelist regex that rejects shell metacharacters, bounding the failure mode even if caller input becomes attacker-influenced.

critical

docker_rpc.uc Command Injection: Unsanitized RPC Parameters

A critical command injection vulnerability in the Docker RPC handler allowed authenticated attackers to execute arbitrary system commands by injecting shell metacharacters into container ID, port, user ID, or command parameters. The fix validates all user-supplied inputs against strict whitelist patterns before interpolating them into shell commands.