Skip to content

fix: exact own origin for allowedOrigins lists, and open-editor over the network - #5776

Open
alexander-akait wants to merge 3 commits into
mainfrom
fix/origin-and-open-editor-gaps
Open

alexander-akait wants to merge 3 commits into
mainfrom
fix/origin-and-open-editor-gaps

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

I audited the security behavior of main after #5770. The test drove a real browser against a dev server, with a hostile page on another loopback port and the server reached by a LAN address. It found two gaps; this PR closes both. The default setup on localhost behaves exactly as before.

1. allowedOrigins as a list: the page's own origin is now exact

With a list, even [], the page's own origin used to mean its hostname on any port, with localhost, 127.0.0.1 and [::1] counted as one. Explicit mode answers an allowed origin with an echoed Access-Control-Allow-Origin. So any other server's page on localhost could:

  • load the bundle,
  • read it through CORS (including the /webpack-dev-server assets page),
  • and connect to the HMR WebSocket.

It now means the host and port the request reached the server by, exactly. The scheme is not compared, so a proxy that terminates TLS in front of the server still counts. A pretend Host (with a path or credentials) never matches.

  • Pages on other ports must be listed.
  • The injected client is handed the hot token in this mode, so its HMR connection still gets in even when client.webSocketURL points elsewhere.

