From 3c005f2039db26064f6ad95cdf2b26d979461ffe Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:05:19 -0300 Subject: [PATCH 1/9] fix: disable PDF.js eval and scripting Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/utils/asyncReader.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/utils/asyncReader.ts b/src/utils/asyncReader.ts index 9be409e..f2cb059 100644 --- a/src/utils/asyncReader.ts +++ b/src/utils/asyncReader.ts @@ -52,6 +52,14 @@ async function getSharedWorker(pdfjs: PdfjsModule): Promise { return sharedWorker } +function getSafePdfjsOptions(options: Record) { + return { + ...options, + isEvalSupported: false, + enableScripting: false, + } +} + export function setWorkerPath(path: string) { workerSrcOverride = path sharedWorker = null @@ -97,19 +105,20 @@ export async function readAsPDF( ): Promise { 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), ...options, worker }).promise + return pdfjs.getDocument({ ...(file as Record), ...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) @@ -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 } From cbb95118766480a5a56565df60fbd464edd5e879 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:05:19 -0300 Subject: [PATCH 2/9] test: cover safe PDF.js options Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/utils/asyncReader.spec.ts | 61 +++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/utils/asyncReader.spec.ts diff --git a/tests/utils/asyncReader.spec.ts b/tests/utils/asyncReader.spec.ts new file mode 100644 index 0000000..6763988 --- /dev/null +++ b/tests/utils/asyncReader.spec.ts @@ -0,0 +1,61 @@ +// 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', () => ({ + GlobalWorkerOptions: { + workerSrc: '/pdf.worker.min.mjs', + }, + PDFWorker: class PDFWorker {}, + getDocument: vi.fn(() => ({ + promise: Promise.resolve({}), + })), +})) + +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, + })) + }) +}) From fec2d7d5e81a0580c7b5c8a5d6d8aeb2411865b7 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:05:19 -0300 Subject: [PATCH 3/9] test: add strict CSP demo page Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- examples/csp.html | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 examples/csp.html diff --git a/examples/csp.html b/examples/csp.html new file mode 100644 index 0000000..5d1b69f --- /dev/null +++ b/examples/csp.html @@ -0,0 +1,18 @@ + + + + + + + + PDF Elements CSP Demo + + + +
+ + + From 9d5367b6c5838081f09be03450cc4255507803f5 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:05:19 -0300 Subject: [PATCH 4/9] test: verify PDF rendering under strict CSP Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- e2e/csp.spec.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 e2e/csp.spec.ts diff --git a/e2e/csp.spec.ts b/e2e/csp.spec.ts new file mode 100644 index 0000000..5b286c3 --- /dev/null +++ b/e2e/csp.spec.ts @@ -0,0 +1,27 @@ +// 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[] = [] + + 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('/csp.html') + await page.getByRole('button', { name: 'Load sample PDF' }).click() + await expect(page.locator('canvas').first()).toBeVisible() + + expect(evalErrors).toEqual([]) +}) From 6a9219b3a5ed527bbcd2f24ce398aeb2eef26be0 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:05:19 -0300 Subject: [PATCH 5/9] test: apply CSP in Playwright scenario Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- e2e/csp.spec.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/e2e/csp.spec.ts b/e2e/csp.spec.ts index 5b286c3..0ee3ef1 100644 --- a/e2e/csp.spec.ts +++ b/e2e/csp.spec.ts @@ -6,6 +6,25 @@ 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')) { @@ -19,7 +38,7 @@ test('renders a PDF under a CSP without unsafe-eval', async ({ page }) => { } }) - await page.goto('/csp.html') + await page.goto('/') await page.getByRole('button', { name: 'Load sample PDF' }).click() await expect(page.locator('canvas').first()).toBeVisible() From d18d59889f49a166003e22a694e9db7fd186b886 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:05:19 -0300 Subject: [PATCH 6/9] test: remove dedicated CSP example page Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- examples/csp.html | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 examples/csp.html diff --git a/examples/csp.html b/examples/csp.html deleted file mode 100644 index 5d1b69f..0000000 --- a/examples/csp.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - PDF Elements CSP Demo - - - -
- - - From 301246e7b2ac2860f36f4674d18d20eb0b190153 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:05:19 -0300 Subject: [PATCH 7/9] test: fix PDF.js module mock Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/utils/asyncReader.spec.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/tests/utils/asyncReader.spec.ts b/tests/utils/asyncReader.spec.ts index 6763988..d92d3f5 100644 --- a/tests/utils/asyncReader.spec.ts +++ b/tests/utils/asyncReader.spec.ts @@ -4,15 +4,25 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import * as pdfjs from 'pdfjs-dist' -vi.mock('pdfjs-dist', () => ({ - GlobalWorkerOptions: { +vi.mock('pdfjs-dist', () => { + const GlobalWorkerOptions = { workerSrc: '/pdf.worker.min.mjs', - }, - PDFWorker: class PDFWorker {}, - getDocument: vi.fn(() => ({ + } + 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', From 67fb73e979c0c04598790da635c3ba4ebb1fc72c Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:05:19 -0300 Subject: [PATCH 8/9] chore: load Vitest config as ESM Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- vitest.config.mts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 vitest.config.mts diff --git a/vitest.config.mts b/vitest.config.mts new file mode 100644 index 0000000..e361d00 --- /dev/null +++ b/vitest.config.mts @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + test: { + environment: 'happy-dom', + include: ['tests/**/*.spec.ts'], + }, +}) From 87aaed93cc010dc1d2a50245c6ed2e1438568198 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:05:19 -0300 Subject: [PATCH 9/9] chore: remove CommonJS-loaded Vitest config Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- vitest.config.ts | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 vitest.config.ts diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index e361d00..0000000 --- a/vitest.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors -// SPDX-License-Identifier: AGPL-3.0-or-later - -import { defineConfig } from 'vitest/config' -import vue from '@vitejs/plugin-vue' - -export default defineConfig({ - plugins: [vue()], - test: { - environment: 'happy-dom', - include: ['tests/**/*.spec.ts'], - }, -})