diff --git a/scripts/lib/lint-staged-common.js b/scripts/lib/lint-staged-common.js index 0cd3c17c8..6f2d45d55 100644 --- a/scripts/lib/lint-staged-common.js +++ b/scripts/lib/lint-staged-common.js @@ -318,7 +318,7 @@ function sanitizeFilePath(filePath, baseDir, allowedExtensions, maxFileSizeMB = // On Windows, lint-staged may pass paths like C:/path/to/file // Convert these to proper Windows format first - if (IS_WINDOWS && /^[A-Za-z]:[\\\\/]/.test(filePath)) { + if (IS_WINDOWS && /^[A-Za-z]:[\\/]/.test(filePath)) { normalizedPath = filePath.replaceAll("/", path.sep) } diff --git a/scripts/lint-any.js b/scripts/lint-any.js index da7ec42f3..7fd54df22 100644 --- a/scripts/lint-any.js +++ b/scripts/lint-any.js @@ -304,24 +304,32 @@ function runOxfmtCheck(files, fix) { * @param {string[]} files - Array of file paths * @param {boolean} fix - Whether to pass --fix * @param {boolean} clearCache - Whether to pass --clear-cache - * @returns {{ scriptArgs: string[], tempFile: string | null }} + * @returns {{ scriptArgs: string[], cleanup: (() => void) | null }} */ function buildScriptArgs(files, fix, clearCache) { const scriptArgs = [] - let tempFile = null + let cleanup = null const totalLength = files.reduce((sum, f) => sum + f.length + 1, 0) if (totalLength > MAX_ARG_LENGTH) { - const RADIX = 36 - const SUFFIX_LENGTH = 8 - tempFile = path.join( - os.tmpdir(), - `lint-files-${Date.now()}-${Math.random() - .toString(RADIX) - .slice(2, 2 + SUFFIX_LENGTH)}.txt`, - ) - fs.writeFileSync(tempFile, files.join("\n"), "utf8") - scriptArgs.push(`--files-from=${tempFile}`) + // Use mkdtempSync (private mode-0700 dir with cryptographic suffix) so the + // inner file cannot be predicted/preempted by another local process. + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "lint-files-")) + const tempFile = path.join(tempDir, "files.txt") + cleanup = () => { + try { + fs.rmSync(tempDir, { recursive: true, force: true }) + } catch { + /* Best-effort cleanup */ + } + } + try { + fs.writeFileSync(tempFile, files.join("\n"), "utf8") + scriptArgs.push(`--files-from=${tempFile}`) + } catch (error) { + cleanup() + throw error + } } else { scriptArgs.push(...files) } @@ -333,7 +341,7 @@ function buildScriptArgs(files, fix, clearCache) { scriptArgs.push("--clear-cache") } - return { scriptArgs, tempFile } + return { scriptArgs, cleanup } } /** @@ -352,7 +360,7 @@ function runLinter(script, files, description, fix, clearCache) { console.log(`\nšŸ” ${description} (${files.length} files)`) const scriptPath = path.join(__dirname, script) - const { scriptArgs, tempFile } = buildScriptArgs(files, fix, clearCache) + const { scriptArgs, cleanup } = buildScriptArgs(files, fix, clearCache) const result = spawnSync("node", [scriptPath, ...scriptArgs], { stdio: "inherit", @@ -360,13 +368,7 @@ function runLinter(script, files, description, fix, clearCache) { env, }) - if (tempFile) { - try { - fs.unlinkSync(tempFile) - } catch { - /* Best-effort cleanup */ - } - } + cleanup?.() if (result.error) { console.error(`āŒ Failed to run ${script}:`, result.error.message) @@ -392,7 +394,7 @@ function runLinterAsync(script, files, description, fix, clearCache) { console.log(`\nšŸ” ${description} (${files.length} files)`) const scriptPath = path.join(__dirname, script) - const { scriptArgs, tempFile } = buildScriptArgs(files, fix, clearCache) + const { scriptArgs, cleanup } = buildScriptArgs(files, fix, clearCache) return new Promise((resolve) => { const child = spawn("node", [scriptPath, ...scriptArgs], { @@ -402,13 +404,7 @@ function runLinterAsync(script, files, description, fix, clearCache) { }) child.on("close", (code, signal) => { - if (tempFile) { - try { - fs.unlinkSync(tempFile) - } catch { - /* Best-effort cleanup */ - } - } + cleanup?.() if (signal) { console.error(`āŒ ${script} terminated by signal ${signal}`) resolve(1) @@ -417,13 +413,7 @@ function runLinterAsync(script, files, description, fix, clearCache) { resolve(code ?? 1) }) child.on("error", (err) => { - if (tempFile) { - try { - fs.unlinkSync(tempFile) - } catch { - /* Best-effort cleanup */ - } - } + cleanup?.() console.error(`āŒ Failed to run ${script}:`, err.message) resolve(1) }) diff --git a/web/Program.cs b/web/Program.cs index 5fee3318b..c5136eec7 100644 --- a/web/Program.cs +++ b/web/Program.cs @@ -30,7 +30,7 @@ // Load .env.local for local development only (multiple-instance support) // Avoid loading in production - guard by ASPNETCORE_ENVIRONMENT. -var envPath = Path.Combine(Directory.GetCurrentDirectory(), "../.env.local"); +var envPath = Path.Join(Directory.GetCurrentDirectory(), "../.env.local"); var aspNetEnv = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); if (string.Equals(aspNetEnv, "Development", StringComparison.OrdinalIgnoreCase) && File.Exists(envPath)) @@ -476,7 +476,7 @@ void RegisterDbContext(string connectionStringKey) where TContext : Db { DefaultFileNames = new List { "index.html" }, FileProvider = new PhysicalFileProvider( - Path.Combine(builder.Environment.ContentRootPath, "wwwroot/vue")), + Path.Join(builder.Environment.ContentRootPath, "wwwroot/vue")), RequestPath = "/vue", RedirectToAppendTrailingSlash = true }); @@ -487,7 +487,7 @@ void RegisterDbContext(string connectionStringKey) where TContext : Db app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider( - Path.Combine(builder.Environment.ContentRootPath, "wwwroot/vue")), + Path.Join(builder.Environment.ContentRootPath, "wwwroot/vue")), RequestPath = "/2/vue" }); diff --git a/web/ViteProxyHelpers.cs b/web/ViteProxyHelpers.cs index fa939bead..c45d6562a 100644 --- a/web/ViteProxyHelpers.cs +++ b/web/ViteProxyHelpers.cs @@ -339,7 +339,7 @@ public static async Task HandleProxyError(HttpContext context, Exception ex, ILo } } - var physicalPath = Path.Combine(context.RequestServices.GetRequiredService().WebRootPath, + var physicalPath = Path.Join(context.RequestServices.GetRequiredService().WebRootPath, staticPath.TrimStart('/').Replace('/', Path.DirectorySeparatorChar)); // Prevent directory traversal: ensure the resolved physical path is within WebRootPath