Skip to content

Hotfix: Backport host/origin validation to requests and websocket connections#34009

Merged
ghengeveld merged 7 commits into
v8from
origin-validation-v8
Mar 6, 2026
Merged

Hotfix: Backport host/origin validation to requests and websocket connections#34009
ghengeveld merged 7 commits into
v8from
origin-validation-v8

Conversation

@ghengeveld
Copy link
Copy Markdown
Member

@ghengeveld ghengeveld commented Mar 4, 2026

What I did

This is a backport of #33835 for Storybook v8.6.

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

  • Run npx ngrok http 6006
  • Verify that the ngrok URL serves an Invalid host error
  • Update .storybook/main.ts to set core.allowedHosts to include the ngrok hostname (no protocol), then restart Storybook
  • Verify that the ngrok URL serves Storybook properly, and it works

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update
    MIGRATION.MD

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🦋 Canary release

This pull request has been released as version 0.0.0-pr-34009-sha-c90626e7. Try it out in a new sandbox by running npx storybook@0.0.0-pr-34009-sha-c90626e7 sandbox or in an existing project with npx storybook@0.0.0-pr-34009-sha-c90626e7 upgrade.

More information
Published version 0.0.0-pr-34009-sha-c90626e7
Triggered by @ghengeveld
Repository storybookjs/storybook
Branch origin-validation-v8
Commit c90626e7
Datetime Wed Mar 4 15:04:14 UTC 2026 (1772636654)
Workflow run 22675197081

To request a new release of this pull request, mention the @storybookjs/core team.

core team members can create a new canary release here or locally with gh workflow run --repo storybookjs/storybook publish.yml --field pr=34009

Summary by CodeRabbit

  • New Features

    • Host validation added for the dev server and WebSocket connections.
    • New allowedHosts option (string[] | true) plus localAddress/networkAddress to control permitted hosts.
    • Startup now displays allowed hosts and warns when binding to 0.0.0.0 for containerized setups.
  • Refactor

    • Server channel and token handling moved to an options-based flow (public API updated).
  • Chores

    • Added host-validation-middleware dependency.
    • .gitignore adjusted to ignore root node_modules.
  • Tests

    • New comprehensive tests for host validation, token validation, and server channel behavior.

@ghengeveld ghengeveld added maintenance User-facing maintenance tasks ci:daily Run the CI jobs that normally run in the daily job. labels Mar 4, 2026
@ghengeveld ghengeveld self-assigned this Mar 4, 2026
@ghengeveld ghengeveld requested a review from yannbf March 4, 2026 10:54
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds host validation middleware and tests; refactors WebSocket server-channel APIs to accept an options object (token, allowedHosts, localAddress, networkAddress); wires allowedHosts into Vite and core dev-server flows; updates startup output and types; adds runtime dependency and removes root node_modules negation in .gitignore.

Changes

Cohort / File(s) Summary
Host Validation Middleware
code/core/src/core-server/utils/getHostValidationMiddleware.ts, code/core/src/core-server/utils/__tests__/getHostValidationMiddleware.test.ts
New HostValidationOptions, isValidHost, getHostValidationMiddleware and extensive tests covering allowedHosts, local/network addresses, ports, HTTPS and edge cases.
WebSocket Channel API Refactor
code/core/src/core-server/utils/get-server-channel.ts, code/core/src/core-server/utils/__tests__/server-channel.test.ts
getServerChannel and ServerChannelTransport switched from token param to an options object; upgrade flow now validates origin/host/token and returns 403 on invalid attempts; tests updated to exercise options-based flow.
Dev Server Integration
code/core/src/core-server/dev-server.ts, code/core/src/core-server/build-dev.ts
Compute/propagate localAddress and networkAddress earlier; integrate host validation middleware into middleware stack; warn when host is 0.0.0.0 with no allowedHosts; include allowedHosts in startup output.
Vite Server Integration
code/builders/builder-vite/src/vite-server.ts
Read allowedHosts from core presets and set config.server.allowedHosts; replace host+regex check with explicit host === '0.0.0.0' handling for container bindings.
Types & Startup Output
code/core/src/types/modules/core-common.ts, code/core/src/core-server/utils/output-startup-information.ts
Add `allowedHosts?: string[]
Token Validation & Tests
code/core/src/core-server/utils/validate-token.ts, code/core/src/core-server/utils/__tests__/validate-token.test.ts
Rename token param to requestToken and add unit tests for token matching, null/undefined, and special-character cases.
Server Init Typing
code/core/src/core-server/utils/server-init.ts
Add explicit return type `Promise<http.Server
Dependencies & Misc
code/core/package.json, .gitignore
Add runtime dependency host-validation-middleware ^0.1.2; remove negation rule for root node_modules in .gitignore.
Release Workflow & Changelog
.github/workflows/publish.md, CHANGELOG.md
Update release workflow steps and add changelog entry for 8.6.18.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant DevServer as Dev Server
    participant HostValidation as Host Validation
    participant Channel as Server Channel
    participant Upgrader as Upgrade Handler

    Client->>DevServer: HTTP Upgrade (Host, Origin, Token)
    DevServer->>HostValidation: validate Host (allowedHosts, localAddress, networkAddress)
    alt Host invalid
        HostValidation-->>DevServer: invalid
        DevServer->>Client: HTTP 403
    else Host valid
        HostValidation-->>DevServer: ok
        DevServer->>Channel: validate Origin & Token (options)
        alt Token/Origin invalid
            Channel-->>DevServer: reject (403)
            DevServer->>Client: Connection rejected
        else Token/Origin valid
            Channel->>Upgrader: perform upgrade
            Upgrader->>Client: WebSocket established
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs


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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
code/core/src/core-server/utils/__tests__/server-channel.test.ts (1)

34-40: Consolidate mock patterns to align with repo's spy-mocking standard.

This test mixes direct global spying (vi.spyOn(console, 'warn')) with inline socket mock implementations (socket.write = vi.fn(), socket.destroy = vi.fn()). Move all package/file mocks to top-level vi.mock(..., { spy: true }) and consolidate mock behaviors in beforeEach blocks using vi.mocked(...). This pattern repeats across multiple test cases (lines 95–96, 120–121, 145–146, 174–175, 199–200, 222–223, 251–252, 280–281) and should be unified to match repo standards per the spy-mocking rules.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/core-server/utils/__tests__/server-channel.test.ts` around
lines 34 - 40, Consolidate test mocking by replacing ad-hoc spies and inline
mock assignments with top-level package/file mocks and centralized setup: add
vi.mock(..., { spy: true }) for modules that are currently spied (e.g., console)
and move socket method mocks (socket.write, socket.destroy) into the beforeEach
using vi.mocked(...) to set their behaviors; remove inline assignments like
socket.write = vi.fn() and socket.destroy = vi.fn(), restore behaviors in
afterEach with vi.restoreAllMocks(), and ensure all repeated occurrences (the
socket mocks at the referenced test blocks) follow this single pattern so that
beforeEach manages mocks and top-level vi.mock(..., { spy: true }) enables
spying consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code/builders/builder-vite/src/vite-server.ts`:
- Around line 35-40: The fallback that sets config.server.allowedHosts = true
only checks for IPv4 wildcard '0.0.0.0' and misses IPv6 wildcard '::'; update
the conditional in vite-server.ts to treat both '0.0.0.0' and '::' as wildcard
binds (e.g., check options.host === '0.0.0.0' || options.host === '::' or
normalize options.host into a set of wildcard values) while keeping the existing
allowedHosts empty-array check so that when options.host is an IPv6 wildcard and
allowedHosts is absent/empty you set config.server.allowedHosts = true.

In `@code/core/src/core-server/build-dev.ts`:
- Around line 139-140: The code destructures allowedHosts from the first
presets.apply('core', {}) result and then later reloads presets with
builder/renderer, so allowedHosts can become stale; update the logic to read
core.allowedHosts (or re-destructure allowedHosts) after the second presets pass
(the reload that applies builder/renderer presets) before emitting the startup
warning/output; specifically adjust the use of the originally captured
allowedHosts variable and reference presets.apply('core', {}) / builder /
renderer so the startup message always uses the post-reload value.
- Around line 147-151: The code calls logger.warn(...) but logger is not
imported into build-dev.ts; add the missing import for the logger used (e.g.,
import { logger } from the Storybook logger module) at the top of the file so
logger is in scope before the call in the function that contains the
host/allowedHosts check (the warning around logger.warn in build-dev.ts).

In `@code/core/src/core-server/utils/get-server-channel.ts`:
- Around line 16-18: The upgrade handling currently calls new URL(request.url,
options.localAddress) which throws if options.localAddress is undefined because
upgrade request.url is often relative; update both occurrences (the new URL(...)
usages) to build a safe base when options.localAddress is missing by falling
back to a host-derived base (e.g., use request.headers.host with "http://" or
"ws://" as appropriate) or by wrapping the URL construction in a try/catch and
constructing new URL(request.url, `http://${request.headers.host}`) when the
first call fails, ensuring relative upgrade URLs are parsed instead of causing a
403 rejection.

