Lightbox: fix proxy 404 and PDF iframe block

Two bugs surfaced on first dev-server test:

1. /api/staff/file 404s for valid file ids. The old per-org check
   read civicrm_entity_file and required entity_id==orgId, but our
   upload route anchors files to the submitter's contact id, not the
   org's — the entity_file row is metadata-only on this install
   (see comment in app/api/upload/route.ts). The custom-field column
   is the real ownership signal, which /api/staff/report already uses,
   and the staff key already gates org access. Drop the bogus check;
   keep the entity_table whitelist as defence.

2. Same-origin PDF iframe blocked by frame-ancestors 'none'. The
   strict global CSP excludes /staff/report; add /api/staff/file to
   the same embed-friendly profile so the lightbox iframe can load.

Also move the sandbox/default-src 'none' CSP to the attachment path
only — a strict sandbox header breaks Chrome's PDF viewer on inline
responses (it needs to load fonts and plugin-mode rendering). On
inline we rely on the SAFE_INLINE_MIMES allowlist + X-Content-Type-
Options + the app's global CSP.
This commit is contained in:
Joel Brock
2026-06-15 16:00:30 -07:00
parent 49d0d24950
commit e74462da0b
2 changed files with 44 additions and 43 deletions
+13 -8
View File
@@ -6,14 +6,15 @@ import type { NextConfig } from "next";
*
* Two profiles:
* - strict (default): frame-ancestors 'none' + X-Frame-Options: DENY.
* Applied to every route except /staff/report.
* Applied to every route except the embed-friendly ones.
* - staff-embed: frame-ancestors 'self' <civi-origin>, no X-Frame-Options.
* Lets the CiviCRM "Engagement Report" extension embed the staff page
* in an iframe on contact pages.
* Lets the CiviCRM "Engagement Report" extension embed /staff/report,
* and lets the lightbox iframe inside that page load the
* /api/staff/file proxy for PDF preview.
*
* The catch-all source uses a negative lookahead so it does NOT match
* /staff/report — otherwise both rules apply and the browser ANDs the
* frame-ancestors directives together, blocking embedding entirely.
* The catch-all source uses a negative lookahead so it does NOT match the
* embed-friendly routes — otherwise both rules apply and the browser ANDs
* the frame-ancestors directives together, blocking embedding entirely.
*/
const isDev = process.env.NODE_ENV !== "production";
const devOnlyDynamicScript = isDev ? " 'unsafe-eval'" : "";
@@ -84,8 +85,12 @@ const nextConfig: NextConfig = {
async headers() {
return [
{ source: "/staff/report", headers: staffEmbedHeaders },
// Catch-all that explicitly excludes /staff/report — see header notes.
{ source: "/((?!staff/report).*)", headers: strictHeaders },
// The lightbox in /staff/report iframes this proxy for inline PDF
// previews. Must share the embed-friendly profile so the browser
// doesn't block the same-origin iframe.
{ source: "/api/staff/file", headers: staffEmbedHeaders },
// Catch-all that excludes the embed-friendly routes — see header notes.
{ source: "/((?!staff/report|api/staff/file).*)", headers: strictHeaders },
];
},
};