mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix(security): close file-preview path-injection + tighten subtitle detection
- file-preview.ts: validate the :id URL param against a safe charset and restrict the original-name extension to alphanumerics before they feed filesystem paths (closes 9 CodeQL js/path-injection; defense-in-depth on top of the existing DB lookup). - media-input.ts: require the SRT/VTT timecode structure to detect a subtitle rather than a bare '-->' (closes CodeQL js/bad-tag-filter; also rejects non-subtitle files that merely contain '-->').
This commit is contained in:
@@ -106,7 +106,11 @@ export class MediaInputHandler implements InputHandler {
|
||||
throw new InputValidationError("Not a valid subtitle file (.srt, .vtt, .ass)");
|
||||
}
|
||||
const text = new TextDecoder("utf-8", { fatal: false }).decode(raw);
|
||||
const looksLikeSubtitle = /-->/.test(text) || /\[Script Info\]/i.test(text);
|
||||
// Detect the SRT/VTT cue timecode arrow (e.g. "00:00:01,000 --> 00:00:04,000")
|
||||
// by requiring the surrounding timestamp, not a bare "-->" (which also matches
|
||||
// non-subtitle text and tripped CodeQL's HTML-comment heuristic).
|
||||
const looksLikeSubtitle =
|
||||
/\d{1,2}:\d{2}(?::\d{2})?[.,]\d{3}\s*-->/.test(text) || /\[Script Info\]/i.test(text);
|
||||
if (!looksLikeSubtitle) {
|
||||
throw new InputValidationError("Not a valid subtitle file (.srt, .vtt, .ass)");
|
||||
}
|
||||
|
||||
@@ -67,6 +67,13 @@ export async function filePreviewRoutes(app: FastifyInstance): Promise<void> {
|
||||
|
||||
const { id } = request.params;
|
||||
|
||||
// Confine id to a safe charset (no path separators or dots) before it is
|
||||
// interpolated into filesystem paths below -- prevents path traversal via
|
||||
// the URL param. File ids are generated server-side (randomUUID).
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
|
||||
return reply.status(400).send({ error: "Invalid file id" });
|
||||
}
|
||||
|
||||
const [file] = await db.select().from(schema.userFiles).where(eq(schema.userFiles.id, id));
|
||||
|
||||
if (
|
||||
@@ -116,7 +123,9 @@ export async function filePreviewRoutes(app: FastifyInstance): Promise<void> {
|
||||
|
||||
// Copy to a temp file with the original extension so LibreOffice
|
||||
// can detect the format correctly from the extension.
|
||||
const origExt = file.originalName.match(/\.[^.]+$/)?.[0] ?? "";
|
||||
// Restrict to an alphanumeric extension (no path separators) -- the
|
||||
// original filename is user-controlled and feeds a filesystem path.
|
||||
const origExt = file.originalName.match(/\.[a-zA-Z0-9]+$/)?.[0] ?? "";
|
||||
const tempInput = join(previewDirPath(), `${id}-input${origExt}`);
|
||||
await copyFile(inputPath, tempInput);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user