From fea16d3dfa5a0c32888643e1a866f2f4e134cc81 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 23 Sep 2026 22:17:02 +0800 Subject: [PATCH] fix(sri): preserve supported hashes with malformed padding Keep supported hash metadata through verification so misplaced or excess base64 padding cannot disable integrity checks or select a weaker algorithm. Accept matching noncanonical base64 without changing encoded metadata used by CSP and preload consumption. Cover script, preload, CSP, and Window/Worker fetch consumers. Validate with cargo fmt, workspace Clippy, and all 19,886 nextest tests, plus browser comparisons and SRI/preload WPT regressions. --- .../tests/fixtures/preload-consumption.js | 2 +- .../tests/scripts/preload_consumption.rs | 2 +- .../src/script_vm/tests/fetch_integrity.rs | 14 ++++++++++--- moli-renderer-v8/src/subresource_integrity.rs | 21 +++++++++++++++++++ 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/moli-core/tests/fixtures/preload-consumption.js b/moli-core/tests/fixtures/preload-consumption.js index 1ade387609..b0b91b4f2d 100644 --- a/moli-core/tests/fixtures/preload-consumption.js +++ b/moli-core/tests/fixtures/preload-consumption.js @@ -28,7 +28,7 @@ async function () { const integrity = await (async () => {return (async () => { const rows=[]; for (const cache of ['max-age=60','no-store']) for (const phase of ['pending','complete']) - for (const [name,preloadIntegrity,consumerIntegrity,preloadEvent,consumerEvent,requests] of [["good-empty", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ", "", "load", "load", 1], ["bad-empty", "sha384-AAAA", "", "error", "error", 1], ["bad-same", "sha384-AAAA", "sha384-AAAA", "error", "error", 1], ["bad-good", "sha384-AAAA", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ", "error", "load", 2], ["good-bad", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ", "sha384-AAAA", "load", "error", 2], ["good-options", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ?ignored=option", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ", "load", "load", 1], ["good-different-algorithm", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ", "sha256-cRlncoZ/tNV1cbKTR2cXiKEwVFkjvOz4kToIRLKBiSE=", "load", "load", 2], ["empty-good", "", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ", "load", "load", 2]]) { + for (const [name,preloadIntegrity,consumerIntegrity,preloadEvent,consumerEvent,requests] of [["good-empty", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ", "", "load", "load", 1], ["bad-empty", "sha384-AAAA", "", "error", "error", 1], ["bad-same", "sha384-AAAA", "sha384-AAAA", "error", "error", 1], ["bad-good", "sha384-AAAA", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ", "error", "load", 2], ["good-bad", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ", "sha384-AAAA", "load", "error", 2], ["good-options", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ?ignored=option", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ", "load", "load", 1], ["good-different-algorithm", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ", "sha256-cRlncoZ/tNV1cbKTR2cXiKEwVFkjvOz4kToIRLKBiSE=", "load", "load", 2], ["empty-good", "", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ", "load", "load", 2], ["malformed-empty", "sha384-AAAA===", "", "error", "error", 1], ["good-malformed", "sha384-l+rtjjQWlmlLFL2PC8iIPwnXbiW45bZ6LU1ERjlAxYKUFswREZzY3pPyoT0RsvxJ", "sha384-A=AAA", "load", "error", 2]]) { const token='sri-'+rows.length,url=new URL('/probe-asset',location.href); url.searchParams.set('token',token);url.searchParams.set('as','script');url.searchParams.set('cache',cache); const link=document.createElement('link');Object.assign(link,{rel:'preload',as:'script',href:url.href,integrity:preloadIntegrity}); diff --git a/moli-core/tests/scripts/preload_consumption.rs b/moli-core/tests/scripts/preload_consumption.rs index 95c5f442d6..869633ffdd 100644 --- a/moli-core/tests/scripts/preload_consumption.rs +++ b/moli-core/tests/scripts/preload_consumption.rs @@ -196,7 +196,7 @@ async fn document_preloads_supply_pending_and_completed_consumers_including_fail .context("preload consumption results")?, )?; assert_eq!(value["basic"]["rows"].as_array().unwrap().len(), 24); - assert_eq!(value["integrity"]["rows"].as_array().unwrap().len(), 32); + assert_eq!(value["integrity"]["rows"].as_array().unwrap().len(), 40); assert_eq!(value["basic"]["failures"], serde_json::json!([])); assert_eq!(value["integrity"]["failures"], serde_json::json!([])); assert_eq!(value["basic"]["executions"], 12); diff --git a/moli-renderer-v8/src/script_vm/tests/fetch_integrity.rs b/moli-renderer-v8/src/script_vm/tests/fetch_integrity.rs index c6ca60f6e9..be2e126445 100644 --- a/moli-renderer-v8/src/script_vm/tests/fetch_integrity.rs +++ b/moli-renderer-v8/src/script_vm/tests/fetch_integrity.rs @@ -58,7 +58,7 @@ async fn fetch_integrity_validates_local_bytes_in_window_and_worker() { async fn fetch_integrity_validates_network_responses_in_window_and_worker() { for worker in [false, true] { let server = - StaticHttpServer::spawn_with_bodies(vec!["hello integrity".to_owned(); 8]).await; + StaticHttpServer::spawn_with_bodies(vec!["hello integrity".to_owned(); 16]).await; let expression = format!( r#"(async () => {{ const checks = []; @@ -74,6 +74,14 @@ async fn fetch_integrity_validates_network_responses_in_window_and_worker() { ['whitespace', ' ', 'GET', true], ['no-integrity', '', 'GET', true], ['null-body', 'sha1-ignored', 'HEAD', false], + ['excess-padding-mismatch', 'sha384-AAAA===', 'GET', false], + ['interior-padding-mismatch', 'sha384-A=AAA', 'GET', false], + ['padding-only-mismatch', 'sha384-====', 'GET', false], + ['malformed-stronger-hash', valid + ' sha512-AAAA===', 'GET', false], + ['excess-padding-match', valid + '====', 'GET', true], + ['noncanonical-padding-bits', valid.replace('6A=', '6B='), 'GET', true], + ['non-base64-ignored', 'sha384-***', 'GET', true], + ['malformed-and-matching', 'sha256-A=AAA ' + valid, 'GET', true], ]) {{ try {{ const response = await fetch({} + label, {{integrity, method}}); @@ -85,7 +93,7 @@ async fn fetch_integrity_validates_network_responses_in_window_and_worker() { }})()"#, serde_json::to_string(server.base_url().as_str()).unwrap(), ); - run_integrity_probe(server.base_url().as_str(), worker, &expression, 13).await; - assert_eq!(server.finish().await.len(), 8); + run_integrity_probe(server.base_url().as_str(), worker, &expression, 25).await; + assert_eq!(server.finish().await.len(), 16); } } diff --git a/moli-renderer-v8/src/subresource_integrity.rs b/moli-renderer-v8/src/subresource_integrity.rs index e34466667c..8b5159026e 100644 --- a/moli-renderer-v8/src/subresource_integrity.rs +++ b/moli-renderer-v8/src/subresource_integrity.rs @@ -175,6 +175,18 @@ mod tests { ); } } + assert!(!integrity_metadata_allows_preload_consumption( + &valid, + Some(malformed) + )); + assert!(integrity_metadata_allows_preload_consumption( + malformed, + Some(malformed) + )); + assert!(!integrity_metadata_allows_preload_consumption( + "", + Some(malformed) + )); } } @@ -183,6 +195,7 @@ mod tests { let body = b"noncanonical integrity"; let digest = base64::engine::general_purpose::STANDARD .encode(DigestAlgorithm::Sha256.digest_bytes(body)); + let metadata = format!("sha256-{digest}"); for padding in ["", "=", "===", "======="] { let padded = format!("sha256-{}{padding}", digest.trim_end_matches('=')); assert!(response_matches_subresource_integrity_metadata( @@ -195,6 +208,10 @@ mod tests { Some(&padded), false )); + assert_eq!( + integrity_metadata_allows_preload_consumption(&metadata, Some(&padded)), + metadata == padded + ); } // Only the top four bits of the final SHA-256 base64 symbol carry data. let mut noncanonical = digest.trim_end_matches('=').as_bytes().to_vec(); @@ -207,6 +224,10 @@ mod tests { Some(&noncanonical), true )); + assert!(!integrity_metadata_allows_preload_consumption( + &metadata, + Some(&noncanonical) + )); } #[test]