Skip to content

Commit 2eacc2b

Browse files
authored
fix: should match protocol-relative url of public path and tag src in SRI plugin (#12265)
fix: should match relative protocol of public path and tag src
1 parent a082779 commit 2eacc2b

File tree

8 files changed

+222
-14
lines changed

8 files changed

+222
-14
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/rspack_plugin_sri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ cow-utils = { workspace = true }
1313
derive_more = { workspace = true, features = ["debug"] }
1414
futures = { workspace = true }
1515
indexmap = { workspace = true }
16+
once_cell = { workspace = true }
1617
pathdiff = { workspace = true }
1718
rayon = { workspace = true }
1819
regex = { workspace = true }

crates/rspack_plugin_sri/src/html.rs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use std::sync::Arc;
22

33
use futures::future::join_all;
4+
use once_cell::sync::Lazy;
5+
use regex::Regex;
46
use rspack_error::{Result, ToStringResultToRspackResultExt};
57
use rspack_hook::plugin_hook;
68
use rspack_paths::Utf8Path;
@@ -13,6 +15,9 @@ use rustc_hash::FxHashMap as HashMap;
1315
use tokio::sync::RwLock;
1416
use url::Url;
1517

18+
static HTTP_PROTOCOL_REGEX: Lazy<Regex> =
19+
Lazy::new(|| Regex::new(r"^https?:").expect("Invalid regex"));
20+
1621
use crate::{
1722
SRICompilationContext, SubresourceIntegrityHashFunction, SubresourceIntegrityPlugin,
1823
SubresourceIntegrityPluginInner, config::ArcFs, integrity::compute_integrity,
@@ -172,14 +177,30 @@ async fn process_tag(
172177
};
173178

174179
// A tag which is not generated by chunks should be skipped
175-
if let Ok(url) = Url::parse(&tag_src)
176-
&& (url.scheme() == "http" || url.scheme() == "https")
177-
&& (public_path.is_empty() || !tag_src.starts_with(public_path))
178-
{
179-
return Ok(None);
180-
}
180+
let src = if match Url::parse(&tag_src) {
181+
Ok(url) => url.scheme() == "http" || url.scheme() == "https",
182+
Err(_) => tag_src.starts_with("//"),
183+
} {
184+
if public_path.is_empty() {
185+
return Ok(None);
186+
}
187+
let protocol_relative_public_path = HTTP_PROTOCOL_REGEX.replace(public_path, "").to_string();
188+
let protocol_relative_tag_src = HTTP_PROTOCOL_REGEX.replace(&tag_src, "").to_string();
189+
if protocol_relative_tag_src.starts_with(&protocol_relative_public_path) {
190+
let tag_src_with_scheme = format!("http:{}", protocol_relative_tag_src);
191+
let public_path_with_scheme = if protocol_relative_public_path.starts_with("//") {
192+
format!("http:{}", protocol_relative_public_path)
193+
} else {
194+
protocol_relative_public_path.to_string()
195+
};
196+
get_asset_path(&tag_src_with_scheme, &public_path_with_scheme)
197+
} else {
198+
return Ok(None);
199+
}
200+
} else {
201+
get_asset_path(&tag_src, public_path)
202+
};
181203

182-
let src = get_asset_path(&tag_src, public_path);
183204
if let Some(integrity) =
184205
get_integrity_checksum_for_asset(&src, integrities, normalized_integrities).await
185206
{

packages/rspack/src/builtin-plugin/SubresourceIntegrityPlugin.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { create } from "./base";
1414
const PLUGIN_NAME = "SubresourceIntegrityPlugin";
1515
const NATIVE_HTML_PLUGIN = "HtmlRspackPlugin";
1616

17+
const HTTP_PROTOCOL_REGEX = /^https?:/;
18+
1719
type HtmlTagObject = {
1820
attributes: {
1921
[attributeName: string]: string | boolean | null | undefined;
@@ -190,19 +192,40 @@ export class SubresourceIntegrityPlugin extends NativeSubresourceIntegrityPlugin
190192
return;
191193
}
192194

195+
let isUrlSrc = false;
193196
try {
194197
const url = new URL(tagSrc);
195-
if (
196-
(url.protocol === "http:" || url.protocol === "https:") &&
197-
(!publicPath || !tagSrc.startsWith(publicPath))
198-
) {
198+
isUrlSrc = url.protocol === "http:" || url.protocol === "https:";
199+
} catch (_) {
200+
isUrlSrc = tagSrc.startsWith("//");
201+
}
202+
203+
let src = "";
204+
if (isUrlSrc) {
205+
if (!publicPath) {
199206
return;
200207
}
201-
} catch (_) {
202-
// do nothing
208+
const protocolRelativePublicPath = publicPath.replace(
209+
HTTP_PROTOCOL_REGEX,
210+
""
211+
);
212+
const protocolRelativeTagSrc = tagSrc.replace(HTTP_PROTOCOL_REGEX, "");
213+
if (protocolRelativeTagSrc.startsWith(protocolRelativePublicPath)) {
214+
const tagSrcWithScheme = `http:${protocolRelativeTagSrc}`;
215+
const publicPathWithScheme = protocolRelativePublicPath.startsWith("//")
216+
? `http:${protocolRelativePublicPath}`
217+
: protocolRelativePublicPath;
218+
src = relative(
219+
publicPathWithScheme,
220+
decodeURIComponent(tagSrcWithScheme)
221+
);
222+
} else {
223+
return;
224+
}
225+
} else {
226+
src = relative(publicPath, decodeURIComponent(tagSrc));
203227
}
204228

205-
const src = relative(publicPath, decodeURIComponent(tagSrc));
206229
tag.attributes.integrity =
207230
this.getIntegrityChecksumForAsset(src) ||
208231
computeIntegrity(

tests/rspack-test/configCases/sri/remote-src-protocol/chunk.js

Whitespace-only changes.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import(/* webpackChunkName: "chunk" */ "./chunk.js");
2+
it("should compile", () => { });
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
const { experiments, HtmlRspackPlugin } = require("@rspack/core");
2+
const HtmlWebpackPlugin = require("html-webpack-plugin");
3+
const fs = require("fs");
4+
const path = require("path");
5+
6+
/** @type {import("@rspack/core").Configuration} */
7+
module.exports = (_, { testPath }) => ([{
8+
target: "web",
9+
output: {
10+
publicPath: "http://localhost:3000/",
11+
chunkFilename: "[name].0.js",
12+
crossOriginLoading: "anonymous",
13+
},
14+
plugins: [
15+
new experiments.SubresourceIntegrityPlugin(),
16+
new HtmlRspackPlugin({
17+
filename: "index.html",
18+
}),
19+
{
20+
apply(compiler) {
21+
compiler.hooks.compilation.tap('TestPlugin', (compilation) => {
22+
HtmlRspackPlugin.getCompilationHooks(compilation).beforeAssetTagGeneration.tap('SubresourceIntegrityPlugin', (data) => {
23+
data.assets.js.push("//localhost:3000/chunk.0.js");
24+
data.assets.js.push("http://localhost:3000/chunk.0.js");
25+
data.assets.js.push("//rspack.dev/chunk.0.js");
26+
data.assets.js.push("http://rspack.dev/chunk.0.js");
27+
});
28+
});
29+
}
30+
},
31+
{
32+
apply(compiler) {
33+
compiler.hooks.done.tap('TestPlugin', () => {
34+
const htmlContent = fs.readFileSync(path.resolve(testPath, "index.html"), "utf-8");
35+
expect(htmlContent).toMatch(/<script crossorigin defer integrity=".+" src="\/\/localhost:3000\/chunk\.0\.js">/);
36+
expect(htmlContent).toMatch(/<script crossorigin defer integrity=".+" src="http:\/\/localhost:3000\/chunk\.0\.js">/);
37+
expect(htmlContent).toMatch(/<script defer src="\/\/rspack.dev\/chunk\.0\.js">/);
38+
expect(htmlContent).toMatch(/<script defer src="http:\/\/rspack.dev\/chunk\.0\.js">/);
39+
});
40+
}
41+
}
42+
],
43+
}, {
44+
target: "web",
45+
output: {
46+
publicPath: "http://localhost:3000/",
47+
chunkFilename: "[name].1.js",
48+
crossOriginLoading: "anonymous",
49+
},
50+
plugins: [
51+
new experiments.SubresourceIntegrityPlugin({
52+
htmlPlugin: require.resolve("html-webpack-plugin"),
53+
}),
54+
new HtmlWebpackPlugin({
55+
filename: "index1.html",
56+
}),
57+
{
58+
apply(compiler) {
59+
compiler.hooks.compilation.tap('TestPlugin', (compilation) => {
60+
HtmlWebpackPlugin.getCompilationHooks(compilation).beforeAssetTagGeneration.tap('SubresourceIntegrityPlugin', (data) => {
61+
data.assets.js.push("//localhost:3000/chunk.1.js");
62+
data.assets.js.push("http://localhost:3000/chunk.1.js");
63+
data.assets.js.push("//rspack.dev/chunk.1.js");
64+
data.assets.js.push("http://rspack.dev/chunk.1.js");
65+
});
66+
});
67+
}
68+
},
69+
{
70+
apply(compiler) {
71+
compiler.hooks.done.tap('TestPlugin', () => {
72+
const htmlContent = fs.readFileSync(path.resolve(testPath, "index1.html"), "utf-8");
73+
expect(htmlContent).toMatch(/<script defer="defer" src="\/\/localhost:3000\/chunk\.1\.js" integrity=".+" crossorigin="anonymous">/);
74+
expect(htmlContent).toMatch(/<script defer="defer" src="http:\/\/localhost:3000\/chunk\.1\.js" integrity=".+" crossorigin="anonymous">/);
75+
expect(htmlContent).toMatch(/<script defer="defer" src="\/\/rspack.dev\/chunk\.1\.js">/);
76+
expect(htmlContent).toMatch(/<script defer="defer" src="http:\/\/rspack.dev\/chunk\.1\.js">/);
77+
});
78+
}
79+
}
80+
],
81+
}, {
82+
target: "web",
83+
output: {
84+
publicPath: "//localhost:3000/",
85+
chunkFilename: "[name].2.js",
86+
crossOriginLoading: "anonymous",
87+
},
88+
plugins: [
89+
new experiments.SubresourceIntegrityPlugin(),
90+
new HtmlRspackPlugin({
91+
filename: "index2.html",
92+
}),
93+
{
94+
apply(compiler) {
95+
compiler.hooks.compilation.tap('TestPlugin', (compilation) => {
96+
HtmlRspackPlugin.getCompilationHooks(compilation).beforeAssetTagGeneration.tap('SubresourceIntegrityPlugin', (data) => {
97+
data.assets.js.push("//localhost:3000/chunk.2.js");
98+
data.assets.js.push("http://localhost:3000/chunk.2.js");
99+
data.assets.js.push("//rspack.dev/chunk.2.js");
100+
data.assets.js.push("http://rspack.dev/chunk.2.js");
101+
});
102+
});
103+
}
104+
},
105+
{
106+
apply(compiler) {
107+
compiler.hooks.done.tap('TestPlugin', () => {
108+
const htmlContent = fs.readFileSync(path.resolve(testPath, "index2.html"), "utf-8");
109+
expect(htmlContent).toMatch(/<script crossorigin defer integrity=".+" src="\/\/localhost:3000\/chunk\.2\.js">/);
110+
expect(htmlContent).toMatch(/<script crossorigin defer integrity=".+" src="http:\/\/localhost:3000\/chunk\.2\.js">/);
111+
expect(htmlContent).toMatch(/<script defer src="\/\/rspack.dev\/chunk\.2\.js">/);
112+
expect(htmlContent).toMatch(/<script defer src="http:\/\/rspack.dev\/chunk\.2\.js">/);
113+
});
114+
}
115+
}
116+
],
117+
}, {
118+
target: "web",
119+
output: {
120+
publicPath: "//localhost:3000/",
121+
chunkFilename: "[name].3.js",
122+
crossOriginLoading: "anonymous",
123+
},
124+
plugins: [
125+
new experiments.SubresourceIntegrityPlugin({
126+
htmlPlugin: require.resolve("html-webpack-plugin"),
127+
}),
128+
new HtmlWebpackPlugin({
129+
filename: "index3.html",
130+
}),
131+
{
132+
apply(compiler) {
133+
compiler.hooks.compilation.tap('TestPlugin', (compilation) => {
134+
HtmlWebpackPlugin.getCompilationHooks(compilation).beforeAssetTagGeneration.tap('SubresourceIntegrityPlugin', (data) => {
135+
data.assets.js.push("//localhost:3000/chunk.3.js");
136+
data.assets.js.push("http://localhost:3000/chunk.3.js");
137+
data.assets.js.push("//rspack.dev/chunk.3.js");
138+
data.assets.js.push("http://rspack.dev/chunk.3.js");
139+
});
140+
});
141+
}
142+
},
143+
{
144+
apply(compiler) {
145+
compiler.hooks.done.tap('TestPlugin', () => {
146+
const htmlContent = fs.readFileSync(path.resolve(testPath, "index3.html"), "utf-8");
147+
expect(htmlContent).toMatch(/<script defer="defer" src="\/\/localhost:3000\/chunk\.3\.js" integrity=".+" crossorigin="anonymous">/);
148+
expect(htmlContent).toMatch(/<script defer="defer" src="http:\/\/localhost:3000\/chunk\.3\.js" integrity=".+" crossorigin="anonymous">/);
149+
expect(htmlContent).toMatch(/<script defer="defer" src="\/\/rspack.dev\/chunk\.3\.js">/);
150+
expect(htmlContent).toMatch(/<script defer="defer" src="http:\/\/rspack.dev\/chunk\.3\.js">/);
151+
});
152+
}
153+
}
154+
],
155+
}]);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
findBundle() {
3+
return [];
4+
}
5+
};

0 commit comments

Comments
 (0)