Back to Blog
critical SEVERITY4 min read

ensureTrivy() CWE-494: Unverified Trivy Binary Download

The `ensureTrivy()` function fetched the Trivy vulnerability scanner from GitHub releases without verifying its integrity, exposing applications to supply-chain attacks. An attacker in a MITM position or a compromised CDN could substitute a malicious binary that executes with the application's privileges. The fix adds cryptographic verification using SHA256 checksums published alongside each release.

O
By Orbis AppSec
•Published September 30, 2026•Reviewed September 30, 2026

Answer Summary

The `ensureTrivy()` function in first-party code downloaded Trivy binaries from GitHub releases without integrity verification. An attacker with network interception capabilities could replace the legitimate scanner with malicious code that executes arbitrary commands with the application's privileges. The fix introduces `verifyChecksum()`, which validates downloaded binaries against SHA256 checksums published by Aqua Security. No CVE or GHSA has been assigned. CWE-494 (Download of Code Without Integrity Check).

Vulnerability at a Glance

cweCWE-494
fixSHA256 checksum verification against upstream checksums file
riskRemote code execution via malicious binary substitution
languageJavaScript (Node.js)
root causeMissing cryptographic verification of downloaded executable
vulnerabilityDownload of Code Without Integrity Check (CWE-494)

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.txt as 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 asset against 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.

Prevention and further reading

Frequently Asked Questions

Does the `verifyChecksum()` function validate GPG signatures or just SHA256 hashes?

The fix implements SHA256 checksum verification only. GPG signature validation is not implemented, though the checksums file itself is fetched over HTTPS with redirect following and a 30-second timeout.

What happens if the checksums file is available but doesn't contain an entry for the specific asset being downloaded?

The `verifyChecksum()` function throws an explicit error with the message "No checksum found for ${asset}", preventing execution of unverified binaries.

Is the 180000ms timeout for the binary download different from the checksum verification timeout?

Yes. The binary download uses a 180000ms (3 minute) timeout via `AbortSignal.timeout(180000)`, while the checksums fetch uses a shorter 30000ms (30 second) timeout, reflecting the expected size difference between a compressed binary and a text file of hashes.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #64

Related Articles

high

image-size 1.2.1 DoS: Zero-Valued Dimensions in Image Buffer Parser

A high-severity denial-of-service vulnerability in image-size 1.2.1 allows attackers to crash Node.js services using malicious image buffers with zero-valued dimensions. The fix removes the vulnerable `queue` dependency and tightens dimension validation in version 2.0.3.

high

linkify-it 5.0.1 mailto: Link Parsing Causes DoS

linkify-it versions up to 5.0.1 can be forced into excessive processing time when autolinking a specially crafted mailto: link, allowing a remote attacker to degrade or stall the parsing thread. Upgrading to linkify-it 5.0.2 closes the issue; any application that runs linkify-it (directly or via markdown-it) against untrusted text should update immediately.

critical

Slim CLI Unverified Remote Fetch in Version Check

The Slim CLI's version check command fetched remote package metadata without integrity verification, enabling attackers to serve malicious responses through repository hijacking or man-in-the-middle attacks. The fix adds input validation, HTTP status checking, and response schema verification to ensure only legitimate version data is processed.

high

@xmldom/xmldom 0.9.10 ReDoS: CVE-2026-83606 in PI Parsing

The dependency tree pinned `@xmldom/xmldom` at 0.9.10, a version vulnerable to CVE-2026-83606: a regular expression in the processing-instruction (`<?target data?>`) parsing path backtracks catastrophically on crafted input, so a single XML document can consume CPU for seconds or minutes. Because Node.js runs application code on one thread, that stall blocks every other request in flight. The lockfile was updated to resolve `@xmldom/xmldom` 0.9.12, which carries the fix.

high

Unbounded Map in createLoginRateLimiter Exhausts API Memory

The console API's `createLoginRateLimiter` and `createMutationRateLimiter` stored one `Map` entry per client key with no upper bound and no expiry sweep, so an attacker rotating source addresses or identifiers could grow those maps until the Node process hit an out-of-memory crash. The fix introduces a `maxTrackedKeys` option (default 5000), a `trackedEntryLimit` sanitizer, and an `evictOldestIfFull` helper that drops the oldest tracked key while protecting the shared global counter. The rate li

critical

path.resolve() Path Traversal in Node CLI's Dynamic import()

A Node.js CLI script for validating expression definitions took a file path from `process.argv[2]`, resolved it with `path.resolve()`, and passed the result straight into a dynamic `import()` — with no check that the resolved path stayed inside the working directory. An attacker (or a malicious skill/plugin invocation) could supply traversal sequences to load and execute arbitrary `.js` files from anywhere on disk. The fix adds a boundary check with `path.relative()` and an extension allowlist b