Security: signed staff tokens, fail-closed checksum, upload & submit hardening #2
@@ -245,9 +245,8 @@ async function probeStageOptionGroup(): Promise<CheckResult> {
|
|||||||
async function probeChecksumApi(): Promise<CheckResult> {
|
async function probeChecksumApi(): Promise<CheckResult> {
|
||||||
// Tries to call validateChecksum with a deliberately invalid checksum.
|
// Tries to call validateChecksum with a deliberately invalid checksum.
|
||||||
// If the API exists, we get back {valid: false} or similar — that's
|
// If the API exists, we get back {valid: false} or similar — that's
|
||||||
// success for THIS check (the API responded). If the API doesn't exist
|
// success for THIS check (the API responded). If the API doesn't exist,
|
||||||
// we fall back to a Contact.get probe in verifyChecksum, which will
|
// verifyChecksum fails closed and every form link will be rejected.
|
||||||
// also fail gracefully.
|
|
||||||
try {
|
try {
|
||||||
await civi("Contact", "validateChecksum", {
|
await civi("Contact", "validateChecksum", {
|
||||||
contactId: 1,
|
contactId: 1,
|
||||||
@@ -266,7 +265,7 @@ async function probeChecksumApi(): Promise<CheckResult> {
|
|||||||
return {
|
return {
|
||||||
check: "contact_validate_checksum_api",
|
check: "contact_validate_checksum_api",
|
||||||
ok: false,
|
ok: false,
|
||||||
detail: `validateChecksum action not present in this Civi version. The verifyChecksum() helper falls back to a Contact.get probe with cs param. Detail: ${msg}`,
|
detail: `validateChecksum action not present in this Civi version. verifyChecksum() fails closed, so every form link will be rejected until Civi is upgraded. Detail: ${msg}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
+11
-14
@@ -297,32 +297,29 @@ export async function civiMultipart<T = unknown>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate a contact checksum (cid + cs) against CiviCRM.
|
* Validate a contact checksum (cid + cs) against CiviCRM via APIv4
|
||||||
|
* Contact.validateChecksum.
|
||||||
*
|
*
|
||||||
* APIv4 exposes Contact.validateChecksum in newer Civi versions. For older
|
* Fails closed: any API error is treated as an invalid checksum. There is
|
||||||
* versions you may need to call Contact.get with the cs param and verify
|
* deliberately no fallback — our API key is privileged, so any probe that
|
||||||
* the contact resolves. We use validateChecksum here and fall back to a
|
* merely checks "does the contact resolve" would accept every checksum.
|
||||||
* Contact.get probe if it returns a "missing API" error.
|
* /api/health confirms validateChecksum is available on the install.
|
||||||
*/
|
*/
|
||||||
export async function verifyChecksum(cid: string, cs: string): Promise<boolean> {
|
export async function verifyChecksum(cid: string, cs: string): Promise<boolean> {
|
||||||
if (isStubMode()) {
|
if (isStubMode()) {
|
||||||
// STUB: any non-empty cs is "valid" so the UI can be exercised locally.
|
// STUB: any non-empty cs is "valid" so the UI can be exercised locally.
|
||||||
return Boolean(cid && cs);
|
return Boolean(cid && cs);
|
||||||
}
|
}
|
||||||
|
const contactId = Number(cid);
|
||||||
|
if (!Number.isSafeInteger(contactId) || contactId <= 0) return false;
|
||||||
try {
|
try {
|
||||||
const res = await civi<{ valid: boolean }>("Contact", "validateChecksum", {
|
const res = await civi<{ valid: boolean }>("Contact", "validateChecksum", {
|
||||||
contactId: Number(cid),
|
contactId,
|
||||||
checksum: cs,
|
checksum: cs,
|
||||||
});
|
});
|
||||||
return Boolean(res.values?.[0]?.valid);
|
return Boolean(res.values?.[0]?.valid);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Fallback: try Contact.get with the checksum as `cs` URL param. If the
|
console.error("[civi] validateChecksum failed; treating as invalid:", e instanceof Error ? e.message : String(e));
|
||||||
// contact resolves, the checksum is valid.
|
return false;
|
||||||
const res = await civi<{ id: number }>("Contact", "get", {
|
|
||||||
where: [["id", "=", Number(cid)]],
|
|
||||||
select: ["id"],
|
|
||||||
checksum: cs,
|
|
||||||
});
|
|
||||||
return Array.isArray(res.values) && res.values.length === 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user