Replace raw staff key in URLs with signed, org-bound tokens
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.
This commit is contained in:
@@ -53,7 +53,7 @@ without prior context can self-serve.
|
||||
- Visible only on **Organization** contacts (Individuals and Households
|
||||
see no tab). The check happens server-side in the tabset hook.
|
||||
- Tab body is an iframe pointing at
|
||||
`${WEBFORM_MW_APP_URL}/staff/report?org=<cid>&key=<secret>&frame=1`.
|
||||
`${WEBFORM_MW_APP_URL}/staff/report?org=<cid>&exp=<unix>&sig=<hmac>&frame=1`.
|
||||
- `frame=1` tells the embedded app to suppress its site header/footer
|
||||
and emit a `postMessage({type:"webform-mw-height", height})` payload
|
||||
on render and on resize. The tab's small inline script listens for
|
||||
@@ -63,13 +63,15 @@ without prior context can self-serve.
|
||||
|
||||
## Security notes
|
||||
|
||||
- The staff secret travels with each tab render inside the iframe `src`.
|
||||
Anyone permitted to view the Engagement Report tab (i.e., anyone with
|
||||
CiviCRM access) can read the URL in their browser's DevTools and reuse
|
||||
the secret to view any org's report. Acceptable model if "CiviCRM
|
||||
access" and "should view any staff report" overlap; otherwise consider
|
||||
upgrading the embedded app to per-user signed tokens and minting them
|
||||
in the page controller.
|
||||
- The shared secret never leaves the server. Each tab render mints a
|
||||
signed token, `HMAC-SHA256(secret, "webform-mw.staff-report.v1:<org>:<exp>")`,
|
||||
valid for one org for 8 hours. The app rejects tokens for other orgs,
|
||||
expired tokens, and anything valid for longer than 12 hours.
|
||||
- Tokens are only minted after `CRM_Contact_BAO_Contact_Permission::allow()`
|
||||
confirms the current user can view the org, so Civi contact ACLs carry
|
||||
through to the embedded report.
|
||||
- Rotating the secret: change `WEBFORM_MW_STAFF_KEY` and the app's
|
||||
`STAFF_REPORT_KEY` together; outstanding tokens stop working at once.
|
||||
- The extension uses `access CiviCRM` as its access argument — anyone
|
||||
with that permission sees the tab on Organization contacts. Tighten by
|
||||
changing the `<access_arguments>` value in `xml/Menu/webform_mw.xml`
|
||||
|
||||
Reference in New Issue
Block a user