Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions e2e/csp.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

import { test, expect } from '@playwright/test'

test('renders a PDF under a CSP without unsafe-eval', async ({ page }) => {
const evalErrors: string[] = []

await page.route('http://localhost:5173/', async (route) => {
const response = await route.fetch()
await route.fulfill({
response,
headers: {
...response.headers(),
'content-security-policy': [
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob:",
"connect-src 'self' https://mozilla.github.io ws://localhost:5173",
"worker-src 'self' blob:",
"font-src 'self' data:",
].join('; '),
},
})
})

page.on('console', (message) => {
const text = message.text()
if (text.includes('unsafe-eval') || text.includes('call to eval() blocked by CSP')) {
evalErrors.push(text)
}
})

page.on('pageerror', (error) => {
if (error.message.includes('eval')) {
evalErrors.push(error.message)
}
})

await page.goto('/')
await page.getByRole('button', { name: 'Load sample PDF' }).click()
await expect(page.locator('canvas').first()).toBeVisible()

expect(evalErrors).toEqual([])
})
17 changes: 13 additions & 4 deletions src/utils/asyncReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ async function getSharedWorker(pdfjs: PdfjsModule): Promise<PDFWorker> {
return sharedWorker
}

function getSafePdfjsOptions(options: Record<string, unknown>) {
return {
...options,
isEvalSupported: false,
enableScripting: false,
}
}

export function setWorkerPath(path: string) {
workerSrcOverride = path
sharedWorker = null
Expand Down Expand Up @@ -97,19 +105,20 @@ export async function readAsPDF(
): Promise<PDFDocumentProxy> {
const pdfjs = await loadPdfjs()
const worker = await getSharedWorker(pdfjs)
const pdfjsOptions = getSafePdfjsOptions(options)
const isArrayBuffer = file instanceof ArrayBuffer
const isView = ArrayBuffer.isView(file)
const isBlob = typeof Blob !== 'undefined' && file instanceof Blob

if (file && typeof file === 'object' && !isArrayBuffer && !isView && !isBlob) {
return pdfjs.getDocument({ ...(file as Record<string, unknown>), ...options, worker }).promise
return pdfjs.getDocument({ ...(file as Record<string, unknown>), ...pdfjsOptions, worker }).promise
}
if (typeof file === 'string') {
return pdfjs.getDocument({ url: file, ...options, worker }).promise
return pdfjs.getDocument({ url: file, ...pdfjsOptions, worker }).promise
}
if (isBlob) {
const data = await readAsArrayBuffer(file as Blob)
return pdfjs.getDocument({ data, ...options, worker }).promise
return pdfjs.getDocument({ data, ...pdfjsOptions, worker }).promise
}
const data = isArrayBuffer
? (file as ArrayBuffer)
Expand All @@ -119,5 +128,5 @@ export async function readAsPDF(
(file as ArrayBufferView).byteLength
)

return pdfjs.getDocument({ data, ...options, worker }).promise
return pdfjs.getDocument({ data, ...pdfjsOptions, worker }).promise
}
71 changes: 71 additions & 0 deletions tests/utils/asyncReader.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

import { beforeEach, describe, expect, it, vi } from 'vitest'
import * as pdfjs from 'pdfjs-dist'

vi.mock('pdfjs-dist', () => {
const GlobalWorkerOptions = {
workerSrc: '/pdf.worker.min.mjs',
}
const PDFWorker = class PDFWorker {}
const getDocument = vi.fn(() => ({
promise: Promise.resolve({}),
}))
const mockedModule = {
GlobalWorkerOptions,
PDFWorker,
getDocument,
}

return {
...mockedModule,
default: mockedModule,
}
})

vi.mock('pdfjs-dist/legacy/build/pdf.worker.min.mjs?url', () => ({
default: '/pdf.worker.min.mjs',
}))

import { readAsPDF } from '../../src/utils/asyncReader'

describe('readAsPDF', () => {
beforeEach(() => {
vi.clearAllMocks()
})

it('disables eval and PDF scripting by default', async () => {
await readAsPDF('https://example.com/sample.pdf')

expect(pdfjs.getDocument).toHaveBeenCalledWith(expect.objectContaining({
url: 'https://example.com/sample.pdf',
isEvalSupported: false,
enableScripting: false,
}))
})

it('does not allow callers to re-enable eval or PDF scripting', async () => {
await readAsPDF('https://example.com/sample.pdf', {
isEvalSupported: true,
enableScripting: true,
})

expect(pdfjs.getDocument).toHaveBeenCalledWith(expect.objectContaining({
isEvalSupported: false,
enableScripting: false,
}))
})

it('applies the safe defaults when loading binary PDF data', async () => {
const data = new Uint8Array([1, 2, 3])

await readAsPDF(data)

expect(pdfjs.getDocument).toHaveBeenCalledWith(expect.objectContaining({
data: expect.any(Uint8Array),
isEvalSupported: false,
enableScripting: false,
}))
})
})
File renamed without changes.
Loading