diff --git a/src/app/api/audiobook/route.ts b/src/app/api/audiobook/route.ts index d2e639d..9d6d0f3 100644 --- a/src/app/api/audiobook/route.ts +++ b/src/app/api/audiobook/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { spawn } from 'child_process'; import { readFile, writeFile, mkdir, unlink, rm, rename, readdir } from 'fs/promises'; import { existsSync, createReadStream } from 'fs'; -import { basename, join } from 'path'; +import { basename, join, resolve } from 'path'; import { randomUUID } from 'crypto'; import { AUDIOBOOKS_V1_DIR, isAudiobooksV1Ready } from '@/lib/server/docstore'; import { encodeChapterFileName, encodeChapterTitleTag, listStoredChapters, ffprobeAudio } from '@/lib/server/audiobook'; @@ -15,7 +15,14 @@ function getAudiobooksRootDir(request: NextRequest): string { const raw = request.headers.get('x-openreader-test-namespace')?.trim(); if (!raw) return AUDIOBOOKS_V1_DIR; const safe = raw.replace(/[^a-zA-Z0-9._-]/g, ''); - return safe ? join(AUDIOBOOKS_V1_DIR, safe) : AUDIOBOOKS_V1_DIR; + if (!safe || safe === '.' || safe === '..' || safe.includes('..')) { + return AUDIOBOOKS_V1_DIR; + } + const resolved = resolve(AUDIOBOOKS_V1_DIR, safe); + if (!resolved.startsWith(resolve(AUDIOBOOKS_V1_DIR) + '/')) { + return AUDIOBOOKS_V1_DIR; + } + return resolved; } interface ConversionRequest { diff --git a/tests/docstore_unit.spec.ts b/tests/docstore_unit.spec.ts index fda3d92..ccd4879 100644 --- a/tests/docstore_unit.spec.ts +++ b/tests/docstore_unit.spec.ts @@ -26,13 +26,11 @@ test.describe('Docstore Filename Safety', () => { const result = getMigratedDocumentFileName(id, specialName); expect(result.length).toBeLessThanOrEqual(240); - if (result.includes('truncated-')) { - expect(result).toContain('truncated-'); - } else { - // If it fits (depends on length), just ensure it is valid - // 30 * 4 = 120 chars raw, but encoded? - // Let's ensure logic works. - } + expect(result).toContain('truncated-'); + // The implementation replaces the name with a hash, so it should NOT contain the original special chars + // and might not contain % if the hash is hex. + expect(result).toMatch(/truncated-[a-f0-9]{32}$/); + expect(result.startsWith(`${id}__`)).toBeTruthy(); }); test('should handle edge case length exactly', () => {