From 141cd52f7e6eb19ee69e007bd8e34bb4d2cdf9fd Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Fri, 3 Jul 2026 21:18:34 +0200 Subject: [PATCH] fix(frontend): percent-encode fileUrl key segments fileUrl interpolated the raw storage key into the path, so a key segment containing a reserved character (`?`, `#`, `%`, space) could be reinterpreted as a query/fragment delimiter. Encode each `/`-separated segment with encodeURIComponent, keeping slashes as literal path separators. Keys are backend-generated today, so this is defence-in-depth. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/Cargo.lock | 2 +- backend/Cargo.toml | 2 +- frontend/package.json | 2 +- frontend/src/lib/api/client.test.ts | 19 ++++++++++++++++++- frontend/src/lib/api/client.ts | 9 ++++++++- 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 91317d3..a0ae3d6 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1558,7 +1558,7 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mangalord" -version = "0.93.8" +version = "0.93.9" dependencies = [ "anyhow", "argon2", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index b0eef30..94b2e05 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mangalord" -version = "0.93.8" +version = "0.93.9" edition = "2021" default-run = "mangalord" diff --git a/frontend/package.json b/frontend/package.json index 2ea9ac7..d093627 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mangalord-frontend", - "version": "0.93.8", + "version": "0.93.9", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/lib/api/client.test.ts b/frontend/src/lib/api/client.test.ts index dc5bc3f..2bd7c1b 100644 --- a/frontend/src/lib/api/client.test.ts +++ b/frontend/src/lib/api/client.test.ts @@ -1,7 +1,24 @@ import { describe, it, expect, vi, beforeEach, afterEach, type MockInstance } from 'vitest'; -import { ApiError, request, setOn401Hook } from './client'; +import { ApiError, request, setOn401Hook, fileUrl } from './client'; import { getManga } from './mangas'; +describe('fileUrl', () => { + it('keeps a normal /-separated key path literal', () => { + expect(fileUrl('mangas/abc/chapters/def/pages/0001.jpg')).toBe( + '/api/v1/files/mangas/abc/chapters/def/pages/0001.jpg' + ); + }); + + it('percent-encodes reserved characters within a segment', () => { + // `?`, `#`, `%`, and spaces inside a segment must be encoded so they + // can't be reinterpreted as query/fragment delimiters — while the + // slashes stay as literal path separators. + expect(fileUrl('weird key/a?b#c%d/x y.png')).toBe( + '/api/v1/files/weird%20key/a%3Fb%23c%25d/x%20y.png' + ); + }); +}); + describe('request error envelope parsing', () => { let fetchSpy: MockInstance; diff --git a/frontend/src/lib/api/client.ts b/frontend/src/lib/api/client.ts index 22df9d3..da01af5 100644 --- a/frontend/src/lib/api/client.ts +++ b/frontend/src/lib/api/client.ts @@ -7,9 +7,16 @@ const BASE = import.meta.env?.VITE_API_BASE ?? '/api'; * Builds an absolute URL to the streaming `/files/{key}` endpoint so * components can use it directly in `` etc., without * reconstructing the API base in each call site. + * + * Storage keys are `/`-separated paths, so each segment is percent-encoded + * individually — the slashes stay literal path separators while any + * reserved character inside a segment (`?`, `#`, `%`, space, …) can't be + * reinterpreted as a query/fragment delimiter. Keys are backend-generated + * today, so this is defence-in-depth against a future key source. */ export function fileUrl(key: string): string { - return `${BASE}/v1/files/${key}`; + const encoded = key.split('/').map(encodeURIComponent).join('/'); + return `${BASE}/v1/files/${encoded}`; } /**