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:
2
backend/Cargo.lock
generated
2
backend/Cargo.lock
generated
@@ -1558,7 +1558,7 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mangalord"
|
name = "mangalord"
|
||||||
version = "0.93.8"
|
version = "0.93.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mangalord"
|
name = "mangalord"
|
||||||
version = "0.93.8"
|
version = "0.93.9"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
default-run = "mangalord"
|
default-run = "mangalord"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mangalord-frontend",
|
"name": "mangalord-frontend",
|
||||||
"version": "0.93.8",
|
"version": "0.93.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,7 +1,24 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach, type MockInstance } from 'vitest';
|
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';
|
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', () => {
|
describe('request error envelope parsing', () => {
|
||||||
let fetchSpy: MockInstance<typeof globalThis.fetch>;
|
let fetchSpy: MockInstance<typeof globalThis.fetch>;
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,16 @@ const BASE = import.meta.env?.VITE_API_BASE ?? '/api';
|
|||||||
* Builds an absolute URL to the streaming `/files/{key}` endpoint so
|
* Builds an absolute URL to the streaming `/files/{key}` endpoint so
|
||||||
* components can use it directly in `<img src>` etc., without
|
* components can use it directly in `<img src>` etc., without
|
||||||
* reconstructing the API base in each call site.
|
* 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 {
|
export function fileUrl(key: string): string {
|
||||||
return `${BASE}/v1/files/${key}`;
|
const encoded = key.split('/').map(encodeURIComponent).join('/');
|
||||||
|
return `${BASE}/v1/files/${encoded}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user