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 { 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 {

View file

@ -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', () => {