mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix(security): basename-sanitize file-preview paths (CodeQL js/path-injection)
The resolve()+startsWith containment check was correct but CodeQL did not recognize it. Apply path.basename() to the name in resolveWithinPreviewDir -- a sanitizer CodeQL recognizes -- so every id-derived preview path is provably a single filename inside the preview dir. Behaviour is unchanged for valid ids (already charset-validated); containment check kept as a backstop.
This commit is contained in:
@@ -9,7 +9,7 @@ import { randomUUID } from "node:crypto";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { access, copyFile, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve, sep } from "node:path";
|
||||
import { basename, join, resolve, sep } from "node:path";
|
||||
import { convertDocument, sofficeAvailable } from "@snapotter/doc-engine";
|
||||
import { runFfmpeg } from "@snapotter/media-engine";
|
||||
import { eq } from "drizzle-orm";
|
||||
@@ -39,8 +39,11 @@ async function ensurePreviewDir(): Promise<void> {
|
||||
* check is the authoritative path-traversal barrier for every preview path.
|
||||
*/
|
||||
function resolveWithinPreviewDir(name: string): string {
|
||||
// basename() strips any directory component, so the result can only ever be a
|
||||
// single filename inside the preview dir; the containment check is a
|
||||
// defense-in-depth backstop.
|
||||
const base = resolve(previewDirPath());
|
||||
const resolved = resolve(base, name);
|
||||
const resolved = join(base, basename(name));
|
||||
if (resolved !== base && !resolved.startsWith(base + sep)) {
|
||||
throw new Error("Preview path escapes the preview directory");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user