fix(security): restrict local path extraction in media parser to prevent LFI (#4880)

* Media: restrict local path extraction to prevent LFI

* Lint: remove unused variable hasValidMediaOnLine
This commit is contained in:
Evan Otero
2026-01-30 21:44:11 -05:00
committed by GitHub
parent b17e6fdd07
commit c67df653b6
2 changed files with 36 additions and 19 deletions

View File

@@ -19,11 +19,9 @@ function isValidMedia(candidate: string, opts?: { allowSpaces?: boolean }) {
if (candidate.length > 4096) return false;
if (!opts?.allowSpaces && /\s/.test(candidate)) return false;
if (/^https?:\/\//i.test(candidate)) return true;
if (candidate.startsWith("/")) return true;
if (candidate.startsWith("./")) return true;
if (candidate.startsWith("../")) return true;
if (candidate.startsWith("~")) return true;
return false;
// Local paths: only allow safe relative paths starting with ./ that do not traverse upwards.
return candidate.startsWith("./") && !candidate.includes("..");
}
function unwrapQuoted(value: string): string | undefined {
@@ -85,10 +83,8 @@ export function splitMediaFromOutput(raw: string): {
continue;
}
foundMediaToken = true;
const pieces: string[] = [];
let cursor = 0;
let hasValidMedia = false;
for (const match of matches) {
const start = match.index ?? 0;
@@ -101,11 +97,13 @@ export function splitMediaFromOutput(raw: string): {
const mediaStartIndex = media.length;
let validCount = 0;
const invalidParts: string[] = [];
let hasValidMedia = false;
for (const part of parts) {
const candidate = normalizeMediaSource(cleanCandidate(part));
if (isValidMedia(candidate, unwrapped ? { allowSpaces: true } : undefined)) {
media.push(candidate);
hasValidMedia = true;
foundMediaToken = true;
validCount += 1;
} else {
invalidParts.push(part);
@@ -130,6 +128,7 @@ export function splitMediaFromOutput(raw: string): {
if (isValidMedia(fallback, { allowSpaces: true })) {
media.splice(mediaStartIndex, media.length - mediaStartIndex, fallback);
hasValidMedia = true;
foundMediaToken = true;
validCount = 1;
invalidParts.length = 0;
}
@@ -140,12 +139,18 @@ export function splitMediaFromOutput(raw: string): {
if (isValidMedia(fallback, { allowSpaces: true })) {
media.push(fallback);
hasValidMedia = true;
foundMediaToken = true;
invalidParts.length = 0;
}
}
if (hasValidMedia && invalidParts.length > 0) {
pieces.push(invalidParts.join(" "));
if (hasValidMedia) {
if (invalidParts.length > 0) {
pieces.push(invalidParts.join(" "));
}
} else {
// If no valid media was found in this match, keep the original token text.
pieces.push(match[0]);
}
cursor = start + match[0].length;