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
•Technically reviewed by Anupam Mediratta•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

critical

Lampa Desktop Auto-Update Heuristic Bypass: Execution of Unverified

Lampa Desktop's auto-update mechanism downloaded JavaScript and CSS from `raw.githubusercontent.com` using only heuristic validation—file size thresholds and string pattern matching—that attackers could trivially satisfy. The fix introduces cryptographic integrity verification by cross-referencing Git blob hashes from the GitHub Contents API, ensuring downloaded code matches the repository's authoritative state before execution.

high

adm-zip 0.6.0 Preserves SUID Bits From ZIPs: CVE-2026-102282

The `adm-zip` dependency resolved to 0.6.0 in this project's dependency tree, a version affected by CVE-2026-102282: during extraction it applies the Unix permission bits stored in each ZIP entry's external file attributes verbatim, including the setuid (`04000`), setgid (`02000`), and sticky bits. An attacker who controls an archive passed to `extractAllTo()` or `extractEntryTo()` can therefore have the extractor create a setuid binary owned by whatever user the extraction process runs as. The

high

requestInput() Type Confusion: NaN and Object Bypass in JavaScript

The `requestInput()` utility function lacked validation on its `type` parameter and failed to handle `NaN` results from float conversions, creating a type confusion weakness. An attacker could supply malformed inputs that propagate unhandled `NaN` values or unexpected object types through the type system. The fix adds explicit guards against `NaN` type parameters and rejects non-primitive type values.

critical

No Rate Limit on /api/uploads/presign Enables DoS

The `/api/uploads/presign` endpoint accepted unlimited concurrent requests to generate storage presigned URLs, giving an attacker a free lever to exhaust storage-provider quotas and server resources. The fix adds an `express-rate-limit` middleware capping each client to 30 requests per minute on that route.

high

CVE-2026-54673: builder-util-runtime Leaks Auth Headers on Redirect

electron-updater and electron-builder rely on builder-util-runtime to fetch update manifests and artifacts over HTTP. A flaw in that shared HTTP executor allowed credential headers attached to the original update-feed request to be re-sent after a redirect, exposing them to any host the redirect pointed to. The project fixes this by upgrading builder-util-runtime to 9.7.0 and collapsing a duplicate, older copy of the package that electron-updater had pinned on its own.

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.