1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
import { EventEmitter } from "node:events";
import type { IncomingMessage, ServerResponse } from "node:http";
import type { Connect, Plugin, PreviewServer, ViteDevServer } from "vite";
import { describe, expect, it, vi } from "vitest";
import { applyViteNetworkPolicy, devNetworkGuard } from "./vite-network-guard";
const policy = {
allowedHosts: new Set([
"localhost",
"127.0.0.1",
"::1",
"dev-node.tailnet.example",
"192.0.2.10",
]),
allowedAddresses: new Set(["127.0.0.1", "::1", "192.0.2.10"]),
};
function install(mode: "dev" | "preview") {
let middleware: Connect.NextHandleFunction | undefined;
const http = new EventEmitter();
const upgrade = vi.fn();
http.on("upgrade", upgrade);
const server = {
middlewares: {
use: (handler: Connect.NextHandleFunction) => {
middleware = handler;
},
},
httpServer: http,
};
const plugin = devNetworkGuard(policy);
const hook = mode === "dev" ? plugin.configureServer : plugin.configurePreviewServer;
if (typeof hook !== "function") throw new Error(`Missing ${mode} guard hook`);
// Hooks only need the injected middleware stack and HTTP event emitter.
hook.call({} as never, server as unknown as ViteDevServer & PreviewServer);
if (!middleware) throw new Error(`Missing ${mode} middleware`);
return { middleware, http, upgrade };
}
function request(localAddress: string | undefined, host?: string, origin?: string) {
return { socket: { localAddress }, headers: { host, origin } } as IncomingMessage;
}
const cases = [
["loopback", "127.0.0.1", "localhost", undefined, 200],
["IPv6 loopback", "::1", "[::1]:34204", undefined, 200],
["mapped address / canonical Host", "::ffff:127.0.0.1", "LOCALHOST.:34204", undefined, 200],
[
"tailnet",
"192.0.2.10",
"dev-node.tailnet.example",
"https://dev-node.tailnet.example:34204",
200,
],
["unselected interface with trusted Host", "172.17.0.1", "localhost", undefined, 403],
["missing interface", undefined, "localhost", undefined, 403],
["arbitrary IP Host", "127.0.0.1", "127.0.0.2", undefined, 421],
["rebinding Host", "127.0.0.1", "evil.example", undefined, 421],
["missing Host", "127.0.0.1", undefined, undefined, 421],
["disallowed Origin", "127.0.0.1", "localhost", "https://evil.example", 403],
["opaque Origin", "127.0.0.1", "localhost", "null", 403],
["Host before Origin", "127.0.0.1", "evil.example", "null", 421],
] as const;
describe.each(["dev", "preview"] as const)("%s HTTP guard", (mode) => {
it.each(cases)("%s", (_label, local, host, origin, status) => {
const { middleware } = install(mode);
const req = request(local, host, origin);
const writeHead = vi.fn();
const end = vi.fn();
const next = vi.fn();
middleware(req, { writeHead, end } as unknown as ServerResponse, next);
if (status === 200) {
expect(next).toHaveBeenCalledExactlyOnceWith();
expect(writeHead).not.toHaveBeenCalled();
if (host === "LOCALHOST.:34204") expect(req.headers.host).toBe("localhost:34204");
} else {
expect(writeHead).toHaveBeenCalledWith(status, { "Content-Type": "text/plain" });
expect(end).toHaveBeenCalledOnce();
expect(next).not.toHaveBeenCalled(); // In particular, cannot reach CORS/static serving.
}
});
});
describe("dev upgrades remain protected", () => {
it.each(cases)("%s", (_label, local, host, origin, status) => {
const { http, upgrade } = install("dev");
const end = vi.fn();
http.emit("upgrade", request(local, host, origin), { end }, Buffer.alloc(0));
if (status === 200) {
expect(upgrade).toHaveBeenCalledOnce();
expect(end).not.toHaveBeenCalled();
} else {
expect(upgrade).not.toHaveBeenCalled();
expect(end).toHaveBeenCalledWith(expect.stringContaining(`HTTP/1.1 ${status}`));
}
});
});
it("sets explicit allowlists and guarded CORS for preview as well as dev", () => {
const config = applyViteNetworkPolicy(
{
server: { host: true, port: 34204 },
preview: { host: true, port: 34204, cors: true, allowedHosts: true },
},
policy,
);
for (const serving of [config.server, config.preview]) {
expect(serving?.allowedHosts).toEqual([...policy.allowedHosts]);
expect(serving?.cors).toEqual({ origin: true });
expect(serving?.host).toBe(true);
expect(serving?.port).toBe(34204);
}
const plugin = config.plugins?.[0] as Plugin;
expect(plugin.configureServer).toBeTypeOf("function");
expect(plugin.configurePreviewServer).toBeTypeOf("function");
});
|