Affected Versions
| Affected | not applicable (first-party code) — commit prior to fix |
| Fixed in | commit implementing verifyChecksum() |
| Ecosystem | N/A |
| CVE / GHSA | not assigned |
| CWE | CWE-494 — Download of Code Without Integrity Check |
The Vulnerability Explained
The ensureTrivy() function was designed to automatically provision the Trivy container security scanner by downloading it from GitHub releases on demand. This convenience came with a critical gap: the downloaded binary was executed without any cryptographic proof of authenticity.
The vulnerable code fetched the release asset directly:
const r = await fetch(`https://github.com/aquasecurity/trivy/releases/download/v${version}/${asset}`, { redirect: 'follow', signal: AbortSignal.timeout(180000) });
if (!r.ok) throw new Error(`Downloading trivy failed (${r.status}).`);
fs.writeFileSync(tgz, Buffer.from(await r.arrayBuffer()));
The fs.writeFileSync(tgz, Buffer.from(await r.arrayBuffer())) line at position 94 committed the binary to disk immediately after the HTTP response arrived. No subsequent verification occurred before execution. The only "validation" in the original flow was checking HTTP status and magic bytes elsewhere—not cryptographic integrity.
An attacker positioned between the application and GitHub (via compromised DNS, rogue Wi-Fi, or BGP hijacking) could serve a malicious binary with a valid-looking filename. Since the code follows redirects and accepts any 200-series response, a compromised CDN edge or mirror could similarly substitute payloads. The malicious binary would then execute during subsequent vulnerability scans with the full privileges of the parent application—potentially accessing container registries, source code, and CI/CD credentials.
The attack is particularly insidious because Trivy itself is a security tool; operators inherently trust its output. A compromised scanner could falsify scan results to hide real vulnerabilities while exfiltrating the very artifacts it was meant to protect.
The Fix
The remediation introduces a dedicated verifyChecksum() function that validates downloaded binaries against SHA256 hashes published by Aqua Security in their official checksums files.
Before:
const r = await fetch(`https://github.com/aquasecurity/trivy/releases/download/v${version}/${asset}`, { redirect: 'follow', signal: AbortSignal.timeout(180000) });
if (!r.ok) throw new Error(`Downloading trivy failed (${r.status}).`);
fs.writeFileSync(tgz, Buffer.from(await r.arrayBuffer()));
After:
const r = await fetch(`https://github.com/aquasecurity/trivy/releases/download/v${version}/${asset}`, { redirect: 'follow', signal: AbortSignal.timeout(180000) });
if (!r.ok) throw new Error(`Downloading trivy failed (${r.status}).`);
const data = Buffer.from(await r.arrayBuffer());
await verifyChecksum(version, asset, data);
fs.writeFileSync(tgz, data);
The new verifyChecksum() implementation:
async function verifyChecksum(version, asset, data) {
const r = await fetch(`https://github.com/aquasecurity/trivy/releases/download/v${version}/trivy_${version}_checksums.txt`, { redirect: 'follow', signal: AbortSignal.timeout(30000) });
if (!r.ok) throw new Error(`Downloading trivy checksums failed (${r.status}).`);
const line = (await r.text()).split('\n').find((l) => l.trim().endsWith(asset));
const expected = (line || '').trim().split(/\s+/)[0];
if (!expected) throw new Error(`No checksum found for ${asset}.`);
const actual = createHash('sha256').update(data).digest('hex');
if (actual !== expected) throw new Error('trivy download failed checksum verification.');
}
This change enforces a verification gate: the binary is held in memory as data, validated against the checksums file, and only then written to tgz if the hashes match. The separation of download, verification, and persistence ensures that no unverified byte reaches the filesystem.
The checksums file is fetched independently with its own timeout, and the parsing logic specifically matches the asset filename against the end of each line to handle the standard sha256sum output format. Missing checksums or hash mismatches both result in explicit, actionable error messages rather than silent failures.
Key Takeaways
-
Never persist executable code before cryptographic verification: The original code wrote directly to disk; the fix buffers in memory, validates, then persists only on success.
-
Trust but verify—even for security tools: Trivy scans for vulnerabilities, yet the tool itself needed protection. Supply-chain security applies to all dependencies, especially those with privileged access.
-
Checksum files are infrastructure, not optional metadata: Aqua Security publishes
trivy_${version}_checksums.txtas a standard release artifact. Treating these as mandatory rather than advisory closes a significant attack window. -
Separate timeouts for different risk profiles: The fix uses 30 seconds for checksums (small, critical-path) versus 3 minutes for binaries (large, tolerant of delay), reflecting operational realities without compromising security.
-
Filename-bound verification prevents substitution attacks: By matching
assetagainst the end of checksum lines, the code ensures the hash corresponds to the specific file requested, not merely any file in the release.
How Orbis AppSec Detected This
Source: The version parameter derived from latestVersion() and the hardcoded TRIVY_FALLBACK_VERSION constant, combined with the configurable asset name constructed from platform detection.
Sink: The fs.writeFileSync(tgz, Buffer.from(await r.arrayBuffer())) call that persists downloaded bytes to a path later executed via execFileAsync().
Missing control: No verification of cryptographic integrity between download completion and filesystem write. The code validated HTTP transport success and magic bytes elsewhere, but not the authenticity of the payload itself.
CWE: CWE-494 — Download of Code Without Integrity Check
Fix: Introduced verifyChecksum() to fetch upstream SHA256 checksums and validate in-memory before persistence, with explicit failure on missing or mismatched hashes.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
The ensureTrivy() vulnerability demonstrates how convenience features—automatic tool provisioning—can introduce critical supply-chain risks when cryptographic verification is omitted. The fix's pattern of buffer-then-validate-then-persist is applicable to any code that downloads executables, plugins, or configuration from remote sources. For operators of this code, the immediate priority is applying the verification logic; for developers building similar automation, the lesson is to treat integrity checks as non-negotiable infrastructure, not optional enhancements.