Skip to content

fix: treat loopback aliases as equivalent in isSameOrigin#5659

Open
ThierryRakotomanana wants to merge 1 commit intowebpack:mainfrom
ThierryRakotomanana:fix/isSameOrigin-loopback-alias-mismatch
Open

fix: treat loopback aliases as equivalent in isSameOrigin#5659
ThierryRakotomanana wants to merge 1 commit intowebpack:mainfrom
ThierryRakotomanana:fix/isSameOrigin-loopback-alias-mismatch

Conversation

@ThierryRakotomanana
Copy link
Copy Markdown

@ThierryRakotomanana ThierryRakotomanana commented Apr 20, 2026

Summary

This is a follow-up to webpack/webpack-cli#4715, where Alexander pointed out after merging. That PR fixed the webpack-cli templates by removing the hardcoded host: "localhost" that was causing WebSocket disconnection loops. But the root cause lives deeper, in webpack-dev-server itself.

When host: 'localhost' is configured, the OS decides at bind time whether localhost resolves to 127.0.0.1 (IPv4) or ::1 (IPv6). This is OS-dependent and not under the user's control, Windows 10/11 with modern network stacks commonly prioritizes IPv6, linux 127.0.0.1

During the WebSocket handshake, createWebSocketServer() calls isSameOrigin() which compares the parsed origin header against the parsed host header. When the OS resolves localhost to ::1, the browser sends:

origin: http://localhost
host:   ::1

The final line of isSameOrigin() is a strict string comparison:

return origin === host; // "localhost" === "::1" → false

This causes the server to reject the WebSocket connection and send Invalid Host/Origin header back to the client, triggering an infinite reconnection loop visible in the browser console:

[webpack-dev-server] Invalid Host/Origin header
[webpack-dev-server] Disconnected!
[webpack-dev-server] Trying to reconnect...

This happens even though localhost, 127.0.0.1, and ::1 are all loopback addresses and already individually trusted by isValidHost() earlier in the same handshake check.

What kind of change does this PR introduce?
a fix when host is set to localhost

Did you add tests for your changes?
Four tests added to test/e2e/allowed-hosts.test.js inside the existing describe("check host headers") block, following the same direct server.isSameOrigin() call pattern as existing isValidHost()

Does this PR introduce a breaking change?

No, and this fix does not widen the trust boundary in any way. Here is the exact reasoning:

  • All three values are already individually trusted. isValidHost(), called earlier in the same WebSocket handshake, already passes localhost, 127.0.0.1, and ::1 unconditionally. This fix only resolves the inconsistency when comparing two values that are both already trusted.

  • Both sides must be loopback simultaneously. The condition uses &&, it only passes when both origin and host are in the loopback set. The host header comes from the server's own socket binding. An external attacker cannot control the server's host header to be 127.0.0.1 or ::1.

  • Browsers enforce origin strictly. A page served from https://evil.com will always send Origin: https://evil.com. No browser will send Origin: http://localhost for a cross-origin request from an external site, making spoofing of the loopback origin impossible.

If relevant, what needs to be documented once your changes are merged or what have you already documented?
no need

Use of AI
no

 - When host: localhost is configured, the OS decides at bind time whether localhost resolves to 127.0.0.1 (IPv4) or ::1 (IPv6).
 - This causes isSameOrigin() to reject valid WebSocket connections
because the final string comparison localhost === ::1 fails,
triggering an infinite reconnection loop in the browser console.
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