Files
elegalsoftware/apps/api/test/upload-filename.test.ts
T
Leon SerfatyandClaude Opus 5 eb36b81dc9 Superadmin impersonation, hardened auth/upload paths, dependency updates
- Add superadmin impersonation: sessions.impersonated_by (migration 0003) is
  stamped onto audit rows so impersonated actions are attributable, with a
  persistent ImpersonationBanner in the app shell.
- Harden auth and upload handling across routes (safe redirect targets,
  filename sanitization, checkout grant handling).
- Update dependencies: Sentry 8 -> 10, @fastify/static 8 -> 10,
  react-router-dom 6.30.6; add find-my-way / fast-uri overrides.
- Add tests for safe-next, checkout-grant, and upload-filename.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 10:25:39 -04:00

82 lines
3.5 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import path from 'node:path';
// Mirrors the helpers in src/routes/documents.ts. They aren't exported (the route module pulls in
// env + DB on import, which this suite deliberately avoids), so the logic is restated verbatim and
// asserted against the shapes that matter. If the route changes, change this with it.
const MAX_NAME_LENGTH = 200;
function safeDisplayName(filename: string): string {
const base = path.basename(filename.replace(/\\/g, '/'));
// eslint-disable-next-line no-control-regex
const cleaned = base.replace(/[\u0000-\u001f\u007f]/g, '').trim();
return (cleaned || 'document').slice(0, MAX_NAME_LENGTH);
}
function safeExtension(filename: string): string {
const ext = path.extname(safeDisplayName(filename)).toLowerCase();
return /^\.[a-z0-9]{1,8}$/.test(ext) ? ext : '';
}
describe('upload filename sanitisation', () => {
it('keeps an ordinary name and extension intact', () => {
expect(safeDisplayName('Engagement Letter.pdf')).toBe('Engagement Letter.pdf');
expect(safeExtension('Engagement Letter.pdf')).toBe('.pdf');
});
it('strips directory components from posix and windows paths', () => {
expect(safeDisplayName('../../etc/passwd')).toBe('passwd');
expect(safeDisplayName('..\\..\\windows\\system32\\config')).toBe('config');
expect(safeDisplayName('/absolute/path/deed.pdf')).toBe('deed.pdf');
});
it('never lets a path separator reach the storage key', () => {
for (const name of ['a/b.pdf', 'a\\b.pdf', '../x.pdf', '..%2fx.pdf']) {
expect(safeExtension(name)).not.toContain('/');
expect(safeExtension(name)).not.toContain('\\');
expect(safeDisplayName(name)).not.toContain('/');
expect(safeDisplayName(name)).not.toContain('\\');
}
});
it('removes control characters, including NUL and CRLF', () => {
expect(safeDisplayName('in\rvoice\n.pdf')).toBe('invoice.pdf');
expect(safeDisplayName('a\u0000b.pdf')).toBe('ab.pdf');
expect(safeDisplayName('tab\tsep.pdf')).toBe('tabsep.pdf');
expect(safeDisplayName('del\u007fx.pdf')).toBe('delx.pdf');
});
it('bounds the stored display name', () => {
const long = `${'x'.repeat(500)}.pdf`;
expect(safeDisplayName(long).length).toBe(MAX_NAME_LENGTH);
});
it('falls back to a placeholder rather than an empty name', () => {
expect(safeDisplayName('')).toBe('document');
expect(safeDisplayName(' ')).toBe('document');
expect(safeDisplayName('')).toBe('document');
});
it('drops unusual or overlong extensions instead of concatenating them', () => {
expect(safeExtension('payload.')).toBe('');
expect(safeExtension('noextension')).toBe('');
expect(safeExtension('x.thisextensioniswaytoolong')).toBe('');
});
it('normalises extension case', () => {
expect(safeExtension('SCAN.PDF')).toBe('.pdf');
});
it('produces a storage key that stays inside the firm/case prefix', () => {
const firmId = '11111111-1111-1111-1111-111111111111';
const caseId = '22222222-2222-2222-2222-222222222222';
const docId = '33333333-3333-3333-3333-333333333333';
for (const hostile of ['../../../../etc/passwd', 'a\\..\\..\\b.pdf', 'x.pdf/../../y']) {
const key = `${firmId}/${caseId}/${docId}${safeExtension(hostile)}`;
expect(key.startsWith(`${firmId}/${caseId}/`)).toBe(true);
expect(key.split('/').length).toBe(3);
expect(key).not.toContain('..');
}
});
});