From 0c34f4886554dc4715afb96f2ecc2cf4e074c4a4 Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Fri, 24 Jul 2026 15:18:25 -0400 Subject: [PATCH] fix(vendor): accept sha1-only SRI as last-resort verification in registry_fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit npm-era lockfile entries carry ONLY `integrity sha1-…` (yarn classic writes them for packages published before sha512 metadata existed). verify_sri ranked sha512/384/256 and refused everything else, so every legacy package became unvendorable whenever the prebuilt-artifact service missed and the local-build fallback fetched the pristine tarball: `vendor_fetch_failed: no usable hash in SRI sha1-…`. A clean vendored run on a real 2019-era monorepo failed 14 of 62 packages this way (ansi-regex@3.0.0, decode-uri-component@0.2.0, semver@5.3.0, …). sha1 now ranks BELOW sha256/384/512 (never preferred, only used when it is all the lockfile records) — the same trust the package manager itself enforces for those entries, and the same decision the LockIntegrity::Sha1Hex bare-hex arm already made in this file. New unit test pins: sha1-only verifies, sha1 mismatch refuses (not fail-open), sha1 never outranks a stronger hash in multi-hash SRIs, md5 still refused. Validated end-to-end on the real monorepo: all 14 previously-failing packages vendor, scan exits success, yarn 1 installs 62/62 vendored packages byte-identical to their patched blobs. Co-Authored-By: Claude Fable 5 --- .../src/patch/vendor/registry_fetch.rs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/crates/socket-patch-core/src/patch/vendor/registry_fetch.rs b/crates/socket-patch-core/src/patch/vendor/registry_fetch.rs index 851d3ac..0f0c92a 100644 --- a/crates/socket-patch-core/src/patch/vendor/registry_fetch.rs +++ b/crates/socket-patch-core/src/patch/vendor/registry_fetch.rs @@ -756,6 +756,14 @@ fn verify_integrity(bytes: &[u8], integrity: &LockIntegrity) -> Result<(), Fetch /// SRI verification: pick the strongest hash of a (possibly multi-hash, /// whitespace-separated) SRI string and compare base64 digests. +/// +/// `sha1` is accepted as a LAST resort (never preferred over sha256+): it is +/// the only integrity npm-era lockfile entries carry (yarn classic writes +/// `integrity sha1-…` for them), and it is the exact guarantee the package +/// manager itself enforces for those entries — refusing it would make every +/// legacy package unvendorable whenever the prebuilt-artifact service misses +/// (the 2026-07 strapi clean-run regression). The bare-hex twin of this trust +/// decision already lives in the `LockIntegrity::Sha1Hex` arm above. fn verify_sri(bytes: &[u8], sri: &str) -> Result<(), String> { let mut best: Option<(u8, &str, &str)> = None; for token in sri.split_whitespace() { @@ -766,6 +774,7 @@ fn verify_sri(bytes: &[u8], sri: &str) -> Result<(), String> { "sha512" => 3, "sha384" => 2, "sha256" => 1, + "sha1" => 0, _ => continue, }; if best.map(|(r, _, _)| rank > r).unwrap_or(true) { @@ -779,6 +788,7 @@ fn verify_sri(bytes: &[u8], sri: &str) -> Result<(), String> { let actual = match algo { "sha512" => b64.encode(Sha512::digest(bytes)), "sha384" => b64.encode(Sha384::digest(bytes)), + "sha1" => b64.encode(Sha1::digest(bytes)), _ => b64.encode(Sha256::digest(bytes)), }; if actual == expect { @@ -990,6 +1000,30 @@ mod tests { ); } + #[test] + fn sri_sha1_is_accepted_as_last_resort() { + use base64::Engine as _; + let bytes = b"hello"; + let sha1_b64 = base64::engine::general_purpose::STANDARD.encode(Sha1::digest(bytes)); + // npm-era lockfile entries carry ONLY `sha1-…` (the strapi clean-run + // regression: `no usable hash in SRI`); it must verify… + assert!( + verify_sri(bytes, &format!("sha1-{sha1_b64}")).is_ok(), + "sha1-only SRI must be usable" + ); + // …and still be a REAL check, not a fail-open. + let wrong = base64::engine::general_purpose::STANDARD.encode(Sha1::digest(b"other")); + assert!( + verify_sri(bytes, &format!("sha1-{wrong}")).is_err(), + "sha1 mismatch must refuse" + ); + // sha1 never outranks a stronger hash: a correct sha1 alongside a + // wrong sha512 fails (strongest wins), the reverse passes. + let sha512_good = sri_of(bytes); + assert!(verify_sri(bytes, &format!("sha1-{sha1_b64} sha512-WRONG=")).is_err()); + assert!(verify_sri(bytes, &format!("sha1-{wrong} {sha512_good}")).is_ok()); + } + #[tokio::test] async fn fetch_verifies_sri_and_extracts_with_modes() { let tgz = make_tgz(&[