mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-19 02:42:29 +08:00
fix(security): validateOutputPath symlink bypass — check file-level symlinks
validateOutputPath() previously only resolved symlinks on the parent directory. A symlink at /tmp/evil.png → /etc/crontab passed the parent check (parent is /tmp, which is safe) but the write followed the symlink outside safe dirs. Add lstatSync() check: if the target file exists and is a symlink, resolve through it and verify the real target is within SAFE_DIRECTORIES. ENOENT (file doesn't exist yet) falls through to the existing parent-dir check. Closes #921 Co-Authored-By: Yunsu <Hybirdss@users.noreply.github.com>
This commit is contained in:
@@ -33,7 +33,26 @@ const TEMP_ONLY = [TEMP_DIR].map(d => {
|
|||||||
export function validateOutputPath(filePath: string): void {
|
export function validateOutputPath(filePath: string): void {
|
||||||
const resolved = path.resolve(filePath);
|
const resolved = path.resolve(filePath);
|
||||||
|
|
||||||
// Resolve real path of the parent directory to catch symlinks.
|
// If the target already exists and is a symlink, resolve through it.
|
||||||
|
// Without this, a symlink at /tmp/evil.png → /etc/crontab passes the
|
||||||
|
// parent-directory check (parent is /tmp, which is safe) but the actual
|
||||||
|
// write follows the symlink to /etc/crontab.
|
||||||
|
try {
|
||||||
|
const stat = fs.lstatSync(resolved);
|
||||||
|
if (stat.isSymbolicLink()) {
|
||||||
|
const realTarget = fs.realpathSync(resolved);
|
||||||
|
const isSafe = SAFE_DIRECTORIES.some(dir => isPathWithin(realTarget, dir));
|
||||||
|
if (!isSafe) {
|
||||||
|
throw new Error(`Path must be within: ${SAFE_DIRECTORIES.join(', ')}`);
|
||||||
|
}
|
||||||
|
return; // symlink target verified, no need to check parent
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
// ENOENT = file doesn't exist yet, fall through to parent-dir check
|
||||||
|
if (e.code !== 'ENOENT') throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For new files (no existing symlink), verify the parent directory.
|
||||||
// The file itself may not exist yet (e.g., screenshot output).
|
// The file itself may not exist yet (e.g., screenshot output).
|
||||||
// This also handles macOS /tmp → /private/tmp transparently.
|
// This also handles macOS /tmp → /private/tmp transparently.
|
||||||
let dir = path.dirname(resolved);
|
let dir = path.dirname(resolved);
|
||||||
|
|||||||
Reference in New Issue
Block a user