mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-05 01:28:15 +02:00
fix(design): reload guard rejects directory paths
design/src/serve.ts:200-212 used to accept a path that resolved to the
allowedDir itself (the OR branch `|| resolvedReload === allowedDir`),
which then crashed readFileSync with EISDIR. Now:
1. startsWith(allowedDir + path.sep) must pass — rejects the dir itself
and anything outside (403).
2. statSync(resolvedReload).isFile() must pass — rejects subdirectories
inside allowedDir with a clear "Path must be a file" 400.
The test stub in serve.test.ts mirrors prod; both updated, plus two new
test cases for the previously-broken paths. Codex caught this in the
plan-review pass; it's a latent bug in shipping code, not a regression
from the daemon work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+13
-7
@@ -197,19 +197,25 @@ export async function serve(options: ServeOptions): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
// Security: resolve symlinks and validate the reload path is within the
|
||||
// allowed directory (anchored to the initial HTML file's parent).
|
||||
// Prevents path traversal via /api/reload reading arbitrary files.
|
||||
// Security: resolve symlinks and validate the reload path is a FILE
|
||||
// inside the allowed directory (anchored to the initial HTML file's
|
||||
// parent). Prevents path traversal via /api/reload reading arbitrary
|
||||
// files. A path resolving to the allowedDir itself (a directory) used
|
||||
// to pass the guard and then crash readFileSync with EISDIR — reject
|
||||
// it explicitly with a clear 400 instead.
|
||||
const resolvedReload = fs.realpathSync(path.resolve(newHtmlPath));
|
||||
if (
|
||||
!resolvedReload.startsWith(allowedDir + path.sep) &&
|
||||
resolvedReload !== allowedDir
|
||||
) {
|
||||
if (!resolvedReload.startsWith(allowedDir + path.sep)) {
|
||||
return Response.json(
|
||||
{ error: `Path must be within: ${allowedDir}` },
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
if (!fs.statSync(resolvedReload).isFile()) {
|
||||
return Response.json(
|
||||
{ error: `Path must be a file, not a directory: ${newHtmlPath}` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Swap the HTML content
|
||||
htmlContent = fs.readFileSync(resolvedReload, "utf-8");
|
||||
|
||||
Reference in New Issue
Block a user