Skip to content

Commit df073c5

Browse files
Merge commit from fork
* test: add regression tests for CORP header on cross-origin responses Ref: https://github.com/webpack/webpack-dev-server/security/advisories/GHSA-79cf-xcqc-c78w * fix: set Cross-Origin-Resource-Policy header to prevent source code theft over HTTP Ref: https://github.com/webpack/webpack-dev-server/security/advisories/GHSA-79cf-xcqc-c78w * fix: update CORS handling to differentiate between wildcard and specific-origin headers --------- Co-authored-by: Sebastian Beltran <bjohansebas@gmail.com>
1 parent b550a70 commit df073c5

File tree

3 files changed

+215
-0
lines changed

3 files changed

+215
-0
lines changed

lib/Server.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2019,6 +2019,14 @@ class Server {
20192019
return;
20202020
}
20212021

2022+
// Block cross-origin resource loading when Sec-Fetch-* headers are absent (HTTP origins)
2023+
if (
2024+
this.options.allowedHosts !== "all" &&
2025+
!this.isUserCORSWildcardEnabled()
2026+
) {
2027+
res.setHeader("Cross-Origin-Resource-Policy", "same-origin");
2028+
}
2029+
20222030
next();
20232031
},
20242032
});
@@ -3184,6 +3192,53 @@ class Server {
31843192
return false;
31853193
}
31863194

