fix: Enhance path sanitization in getAudiobooksDir to prevent directory traversal and update getMigratedDocumentFileName tests to assert truncated- prefix and hash suffix.

This commit is contained in:
Richard R 2026-01-19 17:17:51 -07:00
parent 47d838039a
commit 9c941b57ed
2 changed files with 14 additions and 9 deletions

View file

@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import { readFile, writeFile, mkdir, unlink, rm, rename, readdir } from 'fs/promises'; import { readFile, writeFile, mkdir, unlink, rm, rename, readdir } from 'fs/promises';
import { existsSync, createReadStream } from 'fs'; import { existsSync, createReadStream } from 'fs';
import { basename, join } from 'path'; import { basename, join, resolve } from 'path';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { AUDIOBOOKS_V1_DIR, isAudiobooksV1Ready } from '@/lib/server/docstore'; import { AUDIOBOOKS_V1_DIR, isAudiobooksV1Ready } from '@/lib/server/docstore';
import { encodeChapterFileName, encodeChapterTitleTag, listStoredChapters, ffprobeAudio } from '@/lib/server/audiobook'; 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(); const raw = request.headers.get('x-openreader-test-namespace')?.trim();
if (!raw) return AUDIOBOOKS_V1_DIR; if (!raw) return AUDIOBOOKS_V1_DIR;
const safe = raw.replace(/[^a-zA-Z0-9._-]/g, ''); 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 { interface ConversionRequest {

View file

@ -26,13 +26,11 @@ test.describe('Docstore Filename Safety', () => {
const result = getMigratedDocumentFileName(id, specialName); const result = getMigratedDocumentFileName(id, specialName);
expect(result.length).toBeLessThanOrEqual(240); expect(result.length).toBeLessThanOrEqual(240);
if (result.includes('truncated-')) { expect(result).toContain('truncated-');
expect(result).toContain('truncated-'); // The implementation replaces the name with a hash, so it should NOT contain the original special chars
} else { // and might not contain % if the hash is hex.
// If it fits (depends on length), just ensure it is valid expect(result).toMatch(/truncated-[a-f0-9]{32}$/);
// 30 * 4 = 120 chars raw, but encoded? expect(result.startsWith(`${id}__`)).toBeTruthy();
// Let's ensure logic works.
}
}); });
test('should handle edge case length exactly', () => { test('should handle edge case length exactly', () => {