Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/independent-origin-authorization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"webpack-dev-server": minor
---

Add `allowedOrigins` to say which pages may use the server — its HTTP endpoints, hot module replacement and proxied WebSockets — independently of `allowedHosts`, which goes back to checking `Host`. Nothing changes until it is set: the default, `"auto"`, keeps the rules derived from `allowedHosts`. A list allows the listed origins plus the page's own hostname (any port, loopback names counted as one, as before), answering them with CORS headers and allowing a plain `<script>` they load by its `Referer`; `"all"` allows every origin without turning off the `Host` check. Under `"auto"`, `allowedHosts: "all"` and an `Access-Control-Allow-Origin: *` entry in `headers` still open the server to every origin, and are deprecated for doing so.

Once `allowedOrigins` is set, the injected hot client is also handed a per-run token, and a client carrying it may connect from an origin the server does not otherwise know, such as a proxy's; the `Host` check still applies. webpack-dev-middleware is now required at `^8.5.0`, the first release that hands over a token without requiring it.

Add `static.deny`: `true` refuses `.env`, `.env.*`, keys and certificates, `.npmrc`, `.yarnrc.yml` and `.git` in a static directory with `403` and leaves them out of directory listings, or give patterns of your own. Off by default for now.
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@
"privkey",
"geomanist",
"nodenext",
"WHATWG"
"WHATWG",
"Referer"
],
"ignorePaths": [
"CHANGELOG.md",
Expand Down
83 changes: 83 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ fast in-memory access to the webpack assets.
- [With the API](#with-the-api)
- [With TypeScript](#with-typescript)
- [The Result](#the-result)
- [Security](#security)
- [allowedOrigins](#allowedorigins)
- [static.deny](#staticdeny)
- [Browser Support](#browser-support)
- [Support](#support)
- [Contributing](#contributing)
Expand Down Expand Up @@ -105,6 +108,8 @@ Options:
--no-watch-options-stdin Negative 'watch-options-stdin' option.
--allowed-hosts <value...> Allows to enumerate the hosts from which access to the dev server are allowed (useful when you are proxying dev server, by default is 'auto').
--allowed-hosts-reset Clear all items provided in 'allowedHosts' configuration. Allows to enumerate the hosts from which access to the dev server are allowed (useful when you are proxying dev server, by default is 'auto').
--allowed-origins <value...> Which origins may use the dev server: its HTTP endpoints, the hot module replacement connection and proxied WebSockets. 'auto' (the default) keeps the rules derived from 'allowedHosts'. A list of HTTP(S) origins allows those plus the page's own hostname, on any port, independently of 'allowedHosts'. 'all' allows every origin without turning off the 'Host' check.
--allowed-origins-reset Clear all items provided in 'allowedOrigins' configuration. Which origins may use the dev server: its HTTP endpoints, the hot module replacement connection and proxied WebSockets. 'auto' (the default) keeps the rules derived from 'allowedHosts'. A list of HTTP(S) origins allows those plus the page's own hostname, on any port, independently of 'allowedHosts'. 'all' allows every origin without turning off the 'Host' check.
--bonjour Allows to broadcasts dev server via ZeroConf networking on start.
--no-bonjour Disallows to broadcasts dev server via ZeroConf networking on start.
--no-client Disables client script.
Expand Down Expand Up @@ -170,8 +175,11 @@ Options:
--no-static-serve-index Does not tell dev server to use serveIndex middleware.
--static-watch Watches for files in static content directory.
--no-static-watch Does not watch for files in static content directory.
--static-deny [value...] Files never to serve from the static content directory, matched against each segment of the path: 'true' for the default list of secrets ('.env', '.env.*', keys and certificates, '.npmrc', '.yarnrc.yml', '.git'), or patterns of your own, with '*', '?' and '{a,b}'.
--no-static-deny Serves every file in the static content directory.
--static-reset Clear all items provided in 'static' configuration. Allows to configure options for serving static files from directory (by default 'public' directory).
--static-public-path-reset Clear all items provided in 'static.publicPath' configuration. The static files will be available in the browser under this public path.
--static-deny-reset Clear all items provided in 'static.deny' configuration. Files never to serve from the static content directory, matched against each segment of the path: 'true' for the default list of secrets ('.env', '.env.*', keys and certificates, '.npmrc', '.yarnrc.yml', '.git'), or patterns of your own, with '*', '?' and '{a,b}'.
--watch-files <value...> Allows to configure list of globs/directories/files to watch for file changes.
--watch-files-reset Clear all items provided in 'watchFiles' configuration. Allows to configure list of globs/directories/files to watch for file changes.
--no-web-socket-server Disallows to set web socket server and options.
Expand Down Expand Up @@ -257,6 +265,81 @@ you edit your assets while the server is running.

See [**the documentation**][docs-url] for more use cases and options.

## Security

The dev server is for development and is not meant to be reachable by anyone but
you. It checks two different things, with two options:

- **`allowedHosts`** checks the `Host` header, so a website cannot reach the server
through a DNS name it points at your machine (DNS rebinding).
- **`allowedOrigins`** checks which pages may use the server — read its bundles,
connect for hot module replacement, open proxied WebSockets, and call its
internal endpoints — so a website you have open cannot read your source.

### allowedOrigins

Type: `"auto" | "all" | string | string[]`
Default: `"auto"`

- `"auto"` keeps the rules this server has always applied: the page's own
hostname, on any port, with `localhost`, `127.0.0.1` and `[::1]` counted as one;
`file:` and browser-extension pages; and whatever `allowedHosts` allows. With
`"auto"`, `allowedHosts: "all"` and an `Access-Control-Allow-Origin: *` entry in
`headers` still open the server to every origin — both are **deprecated** and
will stop doing so in the next major release, so set `allowedOrigins` instead.
- A list of HTTP(S) origins allows those, plus the page's own hostname — on any
port, with `localhost`, `127.0.0.1` and `[::1]` counted as one, as with `"auto"`
— and `file:` and browser-extension pages, whatever `allowedHosts` says. A
listed origin's scheme, hostname and port must match. An empty list allows only
the page's own hostname, so another page on `localhost`, on any port, can still
use a server on `localhost`.
- `"all"` allows every origin. It does not turn off the `Host` check.

```js
module.exports = {
devServer: {
// a tunnel or a container: stop checking `Host`...
allowedHosts: "all",
// ...and still allow only your frontend to use the server
allowedOrigins: ["https://app.example.com"],
},
};
```

With a list, a listed origin's requests are answered with
`Access-Control-Allow-Origin` and `Access-Control-Allow-Credentials`, and its
preflight requests with the methods and headers it asks for, and a plain `<script>` or `<img>` it loads (one
without `crossorigin`) is allowed by the page's `Referer`. Other origins are
refused with `403`. A page with `Referrer-Policy: no-referrer` has to load the
bundle with `crossorigin` (and `output.crossOriginLoading: "anonymous"`) instead.
See the [allowed origins example](examples/allowed-origins/README.md).

With `allowedOrigins` set, the injected client is also handed a token that is
new on every run. A hot client that carries it may connect from an origin the
server does not know, such as a proxy's, as long as its `Host` is allowed. A
client you wire yourself has no token and is judged by its origin. Under
`"auto"` there is no token, so nothing about the socket changes.

### static.deny

Type: `boolean | string[]`
Default: `false`

Files never to serve from a [`static`](https://webpack.js.org/configuration/dev-server/#devserverstatic)
directory, matched against each segment of the request path, case-insensitively.
`true` refuses `.env`, `.env.*`, keys and certificates (`*.{crt,pem,key,p12,pfx,cer,der}`),
`.npmrc`, `.yarnrc.yml` and `.git` with `403`, and leaves them out of directory
listings; a list of patterns (with `*`, `?` and `{a,b}`) refuses those instead.
It will be `true` by default in the next major release.

```js
module.exports = {
devServer: {
static: { directory: "public", deny: true },
},
};
```

## Browser Support

The browser runtime is [webpack-dev-middleware]'s, which ships as ES5 and uses
Expand Down
96 changes: 96 additions & 0 deletions examples/allowed-origins/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Allowing a frontend on another origin

`allowedHosts` checks the `Host` header the request was sent to. `allowedOrigins`
checks which pages may use the server: its bundles, hot module replacement
(WebSocket or Server-Sent Events), WebSocket proxies and its internal endpoints.

```js
export default {
devServer: {
allowedOrigins: ["http://app.localhost:3000"],
},
};
```

Run this example from this directory:

```console
npx webpack serve --port 8080 --open
```

The page at `http://localhost:8080` should display `Success!`. A frontend at
`http://app.localhost:3000` (any page served there, such as a backend's template)
can load the bundle with a plain script tag and connect for hot module
replacement:

```html
<script src="http://localhost:8080/main.js"></script>
```

or read it with `fetch`, in that page's console:

```js
const response = await fetch("http://localhost:8080/main.js");
console.log(response.status); // 200
```

A listed origin's requests get `Access-Control-Allow-Origin` and
`Access-Control-Allow-Credentials`, its preflight requests are answered with the methods
and headers it asks for (and Private Network Access), and a script or
image it loads without `crossorigin` is allowed by the page's `Referer`. Any
other origin gets `403`, cannot receive hot updates and cannot connect through a
WebSocket proxy. The `Host` check still applies.

## Values

- `"auto"` (the default): the rules derived from `allowedHosts`, unchanged. The
page's own hostname on any port is allowed, with `localhost`, `127.0.0.1` and
`[::1]` counted as one, and so is every origin when `allowedHosts` is `"all"`
(deprecated: set `allowedOrigins` instead).
- An HTTP(S) origin, or a list of them: those, plus the page's own hostname (on
any port, loopback names counted as one, as with `"auto"`), whatever
`allowedHosts` says. A listed origin's scheme, hostname and port must match.
Entries may have a trailing slash, but no path, credentials, query or
fragment. `[]` allows the page's own hostname only — which still includes
every other page on `localhost` when the server is on `localhost`.
- `"all"`: every origin, with `Access-Control-Allow-Origin: *`. It does not turn
off the `Host` check.

The CLI takes the flag more than once:

```console
npx webpack serve --allowed-origins http://app.localhost:3000 --allowed-origins https://app.example.com
```

## Tunnels, containers and reverse proxies

`allowedHosts: "all"` is the usual answer to a `Host` the server does not know.
Under `"auto"` it also lets every website use the server; list the origin your
frontend is served from to keep that closed:

```js
export default {
devServer: {
allowedHosts: "all",
allowedOrigins: ["https://my-app.example-tunnel.dev"],
},
};
```

Forwarded headers grant nothing. For TLS terminated at a proxy, list the public
`https` origin.

Hot module replacement does not need the list for a proxy once `allowedOrigins`
is set: the injected client is handed a token, new on every run, and a client
that carries it may connect from an origin the server does not know.

## A page that sends no `Referer`

With `Referrer-Policy: no-referrer`, a plain script load carries nothing to
identify the page, and a cross-site one is refused. Load the bundle with
`crossorigin` so the browser sends `Origin`, and set
`output.crossOriginLoading: "anonymous"` so lazy chunks and hot updates do too:

```html
<script src="http://localhost:8080/main.js" crossorigin="anonymous"></script>
```
1 change: 1 addition & 0 deletions examples/allowed-origins/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
document.querySelector("#target").textContent = "Success!";
11 changes: 11 additions & 0 deletions examples/allowed-origins/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { setup } from "../util.js";

export default setup(
{
entry: "./app.js",
devServer: {
allowedOrigins: ["http://app.localhost:3000"],
},
},
import.meta.url,
);
Loading
Loading