Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 89029b2c6fecf1f53b9ec1a2afe2c6d2f5956e52
parent dd53e9831b1d30ac760eb80c6173f5cca40bb471
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sun, 20 Sep 2026 19:41:11 -0400

fetch-window: refuse a bare `..` as a slug or an id

/^[\w.-]+$/ was written to stop traversal and allows a dot, so ".." passed the
pattern that exists to reject it. It would have died on a missing config
rather than escaping, which is luck, not a guard.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Meditor/app/api/media/fetch-window/route.ts | 11+++++++----
Meditor/e2e/fetch-window.spec.ts | 2++
2 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/editor/app/api/media/fetch-window/route.ts b/editor/app/api/media/fetch-window/route.ts @@ -24,7 +24,12 @@ export const dynamic = "force-dynamic"; // // The download PAUSE does not gate it; see fetchWindowAction for why. +// Both land in a filesystem path. Anchored — and `.` / `..` are refused +// separately, because the class allows a dot and "`..`" alone would otherwise +// pass a pattern written to stop traversal. const ID_RE = /^[\w.-]+$/; +const isPathSegment = (v: string): boolean => + ID_RE.test(v) && v !== "." && v !== ".."; type Body = { channelSlug?: unknown; @@ -98,15 +103,13 @@ export async function POST(request: Request) { const channelSlug = str(body.channelSlug); const videoId = str(body.videoId); - // Both land in a filesystem path. Anchored, so `..` and a separator are - // refused at the door rather than by whatever the path math happens to do. - if (!channelSlug || !ID_RE.test(channelSlug)) { + if (!channelSlug || !isPathSegment(channelSlug)) { return NextResponse.json( { error: "channelSlug is required and must match /^[\\w.-]+$/" }, { status: 400 }, ); } - if (!videoId || !ID_RE.test(videoId)) { + if (!videoId || !isPathSegment(videoId)) { return NextResponse.json( { error: "videoId is required and must match /^[\\w.-]+$/" }, { status: 400 }, diff --git a/editor/e2e/fetch-window.spec.ts b/editor/e2e/fetch-window.spec.ts @@ -89,6 +89,8 @@ test("a window is refused unless it names who asked and a sane span", async ({ const cases: Array<[Record<string, unknown>, string]> = [ [{ channelSlug: "../escape", videoId: VIDEO, from: 1, to: 2, requestedBy: "umtool" }, "traversal in the slug"], [{ channelSlug: SLUG, videoId: "a/b", from: 1, to: 2, requestedBy: "umtool" }, "separator in the id"], + [{ channelSlug: "..", videoId: VIDEO, from: 1, to: 2, requestedBy: "umtool" }, "a bare .. slug"], + [{ channelSlug: SLUG, videoId: "..", from: 1, to: 2, requestedBy: "umtool" }, "a bare .. id"], [{ channelSlug: SLUG, videoId: VIDEO, from: 42, to: 12, requestedBy: "umtool" }, "backwards window"], [{ channelSlug: SLUG, videoId: VIDEO, from: 0, to: 1000, requestedBy: "umtool" }, "past the span cap"], [{ channelSlug: SLUG, videoId: VIDEO, from: 1, to: 2 }, "anonymous"],