Skip to content

Commit 7a4e3d8

Browse files
test: add server-secret leak regression test (#2094)
1 parent 926336c commit 7a4e3d8

5 files changed

Lines changed: 120 additions & 1 deletion

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const serverSecret = "MyServerSuperSecretUniqueString1";
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
"use server";
22

33
import { isServer } from "solid-js/web";
4+
import { keepAlive } from "~/utils/keep-alive-util";
5+
6+
const serverSecret = "MyServerSuperSecretUniqueString2";
7+
48

59
export function serverFnWithIsServer() {
10+
keepAlive(serverSecret);
611
return isServer;
712
}

apps/tests/src/routes/is-server-nested.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { createEffect, createSignal } from "solid-js";
22
import { isServer } from "solid-js/web";
3+
import { serverSecret } from "~/functions/server-secret";
4+
import { keepAlive } from "~/utils/keep-alive-util";
35

46
function serverFnWithIsServer() {
57
"use server";
6-
8+
keepAlive("MyServerSuperSecretUniqueString3");
9+
keepAlive(serverSecret);
710
return isServer;
811
}
912

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { existsSync } from "node:fs";
2+
import { readdir, readFile } from "node:fs/promises";
3+
import path from "node:path";
4+
import { brotliDecompressSync, gunzipSync } from "node:zlib";
5+
import { describe, expect, it } from "vitest";
6+
7+
// Avoid full pattern to exclude this file from scan
8+
const SECRET_MARKER = new RegExp(`${"MyServer"}${"SuperSecretUniqueString"}\\d+`, "g");
9+
const ALL_FILE_EXTENSIONS = /\.(ts|tsx|js|jsx|mjs|cjs|mts|cts|css|map|gz|br)$/;
10+
11+
describe("server code does not leak to client bundle", () => {
12+
it("verifies secret markers are server-only and not in client output", async () => {
13+
const appRoot = process.cwd();
14+
const sourceRoot = path.join(appRoot, "src");
15+
const serverOutputRoot = path.join(appRoot, ".output/server");
16+
const clientOutputRoot = path.join(appRoot, ".output/public");
17+
18+
// Verify required directories exist
19+
expect(existsSync(sourceRoot), `Source dir not found: ${sourceRoot}`).toBe(true);
20+
expect(
21+
existsSync(serverOutputRoot),
22+
`Server output dir not found: ${serverOutputRoot}. Did you run the build? (pnpm --filter tests run build)`,
23+
).toBe(true);
24+
expect(
25+
existsSync(clientOutputRoot),
26+
`Client output dir not found: ${clientOutputRoot}. Did you run the build? (pnpm --filter tests run build)`,
27+
).toBe(true);
28+
29+
// Collect and validate markers from source code
30+
const sourceMarkerCounts = await countSourceMarkers(sourceRoot);
31+
expect(
32+
sourceMarkerCounts.size,
33+
`No markers found in source code: ${sourceRoot}`,
34+
).toBeGreaterThan(0);
35+
for (const [marker, files] of sourceMarkerCounts) {
36+
expect(
37+
files.length,
38+
`Marker "${marker}" appears in multiple files: ${files.join(", ")}. Each marker must appear exactly once.`,
39+
).toBe(1);
40+
}
41+
const markers = Array.from(sourceMarkerCounts.keys());
42+
43+
// Verify markers are in server output (not DCE'd)
44+
const serverMarkerCounts = await countSourceMarkers(serverOutputRoot);
45+
for (const marker of markers) {
46+
// Check presence; exact count varies due to bundler duplication
47+
expect(
48+
serverMarkerCounts.has(marker),
49+
`Marker "${marker}" missing from server output (likely DCE'd)`,
50+
).toBe(true);
51+
}
52+
expect(
53+
serverMarkerCounts.size,
54+
`Expected ${markers.length} markers, found ${serverMarkerCounts.size} in server output`,
55+
).toBe(markers.length);
56+
57+
// Verify no markers leak to client
58+
const clientMarkerCounts = await countSourceMarkers(clientOutputRoot);
59+
for (const [marker, files] of clientMarkerCounts) {
60+
expect(files.length, `Marker "${marker}" leaked to client output: ${files.join(", ")}`).toBe(
61+
0,
62+
);
63+
}
64+
});
65+
});
66+
67+
async function countSourceMarkers(rootDir: string) {
68+
const sourceFiles = await getFiles(rootDir, ALL_FILE_EXTENSIONS);
69+
const markerCounts = new Map<string, string[]>();
70+
for (const filePath of sourceFiles) {
71+
const content = await readFileContent(filePath);
72+
for (const [marker] of content.matchAll(SECRET_MARKER)) {
73+
const files = markerCounts.get(marker) ?? [];
74+
files.push(filePath);
75+
markerCounts.set(marker, files);
76+
}
77+
}
78+
return markerCounts;
79+
}
80+
81+
async function getFiles(dir: string, fileRegex: RegExp): Promise<string[]> {
82+
const entries = await readdir(dir, { recursive: true, withFileTypes: true });
83+
return entries
84+
.filter(e => e.isFile() && fileRegex.test(e.name))
85+
.map(e => path.join(e.parentPath, e.name));
86+
}
87+
88+
async function readFileContent(filePath: string) {
89+
if (filePath.endsWith(".br")) {
90+
return brotliDecompressSync(await readFile(filePath)).toString("utf-8");
91+
}
92+
93+
if (filePath.endsWith(".gz")) {
94+
return gunzipSync(await readFile(filePath)).toString("utf-8");
95+
}
96+
97+
return readFile(filePath, "utf-8");
98+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Prevents the provided value from being removed by dead-code elimination or aggressive
3+
* bundler/minifier optimizations by creating an inert side-effect reference. The side-effect
4+
* is intentionally never executed at runtime but ensures the value is
5+
* referenced so bundlers and minifiers won't drop it.
6+
*/
7+
export function keepAlive(value: unknown): void {
8+
if (Date.now() < 0) {
9+
// kept intentionally unreachable to avoid runtime side-effects
10+
console.log(value);
11+
}
12+
}

0 commit comments

Comments
 (0)