allowedOrigins is not released yet (#5770). The release PR #5774 includes it, so please merge this one first.

2. /webpack-dev-server/open-editor reached through a network address

Browsers send neither Sec-Fetch-Site nor Origin for a page that is not a secure context, which is any http: page except one on localhost. With the server reached as http://192.168.x.x:8080 (the default host listens on every interface), any page could load /webpack-dev-server/open-editor?fileName=/etc/hostname as an image and launch the editor. The audit reproduced this in both "auto" and list modes.

Such a request must now show where it came from:

  • a same-origin Referer, which the overlay's own fetch sends;
  • or, in list mode, the hot token.

Requests through localhost are let in as before: browsers send Sec-Fetch-Site there, so a header-less request is a non-browser client. invalidate is unchanged (it only triggers a rebuild, and it is used by typing its URL).

Not changed, already tracked in code TODOs (#5775)

  • Under "auto", a page on another loopback port can still connect to the HMR WebSocket. Real setups rely on this (a backend page on another port using HMR). The next major hands the token to every client and requires the exact origin.
  • host defaults to every interface, serveIndex is on, and static.deny is off.

Tests

  • Unit tests for exact matching: port, loopback alias, default port, TLS-terminating proxy, malformed Host.
  • e2e tests: another port and 127.0.0.1 are refused with no ACAO and no HMR; the page's own origin gets no credentials.
  • Open-editor tests through a network address: refused with no page or a foreign Referer; allowed with its own Referer, Sec-Fetch-Site: same-origin, or the token.
  • Full suite run locally. Apart from the sandbox-only failures main also has (IPv6, proxy, overlay snapshots), the only differences were the CLI help snapshots, updated here. harness-server-cleanup hangs in the sandbox on main as well.
  • I re-ran the browser audit: the other-port page is blocked on every path tested in list mode, the editor is no longer launched over the LAN, and the default localhost behavior is unchanged.

Generated by Claude Code

Summary by CodeRabbit

  • Security

    • Requests to open the editor from network addresses now require same-origin validation and acceptable page provenance.
    • Allowed origins are matched against the exact host and port; malformed or mismatched host values are rejected.
    • Requests from the server’s own origin remain allowed, alongside supported loopback and valid hot-token requests.
  • Documentation

    • Updated configuration guidance and CLI descriptions to clarify exact-origin matching and related HMR behavior.

alexander-akait and others added 2 commits October 20, 2026 11:53
…the network

With `allowedOrigins` set, the page's own origin is now the host and port
it reached the server by, exactly, instead of its hostname on any port
with the loopback names counted as one: another server's page on
`localhost` could read the bundles through CORS and receive hot updates.

`/webpack-dev-server/open-editor` reached through a network address now
needs a same-origin `Referer` (or the hot token) when the request carries
neither `Sec-Fetch-Site` nor `Origin`, which a browser omits for any
`http:` page that is not on `localhost`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Walkthrough

Allowed-origin checks now compare exact hosts and ports, with default-port normalization. The editor-opening endpoint now requires same-origin validation and evidence that the request came from the server’s page. Tests and documentation were updated for these rules.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to 38606

A page on an allowed but different origin can cause a network-accessible dev server to open a requested file in the editor. Require proof that the request came from the server’s page before merging.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title names both changes in the PR: exact own-origin matching for allowedOrigins lists, and restricting open-editor requests made over a network address. The raw summary and objectives confirm bot…
Docstring Coverage Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 4…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title names both changes in the PR: exact own-origin matching for allowedOrigins lists, and restricting open-editor requests made over a network address. The raw summary and objectives confirm both are the main changes, so the title is specific and accurate. It is a single line, but it joins two topics with a comma and is somewhat long at 80 characters.

Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 4 files.

✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.26027% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.80%. Comparing base (8e6cf3a) to head (38606e6).

Files with missing lines Patch % Lines
lib/Server.js 97.26% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #5776   +/-   ##
=======================================
  Coverage   95.80%   95.80%           
=======================================
  Files           6        6           
  Lines        5551     5608   +57     
=======================================
+ Hits         5318     5373   +55     
- Misses        233      235    +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@changeset-bot

changeset-bot Bot commented Oct 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 38606e6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack-dev-server Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 411f4468-9b78-406a-9b9c-a60de46477f6
📥 Commits

Reviewing files that changed from the base of the PR and between f6a57ed and 38606e6.

📒 Files selected for processing: 1
  • lib/Server.js

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

Comment thread lib/Server.js
Comment on lines +3836 to +3867
#isEditorRequestFromThisPage(req) {
const headers =
/** @type {{ [key: string]: string | undefined }} */
(req.headers);

// Already judged by `#isSameOriginRequest`.
if (
typeof headers["sec-fetch-site"] === "string" ||
typeof headers.origin === "string"
) {
return true;
}

const hostHeader = headers[":authority"] || headers.host;
const hostname =
typeof hostHeader === "string"
? this.#parseHostnameFromHeader(hostHeader)
: null;

if (hostname !== null && LOOPBACK_HOSTNAMES.has(hostname)) {
return true;
}

const refererOrigin = this.#parseOrigin(headers.referer);

return (
(refererOrigin !== null &&
this.#isListedOrSameOrigin(refererOrigin, headers)) ||
this.#isHotTokenValid(req)
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '3810,3875p' lib/Server.js
sed -n '202,285p' test/server/host-header-validation.test.js
cat .changeset/open-editor-network-csrf.md

Repository: webpack/webpack-dev-server

Length of output: 5404


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- open-editor route and helper callers ---'
sed -n '2228,2265p' lib/Server.js
printf '%s\n' '--- same-origin request implementation ---'
sed -n '3760,3825p' lib/Server.js
printf '%s\n' '--- listed-origin and parser implementations ---'
sed -n '3890,3985p' lib/Server.js
printf '%s\n' '--- current helper ---'
sed -n '3828,3872p' lib/Server.js
printf '%s\n' '--- changed lines versus merge base ---'
git diff --unified=35 8e6cf3a46ad12379e87e43779e1c7082ee0c194f 38606e6ed3bf33e7bc9ec6386b23b959305ba5ce -- lib/Server.js test/server/host-header-validation.test.js .changeset/open-editor-network-csrf.md

Repository: webpack/webpack-dev-server

Length of output: 27037


Require same-origin page proof for the open-editor endpoint.

An explicitly listed cross-origin page can send its normal Origin header and pass both checks. The helper accepts that Origin without a same-origin Referer or hot token. A listed cross-origin Referer also passes the helper.

allowedOrigins should authorize resource access, not prove that the request came from this server's page. Keep the hot token as the list-mode exception.

Suggested fix
-    if (
-      typeof headers["sec-fetch-site"] === "string" ||
-      typeof headers.origin === "string"
-    ) {
+    if (
+      headers["sec-fetch-site"] === "same-origin" ||
+      (typeof headers.origin === "string" &&
+        this.#isListedOrSameOrigin(headers.origin, headers, false))
+    ) {
       return true;
     }
...
-        this.#isListedOrSameOrigin(refererOrigin, headers)) ||
+        this.#isListedOrSameOrigin(refererOrigin, headers, false)) ||
       this.#isHotTokenValid(req)
-  #isListedOrSameOrigin(originHeader, headers) {
+  #isListedOrSameOrigin(originHeader, headers, allowListed = true) {
...
-    if (allowedOrigins === "all") {
+    if (allowListed && allowedOrigins === "all") {
       return true;
     }
...
-    if (DEFAULT_ALLOWED_PROTOCOLS.test(originHeader)) {
+    if (allowListed && DEFAULT_ALLOWED_PROTOCOLS.test(originHeader)) {
       return true;
     }
...
-    if (Array.isArray(allowedOrigins) && allowedOrigins.includes(origin)) {
+    if (
+      allowListed &&
+      Array.isArray(allowedOrigins) &&
+      allowedOrigins.includes(origin)
+    ) {
       return true;
     }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Traced it, and the behavior is as described. A request that carries Origin was already judged by #isSameOriginRequest, which is #isOriginAllowed(headers, "endpoint") in list mode. So a page on a listed origin, or any origin with "all", can call /open-editor.

That is the rule #5770 set on purpose: listing an origin also opens the endpoints that act on the developer's machine to it. allowed-origins.test.js asserts this for invalidate ("opens the state-changing endpoints to the listed origin only").

Abusing it takes control of a page the developer chose to trust. That is narrower than the unlisted and network-address cases this PR closes.

Restricting /open-editor to the server's own page plus the token, as suggested, changes the meaning of allowedOrigins (currently unreleased). That's a call for the maintainer, so I'm leaving this open until they decide, rather than pushing it here.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant