Skip to content

create allowlist for localhost variants and validate host on requests#7032

Open
EvilGenius13 wants to merge 1 commit intomainfrom
change-allowed-hosts-theme-dev
Open

create allowlist for localhost variants and validate host on requests#7032
EvilGenius13 wants to merge 1 commit intomainfrom
change-allowed-hosts-theme-dev

Conversation

@EvilGenius13
Copy link
Contributor

@EvilGenius13 EvilGenius13 commented Mar 17, 2026

WHY are these changes introduced?

Fixes https://github.com/Shopify/developer-tools-team/issues/1092

When running theme dev a potential security vulnerability exists where a developer could visit a malicious site while the CLI is running that uses DNS rebinding to extract data.

WHAT is this pull request doing?

On theme dev boot up, create an allow list of localhost forms 'localhost', '127.0.0.1', '::1', '0.0.0.0'. When requests come in, we deny those that aren't from the allow list.

How to test your changes?

Note This vulnerability does not exist when using an environment. You can test this by running the commands below using an environment and they should return a 401.

On any current version of the CLI:

  • run theme dev and in a separate terminal, try these curl commands:
    • curl -s -I -H "Host: localhost:9292" http://localhost:9292/ | head -1
    • curl -s -I -H "Host: attacker.com:9292" http://localhost:9292/ | head -1
    • curl -s -I -H "Host: evil.local:9292" http://localhost:9292/ | head -1
    • curl -s -I -H "Host: localhost:1234" http://localhost:9292/ | head -1
      These should return a 200

On the current branch:

  • build the branch
  • run theme dev and in a separate terminal, try the curl commands again:
    • curl -s -I -H "Host: localhost:9292" http://localhost:9292/ | head -1 This will return a 200 as it's on the allowlist
      These commands should return a 400
    • curl -s -I -H "Host: attacker.com:9292" http://localhost:9292/ | head -1 **This will return
    • curl -s -I -H "Host: evil.local:9292" http://localhost:9292/ | head -1
    • curl -s -I -H "Host: localhost:1234" http://localhost:9292/ | head -1

Post-release steps

Measuring impact

How do we know this change was effective? Please choose one:

  • n/a - this doesn't need measurement, e.g. a linting rule or a bug-fix
  • Existing analytics will cater for this addition
  • PR includes analytics changes to measure impact

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes

@EvilGenius13 EvilGenius13 requested review from a team as code owners March 17, 2026 14:55
Copilot AI review requested due to automatic review settings March 17, 2026 14:55
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Introduces DNS rebinding protection for the theme dev local development server by validating incoming Host headers against an allowlist of localhost variants, preventing malicious origins from accessing the local server via rebinding.

Changes:

  • Add Host-header allowlist validation middleware to the theme dev H3 server.
  • Add/adjust Vitest coverage to exercise allowlisted vs rejected Host headers.
  • Add a changeset for a patch release of @shopify/theme.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
packages/theme/src/cli/utilities/theme-environment/theme-environment.ts Adds allowed-host set construction and Host header validation to block non-local Host values.
packages/theme/src/cli/utilities/theme-environment/theme-environment.test.ts Adds DNS rebinding protection tests and updates request helpers to include Host headers.
.changeset/few-maps-melt.md Declares a patch release entry describing the security hardening.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +134 to +146
function createAllowedHostsSet(host: string, port: string): Set<string> {
const allowedHosts = new Set<string>()
const portSuffix = `:${port}`

allowedHosts.add(`${host}${portSuffix}`)

// When binding to localhost variants or 0.0.0.0, allow all localhost forms
const localhostVariants = ['localhost', '127.0.0.1', '::1', '0.0.0.0']
if (localhostVariants.includes(host)) {
allowedHosts.add(`localhost${portSuffix}`)
allowedHosts.add(`127.0.0.1${portSuffix}`)
allowedHosts.add(`[::1]${portSuffix}`)
}
Comment on lines +159 to +162
const hostHeader = event.node.req.headers.host
const normalizedHost = hostHeader?.toLowerCase().replace(/\.$/, '')

if (!normalizedHost || !allowedHosts.has(normalizedHost)) {
return resEnd(content)
}
test.each([
['localhost:9292', 'configured host'],
@github-actions
Copy link
Contributor

github-actions bot commented Mar 17, 2026

Coverage report

St.
Category Percentage Covered / Total
🟡 Statements 79.15% 14592/18437
🟡 Branches 73.39% 7238/9862
🟡 Functions 79.32% 3716/4685
🟡 Lines 79.47% 13788/17349

Test suite run success

3825 tests passing in 1460 suites.

Report generated by 🧪jest coverage report action from b0d6fa9

@EvilGenius13 EvilGenius13 force-pushed the change-allowed-hosts-theme-dev branch from 2e14c6c to b0d6fa9 Compare March 17, 2026 16:24

allowedHosts.add(`${normalizedHost}${portSuffix}`)

// When binding to localhost variants or 0.0.0.0, allow all localhost forms
Copy link
Contributor

Choose a reason for hiding this comment

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

Either explain more of a why if it's confusing or drop this comment

Suggested change
// When binding to localhost variants or 0.0.0.0, allow all localhost forms

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you think most people would understand I can drop the comment. Otherwise I can add something like browsers will send whatever localhost you typed such as localhost or 127.0.0.1 so we need to cover the different formats.

Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't think people would actually type 0.0.0.0 - that's usually an address you bind to accept any request.

However, I can imagine people visiting their local private IP address in their home (like 192.168.2.100). Should we also be white listing private IPs? 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

Wait, i think you added the host you ran the theme dev command with to the allowlist. You should be fine here (ignore previous comment).

Copy link
Contributor

Choose a reason for hiding this comment

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

There's a chance I'm blind here: does this work if a user passes in a --host flag?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes it does. In the services/dev.ts file it runs setupDevServer and passes a context in that would include flags like host and port so we would be creating the custom host/port if passed in.


allowedHosts.add(`${normalizedHost}${portSuffix}`)

// When binding to localhost variants or 0.0.0.0, allow all localhost forms
Copy link
Contributor

Choose a reason for hiding this comment

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

Wait, i think you added the host you ran the theme dev command with to the allowlist. You should be fine here (ignore previous comment).

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.

4 participants