3195+
/**
3196+
* @private
3197+
* @returns {boolean} true when the user has configured a wildcard
3198+
* Access-Control-Allow-Origin header (opting into fully open cross-origin access)
3199+
*/
3200+
isUserCORSWildcardEnabled() {
3201+
const { headers } = this.options;
3202+
3203+
if (!headers) {
3204+
return false;
3205+
}
3206+
3207+
if (typeof headers === "function") {
3208+
return false;
3209+
}
3210+
3211+
/**
3212+
* @param {string | string[]} value header value
3213+
* @returns {boolean} true when value is the "*" wildcard
3214+
*/
3215+
const isWildcard = (value) => {
3216+
if (typeof value === "string") {
3217+
return value.trim() === "*";
3218+
}
3219+
3220+
if (Array.isArray(value)) {
3221+
return value.length === 1 && isWildcard(value[0]);
3222+
}
3223+
3224+
return false;
3225+
};
3226+
3227+
if (Array.isArray(headers)) {
3228+
return headers.some(
3229+
(header) =>
3230+
header.key.toLowerCase() === "access-control-allow-origin" &&
3231+
isWildcard(header.value),
3232+
);
3233+
}
3234+
3235+
return Object.entries(headers).some(
3236+
([key, value]) =>
3237+
key.toLowerCase() === "access-control-allow-origin" &&
3238+
isWildcard(value),
3239+
);
3240+
}
3241+
31873242
/**
31883243
* @private
31893244
* @param {{ [key: string]: string | undefined }} headers headers

test/e2e/cross-origin-request.test.js

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,157 @@ describe("cross-origin requests", () => {
218218
}
219219
});
220220
});
221+
222+
// @see https://github.com/webpack/webpack-dev-server/security/advisories/GHSA-79cf-xcqc-c78w
223+
describe("cross-origin resource policy header", () => {
224+
const devServerPort = port1;
225+
226+
let server;
227+
228+
afterEach(async () => {
229+
if (server) {
230+
await server.stop();
231+
// Allow the port to be fully released before the next test
232+
await new Promise((resolve) => {
233+
setTimeout(resolve, 100);
234+
});
235+
server = null;
236+
}
237+
});
238+
239+
function request(url, headers = {}) {
240+
const http = require("node:http");
241+
242+
return new Promise((resolve, reject) => {
243+
const req = http.get(url, { headers }, (res) => {
244+
let body = "";
245+
res.on("data", (chunk) => {
246+
body += chunk;
247+
});
248+
res.on("end", () => {
249+
resolve({ status: res.statusCode, headers: res.headers, body });
250+
});
251+
});
252+
req.on("error", reject);
253+
});
254+
}
255+
256+
it("should set Cross-Origin-Resource-Policy: same-origin by default", async () => {
257+
const compiler = webpack(config);
258+
server = new Server(
259+
{ port: devServerPort, allowedHosts: "auto" },
260+
compiler,
261+
);
262+
263+
await server.start();
264+
265+
const res = await request(`http://localhost:${devServerPort}/main.js`);
266+
267+
expect(res.headers["cross-origin-resource-policy"]).toBe("same-origin");
268+
});
269+
270+
it("should NOT set CORP header when allowedHosts is 'all'", async () => {
271+
const compiler = webpack(config);
272+
server = new Server({ port: devServerPort, allowedHosts: "all" }, compiler);
273+
274+
await server.start();
275+
276+
const res = await request(`http://localhost:${devServerPort}/main.js`);
277+
278+
expect(res.headers["cross-origin-resource-policy"]).toBeUndefined();
279+
});
280+
281+
it("should NOT set CORP header when user configures wildcard CORS", async () => {
282+
const compiler = webpack(config);
283+
server = new Server(
284+
{
285+
port: devServerPort,
286+
allowedHosts: "auto",
287+
headers: { "Access-Control-Allow-Origin": "*" },
288+
},
289+
compiler,
290+
);
291+
292+
await server.start();
293+
294+
const res = await request(`http://localhost:${devServerPort}/main.js`);
295+
296+
expect(res.headers["cross-origin-resource-policy"]).toBeUndefined();
297+
});
298+
299+
it("should set CORP header when user configures a specific-origin Access-Control-Allow-Origin (no-cors embedding is not governed by CORS)", async () => {
300+
const compiler = webpack(config);
301+
server = new Server(
302+
{
303+
port: devServerPort,
304+
allowedHosts: "auto",
305+
headers: {
306+
"Access-Control-Allow-Origin": "http://foo.example.com",
307+
},
308+
},
309+
compiler,
310+
);
311+
312+
await server.start();
313+
314+
const res = await request(`http://localhost:${devServerPort}/main.js`);
315+
316+
expect(res.headers["cross-origin-resource-policy"]).toBe("same-origin");
317+
});
318+
319+
it("should set CORP header when user configures Access-Control-Allow-Origin via headers array with a specific origin", async () => {
320+
const compiler = webpack(config);
321+
server = new Server(
322+
{
323+
port: devServerPort,
324+
allowedHosts: "auto",
325+
headers: [
326+
{
327+
key: "Access-Control-Allow-Origin",
328+
value: "http://foo.example.com",
329+
},
330+
],
331+
},
332+
compiler,
333+
);
334+
335+
await server.start();
336+
337+
const res = await request(`http://localhost:${devServerPort}/main.js`);
338+
339+
expect(res.headers["cross-origin-resource-policy"]).toBe("same-origin");
340+
});
341+
342+
it("should NOT set CORP header when user configures wildcard Access-Control-Allow-Origin via headers array", async () => {
343+
const compiler = webpack(config);
344+
server = new Server(
345+
{
346+
port: devServerPort,
347+
allowedHosts: "auto",
348+
headers: [{ key: "Access-Control-Allow-Origin", value: "*" }],
349+
},
350+
compiler,
351+
);
352+
353+
await server.start();
354+
355+
const res = await request(`http://localhost:${devServerPort}/main.js`);
356+
357+
expect(res.headers["cross-origin-resource-policy"]).toBeUndefined();
358+
});
359+
360+
it("should NOT set CORP header when host is in allowedHosts", async () => {
361+
const compiler = webpack(config);
362+
server = new Server(
363+
{ port: devServerPort, allowedHosts: ["localhost"] },
364+
compiler,
365+
);
366+
367+
await server.start();
368+
369+
const res = await request(`http://localhost:${devServerPort}/main.js`);
370+
371+
expect(res.status).toBe(200);
372+
expect(res.headers["cross-origin-resource-policy"]).toBeUndefined();
373+
});
374+
});

types/lib/Server.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,6 +1356,12 @@ declare class Server<
13561356
* @returns {boolean} true when host allowed, otherwise false
13571357
*/
13581358
private isHostAllowed;
1359+
/**
1360+
* @private
1361+
* @returns {boolean} true when the user has configured a wildcard
1362+
* Access-Control-Allow-Origin header (opting into fully open cross-origin access)
1363+
*/
1364+
private isUserCORSWildcardEnabled;
13591365
/**
13601366
* @private
13611367
* @param {{ [key: string]: string | undefined }} headers headers

0 commit comments

Comments
 (0)