From 1c201b077212c72f9ae03f5fb2409815dae15264 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 4 Nov 2026 03:04:12 +0100 Subject: [PATCH] fix: stop private files with odd casing and links reaching Docker builds --- lib/__tests__/standalone-guard.test.ts | 32 +++++++++++++++++++++++++- scripts/standalone-guard.mjs | 9 +++++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/lib/__tests__/standalone-guard.test.ts b/lib/__tests__/standalone-guard.test.ts index 347d8f526..32965e957 100644 --- a/lib/__tests__/standalone-guard.test.ts +++ b/lib/__tests__/standalone-guard.test.ts @@ -1,6 +1,6 @@ // @vitest-environment node import { afterEach, describe, expect, it } from 'vitest'; -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'; import { spawnSync } from 'node:child_process'; import os from 'node:os'; import path from 'node:path'; @@ -120,6 +120,36 @@ describe('standalone guard check', () => { expect(findForbidden(dir)).toEqual(['worktrees/webmail/data/']); }); + it('goes by the name in any case, as a case-insensitive disk and Docker context would', () => { + const dir = tempDir(); + put(dir, 'server.js'); + put(dir, '.ENV', 'SECRET=1'); + put(dir, 'Plugin-Signing.KEY', 'SECRET=1'); + put(dir, 'Data/admin.json'); + put(dir, 'LOCAL-DATA/a'); + put(dir, 'Repos/a'); + expect(findForbidden(dir)).toEqual(['.ENV', 'Data/', 'LOCAL-DATA/', 'Plugin-Signing.KEY', 'Repos/']); + }); + + it('refuses a link: it would ship whatever it points at', () => { + const dir = tempDir(); + const outside = tempDir(); + put(outside, 'admin.json'); + put(dir, 'server.js'); + symlinkSync(outside, path.join(dir, 'state'), 'junction'); + expect(findForbidden(dir)).toEqual(['state (link)']); + }); + + it('leaves the links of .next and node_modules alone: Turbopack makes those on purpose', () => { + const dir = tempDir(); + const outside = tempDir(); + put(dir, 'node_modules/a/index.js'); + mkdirSync(path.join(dir, '.next', 'node_modules'), { recursive: true }); + symlinkSync(path.join(dir, 'node_modules', 'a'), path.join(dir, '.next', 'node_modules', 'a-1234'), 'junction'); + symlinkSync(outside, path.join(dir, 'node_modules', 'b'), 'junction'); + expect(findForbidden(dir)).toEqual([]); + }); + it('removes the .env files Next copies next to server.js, then passes', () => { const dir = tempDir(); put(dir, 'worktrees/webmail/server.js'); diff --git a/scripts/standalone-guard.mjs b/scripts/standalone-guard.mjs index 192cc94e8..80a268e1d 100644 --- a/scripts/standalone-guard.mjs +++ b/scripts/standalone-guard.mjs @@ -25,7 +25,7 @@ const TRACE_ALLOWED = new Set(['node_modules', '.next', 'public', 'locales', 'pa /** Never in a standalone tree, wherever it sits outside node_modules. */ const FORBIDDEN_DIRS = new Set(['data', 'local-data', 'repos']); -const FORBIDDEN_FILE = /^(\.env.*|plugin-signing\.key)$/; +const FORBIDDEN_FILE = /^(\.env.*|plugin-signing\.key)$/i; function toPosix(p) { return p.split(path.sep).join('/'); @@ -106,10 +106,13 @@ export function findForbidden(standaloneDir) { const visit = (dir) => { for (const entry of readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); - if (entry.isDirectory()) { + if (entry.isSymbolicLink()) { + // Not .next's or a package's, which aren't visited: Turbopack links those on purpose. + if (entry.name !== 'node_modules' && entry.name !== '.next') found.push(`${toPosix(path.relative(standaloneDir, full))} (link)`); + } else if (entry.isDirectory()) { // Packages may hold anything, and .next/server has routes named after data. if (entry.name === 'node_modules' || entry.name === '.next') continue; - if (FORBIDDEN_DIRS.has(entry.name)) found.push(toPosix(path.relative(standaloneDir, full)) + '/'); + if (FORBIDDEN_DIRS.has(entry.name.toLowerCase())) found.push(toPosix(path.relative(standaloneDir, full)) + '/'); else visit(full); } else if (FORBIDDEN_FILE.test(entry.name)) { found.push(toPosix(path.relative(standaloneDir, full)));