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:
SnapOtter
2026-06-21 11:31:39 +08:00
parent a6837b687a
commit c1cd8712f4
2 changed files with 15 additions and 2 deletions
+5 -1
View File
@@ -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)");
}
+10 -1
View File
@@ -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);