---

Nitpick comments:
In `@code/core/src/core-server/utils/__tests__/server-channel.test.ts`:
- Around line 34-40: Consolidate test mocking by replacing ad-hoc spies and
inline mock assignments with top-level package/file mocks and centralized setup:
add vi.mock(..., { spy: true }) for modules that are currently spied (e.g.,
console) and move socket method mocks (socket.write, socket.destroy) into the
beforeEach using vi.mocked(...) to set their behaviors; remove inline
assignments like socket.write = vi.fn() and socket.destroy = vi.fn(), restore
behaviors in afterEach with vi.restoreAllMocks(), and ensure all repeated
occurrences (the socket mocks at the referenced test blocks) follow this single
pattern so that beforeEach manages mocks and top-level vi.mock(..., { spy: true
}) enables spying consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 35231fc7-2de2-4e16-ae0e-1821a55a1007

📥 Commits

Reviewing files that changed from the base of the PR and between c6e550a and 7d88ff1.

⛔ Files ignored due to path filters (1)
  • code/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (14)
  • .gitignore
  • code/builders/builder-vite/src/vite-server.ts
  • code/core/package.json
  • code/core/src/core-server/build-dev.ts
  • code/core/src/core-server/dev-server.ts
  • code/core/src/core-server/utils/__tests__/getHostValidationMiddleware.test.ts
  • code/core/src/core-server/utils/__tests__/server-channel.test.ts
  • code/core/src/core-server/utils/__tests__/validate-token.test.ts
  • code/core/src/core-server/utils/get-server-channel.ts
  • code/core/src/core-server/utils/getHostValidationMiddleware.ts
  • code/core/src/core-server/utils/output-startup-information.ts
  • code/core/src/core-server/utils/server-init.ts
  • code/core/src/core-server/utils/validate-token.ts
  • code/core/src/types/modules/core-common.ts
💤 Files with no reviewable changes (1)
  • .gitignore

Comment thread code/builders/builder-vite/src/vite-server.ts
Comment thread code/core/src/core-server/build-dev.ts
Comment thread code/core/src/core-server/build-dev.ts
Comment thread code/core/src/core-server/utils/get-server-channel.ts
@storybook-bot
Copy link
Copy Markdown
Contributor

Failed to publish canary version of this pull request, triggered by @ghengeveld. See the failed workflow run at: https://github.com/storybookjs/storybook/actions/runs/22670820120

@storybook-bot
Copy link
Copy Markdown
Contributor

Failed to publish canary version of this pull request, triggered by @ghengeveld. See the failed workflow run at: https://github.com/storybookjs/storybook/actions/runs/22674106283

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
code/core/src/core-server/build-dev.ts (1)

139-140: ⚠️ Potential issue | 🟡 Minor

Use post-second-pass allowedHosts to avoid stale security messaging.

allowedHosts is read from the first presets.apply('core', {}) (Line 139), then reused for warning/output after the second preset load. If builder/renderer presets alter core.allowedHosts, Lines 143-146 and Line 267 can be inaccurate.

Suggested fix
-  const { allowedHosts, renderer, builder, disableTelemetry } = await presets.apply('core', {});
+  const { renderer, builder, disableTelemetry } = await presets.apply('core', {});

-  // '0.0.0.0' binds to all interfaces, which is useful for Docker and other containerized environments.
-  // By default we allow requests from all hosts in this case, but the user should be made aware of the risk.
-  if (
-    options.host === '0.0.0.0' &&
-    (!allowedHosts || (allowedHosts !== true && allowedHosts.length === 0))
-  ) {
-    logger.warn(dedent`
-      --host is set to 0.0.0.0 but no allowedHosts are defined. Allowing all hosts.
-      To restrict allowed hosts, set core.allowedHosts in your main Storybook config.
-      See: https://storybook.js.org/docs/api/main-config/main-config-core
-    `);
-  }
@@
   presets = await loadAllPresets({
@@
   });
+
+  const { allowedHosts } = await presets.apply('core', {});
+  // '0.0.0.0' binds to all interfaces, which is useful for Docker and other containerized environments.
+  // By default we allow requests from all hosts in this case, but the user should be made aware of the risk.
+  if (
+    options.host === '0.0.0.0' &&
+    (!allowedHosts || (allowedHosts !== true && allowedHosts.length === 0))
+  ) {
+    logger.warn(dedent`
+      --host is set to 0.0.0.0 but no allowedHosts are defined. Allowing all hosts.
+      To restrict allowed hosts, set core.allowedHosts in your main Storybook config.
+      See: https://storybook.js.org/docs/api/main-config/main-config-core
+    `);
+  }

