The Engagement Report tab wrote STAFF_REPORT_KEY into the iframe src for
every user with "access CiviCRM", without checking contact permissions.
Anyone could copy it from the DOM and read any org's report and files,
bypassing Civi ACLs; the key never expired and landed in access logs.
- Extension (0.4.0): Tab.php checks Contact_Permission::allow(VIEW) and
mints sig = base64url(HMAC-SHA256(key, "webform-mw.staff-report.v1:
<org>:<exp>")) with an 8h exp. The secret no longer leaves the server.
- App: lib/staff-token.mjs verifies the token (constant-time, org must
match, rejects expired or >12h tokens). /staff/report, /api/staff/report
and /api/staff/file share staffAuthQuery(); client links carry exp/sig.
- Legacy ?key= is still accepted (constant-time compare, logs a warning)
so the app can deploy before the extension. Remove it once every Civi
install runs extension 0.4.0.
- PHP/Node signature compatibility is pinned by a PHP-generated vector.
Adds a /api/staff/file proxy that re-streams Civi attachments with
Content-Disposition: inline so a native <dialog> lightbox can preview
images and PDFs in place. Office docs keep their plain download link
and gain a "View in Google Docs" secondary link (uses the Civi-signed
URL so Google can fetch without our staff key).
Also threads mime through /api/staff/report (Attachment.get mime_type)
so the dispatcher picks the right affordance without relying solely on
filename inference.