From 2c84aedad643a5492c92dac0270822bc4dc40859 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 15:36:24 +0000 Subject: [PATCH] Bump pdf.js past CVE-2024-4367; tighten img-src; fix heading order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security: pdf.js was pinned to 4.0.379, which predates the fix (4.2.67) for CVE-2024-4367 — arbitrary JavaScript execution when rendering a crafted PDF via the Type1 FontMatrix path. The app renders user-supplied "reference book" PDFs (200 MB cap), so a booby-trapped file shared in a study group is the realistic vector. Runtime exploit is already blocked by the eval-free CSP (no 'unsafe-eval'), but bump to 4.2.67 so the vulnerable code isn't shipped at all. Both CDN URLs updated; SW cache bumped to aplus-study-v127. Hardening: img-src allowed any https: host though no content (verified: 0 external image URLs across data/) uses remote images — unused headroom that the raw-HTML "concept fix" reader could turn into a tracking-pixel IP leak. Tighten to 'self' data: blob:' in the meta CSP and _headers. Accessibility (WCAG 1.3.1): the quiz/mock results screen jumped h1→h3 (the "Missed" and "By objective" section titles), so heading navigation skipped a level. Promote both to h2 for a contiguous outline; CSS is class-based so styling is unchanged. Verified: 77/77 node tests pass, question data validates clean. The pdf.js/CSP/heading changes are further gated by the CI Puppeteer smoke (axe + PDF rendering) on the PR. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_011CR5oMcpxdjeJr7u6mnDdC --- _headers | 2 +- app.js | 8 ++++---- index.html | 2 +- sw.js | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/_headers b/_headers index 6991591..e5d1dcb 100644 --- a/_headers +++ b/_headers @@ -11,7 +11,7 @@ # font options load on demand: Atkinson Hyperlegible (CSS from # fonts.googleapis.com, files from fonts.gstatic.com) and OpenDyslexic # (CSS + files from fonts.cdnfonts.com). Only fetched when a user opts in. - Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; script-src-elem 'self' https://cdn.jsdelivr.net; worker-src 'self' blob:; img-src 'self' data: blob: https:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://fonts.cdnfonts.com; font-src 'self' data: https://fonts.gstatic.com https://fonts.cdnfonts.com; connect-src 'self' https://cdn.jsdelivr.net https://*.supabase.co https://api.web3forms.com; frame-ancestors 'none'; base-uri 'none'; object-src 'none'; form-action 'none'; upgrade-insecure-requests + Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; script-src-elem 'self' https://cdn.jsdelivr.net; worker-src 'self' blob:; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://fonts.cdnfonts.com; font-src 'self' data: https://fonts.gstatic.com https://fonts.cdnfonts.com; connect-src 'self' https://cdn.jsdelivr.net https://*.supabase.co https://api.web3forms.com; frame-ancestors 'none'; base-uri 'none'; object-src 'none'; form-action 'none'; upgrade-insecure-requests # Force HTTPS for a year, cover subdomains, and allow preloading — matches # the fleet baseline used by the sibling companion apps. Cloudflare Pages # already serves HTTPS-only; this makes the browser refuse any downgrade. diff --git a/app.js b/app.js index 08328d4..ee4b5a8 100644 --- a/app.js +++ b/app.js @@ -1760,7 +1760,7 @@ function renderQuizResults() { const wrongHTML = wrong.length === 0 ? '' : `
-

Missed (${wrong.length})

+

Missed (${wrong.length})

    ${wrong.map(bq => { const q = getQuestion(bq); @@ -1778,7 +1778,7 @@ function renderQuizResults() { // report style so the user knows which domain to focus next pass. const objHTML = !mock || objBreakdown.length === 0 ? '' : `
    -

    By objective

    +

    By objective

      ${objBreakdown.map(([obj, b]) => { const pct = Math.round((b.correct / b.total) * 100); @@ -3284,8 +3284,8 @@ async function loadPdfJs() { if (_pdfjsModule) return _pdfjsModule; // Fixed pinned version, ESM build. The first load lands in the SW // cache so subsequent loads are offline. - const url = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.379/build/pdf.min.mjs'; - const workerUrl = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.379/build/pdf.worker.min.mjs'; + const url = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@4.2.67/build/pdf.min.mjs'; + const workerUrl = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@4.2.67/build/pdf.worker.min.mjs'; const mod = await import(/* @vite-ignore */ url); mod.GlobalWorkerOptions.workerSrc = workerUrl; _pdfjsModule = mod; diff --git a/index.html b/index.html index 9e28300..6f70d66 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,7 @@ including frame-ancestors / upgrade-insecure-requests, which aren't valid in a meta tag). Mirrors _headers so the concept-fixes raw-HTML render path stays backstopped even off Cloudflare. --> - + diff --git a/sw.js b/sw.js index ac11ed3..e6f2808 100644 --- a/sw.js +++ b/sw.js @@ -1,5 +1,5 @@ // Service worker — cache shell + content for offline use -const CACHE = 'aplus-study-v126'; +const CACHE = 'aplus-study-v127'; const ASSETS = [ './', './index.html',