Also applies to: 143-146, 267-267

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/core-server/build-dev.ts` around lines 139 - 140, The code
reads core.allowedHosts from the first presets.apply('core', {}) and then uses
it later for warnings/output, which can be stale if renderer/builder presets
mutate core.allowedHosts; update the logic to re-read allowedHosts after the
second presets.apply that loads/merges renderer and builder presets (i.e., call
presets.apply('core', {}) again or read the merged presets result used after
secondary load) and use that refreshed allowedHosts value for the warning/output
code paths (the places referencing allowedHosts after renderer/builder
processing such as the warning block and the use at the later output point).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@code/core/src/core-server/build-dev.ts`:
- Around line 139-140: The code reads core.allowedHosts from the first
presets.apply('core', {}) and then uses it later for warnings/output, which can
be stale if renderer/builder presets mutate core.allowedHosts; update the logic
to re-read allowedHosts after the second presets.apply that loads/merges
renderer and builder presets (i.e., call presets.apply('core', {}) again or read
the merged presets result used after secondary load) and use that refreshed
allowedHosts value for the warning/output code paths (the places referencing
allowedHosts after renderer/builder processing such as the warning block and the
use at the later output point).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c2fc3710-2879-445b-90d6-90b0d2c7569c

📥 Commits

Reviewing files that changed from the base of the PR and between 7d88ff1 and c90626e.

📒 Files selected for processing (1)
  • code/core/src/core-server/build-dev.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CHANGELOG.md`:
- Line 3: Update the changelog entry to explicitly state the security-related
backport: replace the vague "Add request validation" with a sentence that
details host/origin validation was added for HTTP requests and WebSocket
connections, mention that this may block external access and that users can
configure core.allowedHosts to allow external hostnames (for example ngrok), and
include a short note about the purpose (preventing host header/origin spoofing).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7e0a3310-9d5a-4db0-95c1-c0f6e0818b58

📥 Commits

Reviewing files that changed from the base of the PR and between 9dbe9b1 and 624fcec.

📒 Files selected for processing (1)
  • CHANGELOG.md

Comment thread CHANGELOG.md
…ss and add detailed steps for canary testing
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
.github/workflows/publish.md (1)

14-17: Consider adding a failure-path note for canary cleanup.

Given canary publish failures are common in practice, add a short note that the deployment branch entry must be removed even when Step 2/3 fails (not only on success), plus a quick pointer on where to verify failure cause (workflow run logs).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/publish.md around lines 14 - 17, Add a brief failure-path
note to the publish-canary instructions: update the steps around the "deployment
branch" entry (Step 1) to say that the deployment branch must be removed even if
the canary publish (Step 2/3 invoking the publish-canary workflow/publish.yml)
fails, and add a short pointer to check the publish-canary workflow run logs to
diagnose the failure; reference the "deployment branch" entry, the
"publish-canary" workflow and "publish.yml" so readers know where to remove the
branch and where to verify failure cause.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.github/workflows/publish.md:
- Around line 14-17: Add a brief failure-path note to the publish-canary
instructions: update the steps around the "deployment branch" entry (Step 1) to
say that the deployment branch must be removed even if the canary publish (Step
2/3 invoking the publish-canary workflow/publish.yml) fails, and add a short
pointer to check the publish-canary workflow run logs to diagnose the failure;
reference the "deployment branch" entry, the "publish-canary" workflow and
"publish.yml" so readers know where to remove the branch and where to verify
failure cause.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9b208196-9487-4796-bd77-b6dd640a7f08

📥 Commits

Reviewing files that changed from the base of the PR and between 624fcec and a0498b8.

📒 Files selected for processing (1)
  • .github/workflows/publish.md

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Mar 6, 2026

View your CI Pipeline Execution ↗ for commit 725c81b

Command Status Duration Result
nx run-many -t build -c production --parallel=3 ✅ Succeeded 4m 11s View ↗
nx affected -t check -c production --parallel=7 ✅ Succeeded <1s View ↗

☁️ Nx Cloud last updated this comment at 2026-03-06 09:56:16 UTC

…ific test failures and errors encountered during CI processes.
@ghengeveld ghengeveld merged commit d81a34c into v8 Mar 6, 2026
91 of 98 checks passed
@ghengeveld ghengeveld deleted the origin-validation-v8 branch March 6, 2026 10:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:daily Run the CI jobs that normally run in the daily job. maintenance User-facing maintenance tasks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants