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) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-03 21:18:34 +02:00
parent 83a9ab40cd
commit 141cd52f7e
5 changed files with 29 additions and 5 deletions

View File

@@ -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<typeof globalThis.fetch>;

View File

@@ -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 `<img src>` 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}`;
}
/**