mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
49656ed5cf
* fix: security audit findings from issue #741 CI shell injection (high): - publish.yml: use env var for inputs.bump in case statement - nextjs-tracker.yml: use env vars for since_hours and dry_run inputs - nextjs-tracker.yml: use env var in Skip step echo Test file false positive (critical flag): - tests/vite-hmr-websocket.test.ts: add gitleaks:allow for RFC 6455 example WebSocket nonce (dGhlIHNhbXBsZSBub25jZQ==) innerHTML / XSS (high): - packages/vinext/src/shims/script.tsx: add security comment documenting that dangerouslySetInnerHTML is developer-supplied inline script only - packages/vinext/src/shims/head.ts: document tag is bounded to RAW_CONTENT_TAGS (script/style), not user input dangerouslySetInnerHTML in example (medium): - examples/hackernews/components/comment.jsx: sanitize HN API HTML with DOMPurify before rendering - examples/hackernews/package.json: add dompurify dependency - pnpm-workspace.yaml: add dompurify and @types/dompurify to catalog Non-literal RegExp (medium): documented as internal config values only, no user input reaches any of the 6 flagged patterns. See safeRegExp() in config-matchers.ts which already enforces ReDoS protection. Deprecation cleanup: - examples/hackernews/tsconfig.json: remove deprecated baseUrl option (moduleResolution: bundler handles resolution; all imports are relative) - examples/hackernews/tsconfig.json: add noEmit: true to prevent TypeScript from attempting to write over existing .js files in lib/ - examples/hackernews/package.json: add missing @cloudflare/workers-types devDependency (was referenced in tsconfig types but not installed) * fix: address bonk review comments - Switch dompurify → isomorphic-dompurify so sanitize() works during SSR (DOMPurify is a no-op without window/document; isomorphic-dompurify bundles jsdom for server-side use) - Revert package.json whitespace to 2-space indentation (was inadvertently changed to 4-space, creating noise in the diff) - Replace dompurify + @types/dompurify catalog entries with isomorphic-dompurify (ships its own types, no separate @types needed) - Add missing trailing newline to tsconfig.json * fix: address bonk review comments on PR #769 - Switch from isomorphic-dompurify to sanitize-html, and move sanitization to the server-side data boundary (lib/get-comments.js). isomorphic-dompurify depends on jsdom which is Node.js-specific and does not work in the Cloudflare Workers runtime. sanitize-html is pure-JS (htmlparser2-based) with no DOM dependency and works in both Workers and Node.js. Sanitizing once at the data boundary means both the SSR'd initial HTML response and client re-renders receive safe text. - Remove the DOMPurify import from comment.jsx; the client component no longer needs to sanitize since text is already clean when passed as a prop. - Replace isomorphic-dompurify with sanitize-html in the workspace catalog. - Remove inert eslint-disable-next-line comment from script.tsx; the project uses oxlint which ignores ESLint-specific inline suppression directives. --------- Co-authored-by: Luke Percy <lpercyagile@gmail.com>
105 lines
3.2 KiB
TypeScript
105 lines
3.2 KiB
TypeScript
import net from "node:net";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vite-plus/test";
|
|
import { APP_FIXTURE_DIR, startFixtureServer } from "./helpers";
|
|
|
|
function readUpgradeResponse(
|
|
host: string,
|
|
port: number,
|
|
requestPath: string,
|
|
origin?: string,
|
|
): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const socket = net.createConnection({ host, port });
|
|
let data = "";
|
|
let settled = false;
|
|
|
|
const finish = (fn: (value: any) => void, value: any) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
socket.destroy();
|
|
fn(value);
|
|
};
|
|
|
|
socket.setTimeout(5_000, () => finish(reject, new Error("socket timeout")));
|
|
|
|
socket.on("connect", () => {
|
|
const lines = [
|
|
`GET ${requestPath} HTTP/1.1`,
|
|
`Host: ${host}:${port}`,
|
|
"Connection: Upgrade",
|
|
"Upgrade: websocket",
|
|
"Sec-WebSocket-Version: 13",
|
|
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==", // gitleaks:allow — RFC 6455 example nonce ("the sample nonce"), not a real credential
|
|
"Sec-WebSocket-Protocol: vite-hmr",
|
|
];
|
|
if (origin) lines.push(`Origin: ${origin}`);
|
|
socket.write(lines.join("\r\n") + "\r\n\r\n");
|
|
});
|
|
|
|
socket.on("data", (chunk) => {
|
|
data += chunk.toString("utf8");
|
|
if (data.includes("\r\n\r\n")) {
|
|
finish(resolve, data);
|
|
}
|
|
});
|
|
|
|
socket.on("error", (error) => finish(reject, error));
|
|
socket.on("close", () => finish(resolve, data));
|
|
});
|
|
}
|
|
|
|
async function extractViteWsToken(baseUrl: string): Promise<string> {
|
|
const clientCode = await fetch(`${baseUrl}/@vite/client`).then((res) => res.text());
|
|
const match = clientCode.match(/const wsToken = ["']([^"']+)["']/);
|
|
if (!match) {
|
|
throw new Error("Failed to extract Vite websocket token from /@vite/client");
|
|
}
|
|
return match[1];
|
|
}
|
|
|
|
describe("Vite HMR websocket token checks", () => {
|
|
let server: Awaited<ReturnType<typeof startFixtureServer>>["server"];
|
|
let host: string;
|
|
let port: number;
|
|
let token: string;
|
|
|
|
beforeAll(async () => {
|
|
const started = await startFixtureServer(APP_FIXTURE_DIR, { appRouter: true });
|
|
server = started.server;
|
|
|
|
const url = new URL(started.baseUrl);
|
|
host = url.hostname;
|
|
port = Number(url.port);
|
|
token = await extractViteWsToken(started.baseUrl);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await server.close();
|
|
});
|
|
|
|
it("rejects websocket upgrade with Origin header when token is missing", async () => {
|
|
const response = await readUpgradeResponse(host, port, "/", "http://evil.example");
|
|
expect(response.startsWith("HTTP/1.1 400")).toBe(true);
|
|
});
|
|
|
|
it("rejects websocket upgrade with Origin header when token is invalid", async () => {
|
|
const response = await readUpgradeResponse(
|
|
host,
|
|
port,
|
|
"/?token=not-the-real-token",
|
|
"http://evil.example",
|
|
);
|
|
expect(response.startsWith("HTTP/1.1 400")).toBe(true);
|
|
});
|
|
|
|
it("accepts websocket upgrade with a valid token", async () => {
|
|
const response = await readUpgradeResponse(
|
|
host,
|
|
port,
|
|
`/?token=${encodeURIComponent(token)}`,
|
|
"http://localhost:3000",
|
|
);
|
|
expect(response.startsWith("HTTP/1.1 101")).toBe(true);
|
|
});
|
|
});
|