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.
49 lines
1.7 KiB
TypeScript
49 lines
1.7 KiB
TypeScript
/**
|
|
* Auth for the internal staff routes (/staff/report, /api/staff/report,
|
|
* /api/staff/file).
|
|
*
|
|
* Preferred: signed, org-bound, expiring tokens minted by the CiviCRM
|
|
* extension — /staff/report?org=<id>&exp=<unix>&sig=<hmac>. See
|
|
* lib/staff-token.mjs. The extension only mints a token after checking the
|
|
* Civi user can view that org, so Civi ACLs carry through.
|
|
*
|
|
* Legacy (transitional): ?key=<STAFF_REPORT_KEY>. Still accepted so the app
|
|
* can deploy before the updated extension reaches the Civi server. Remove
|
|
* the legacy branch once every Civi install emits signed tokens.
|
|
*
|
|
* If STAFF_REPORT_KEY is unset, every request is refused (closed by
|
|
* default). Stub mode (CIVI_* unset) does NOT bypass this check — for local
|
|
* dev, set STAFF_REPORT_KEY=dev in .env.local.
|
|
*/
|
|
|
|
import { safeEqual, verifyStaffToken } from "./staff-token.mjs";
|
|
|
|
interface StaffAuthParams {
|
|
org: string | null | undefined;
|
|
exp?: string | null;
|
|
sig?: string | null;
|
|
key?: string | null;
|
|
}
|
|
|
|
/**
|
|
* Validate staff credentials for `org`. Returns the query-string fragment
|
|
* (without `org`) that downstream fetches and file links must carry, or
|
|
* null when unauthorized.
|
|
*/
|
|
export function staffAuthQuery({ org, exp, sig, key }: StaffAuthParams): string | null {
|
|
const secret = process.env.STAFF_REPORT_KEY;
|
|
if (!secret) return null;
|
|
|
|
if (exp || sig) {
|
|
return verifyStaffToken(secret, org, exp, sig)
|
|
? `exp=${encodeURIComponent(exp!)}&sig=${encodeURIComponent(sig!)}`
|
|
: null;
|
|
}
|
|
|
|
if (key && safeEqual(key, secret)) {
|
|
console.warn("[staff-auth] legacy ?key= access used; update the CiviCRM extension to signed tokens");
|
|
return `key=${encodeURIComponent(key)}`;
|
|
}
|
|
return null;
|
|
}
|