security(import): validate PDF magic bytes before S3 upload #618

Merged
marcel merged 10 commits from worktree-feat+issue-529-pdf-magic-bytes into main 2026-05-19 09:45:04 +02:00
3 changed files with 35 additions and 1 deletions
Showing only changes of commit f144b025b0 - Show all commits

View File

@@ -312,6 +312,7 @@ public class MassImportService {
return new ProcessResult(processed, skippedFiles);
}
// package-private: Mockito spy in tests can override to inject IOException
InputStream openFileStream(File file) throws IOException {
return new FileInputStream(file);
}

View File

@@ -78,7 +78,7 @@ function reasonLabel(code: string): string {
</div>
</summary>
<ul class="mt-3 space-y-1">
{#each importStatus.skippedFiles as skipped, i (i)}
{#each importStatus.skippedFiles as skipped (skipped.filename)}
<li class="font-mono text-sm text-ink-2">
{skipped.filename}{reasonLabel(skipped.reason)}
</li>

View File

@@ -199,4 +199,37 @@ describe('ImportStatusCard', () => {
await expect.element(getByTestId('skipped-count')).not.toBeInTheDocument();
});
it('does not show skipped section when FAILED even with skipped > 0', async () => {
const { getByTestId } = render(ImportStatusCard, {
props: {
importStatus: makeStatus({
state: 'FAILED',
statusCode: 'IMPORT_FAILED_INTERNAL',
skipped: 1,
skippedFiles: [{ filename: 'bad.pdf', reason: 'INVALID_PDF_SIGNATURE' }]
}),
ontrigger: () => {}
}
});
await expect.element(getByTestId('skipped-count')).not.toBeInTheDocument();
});
it('shows raw reason code for unknown skip reasons', async () => {
const { getByText } = render(ImportStatusCard, {
props: {
importStatus: makeStatus({
state: 'DONE',
statusCode: 'IMPORT_DONE',
processed: 1,
skipped: 1,
skippedFiles: [{ filename: 'odd.pdf', reason: 'SOME_FUTURE_CODE' }]
}),
ontrigger: () => {}
}
});
await expect.element(getByText('SOME_FUTURE_CODE', { exact: false })).toBeInTheDocument();
});
});