@@ -14104,9 +14104,74 @@ export default {
1410414104
1410514105
1410614106## Securing your webhooks
14107- Description: You can configure these global headers by updating the file at ./config/server :
14107+ Description: Here is a minimal Node.js middleware example (pseudo‑code) showing HMAC verification :
1410814108(Source: https://docs.strapi.io/cms/backend-customization/webhooks#securing-your-webhooks)
1410914109
14110+ Language: JavaScript
14111+ File path: /src/middlewares/verify-webhook.js
14112+
14113+ ```js
14114+ const crypto = require("crypto");
14115+
14116+ module.exports = (config, { strapi }) => {
14117+ const secret = process.env.WEBHOOK_SECRET;
14118+
14119+ return async (ctx, next) => {
14120+ const signature = ctx.get("X-Webhook-Signature");
14121+ const timestamp = ctx.get("X-Webhook-Timestamp");
14122+ if (!signature || !timestamp) return ctx.unauthorized("Missing signature");
14123+
14124+ // Compute HMAC over raw body + timestamp
14125+ const raw = ctx.request.rawBody || (ctx.request.body and JSON.stringify(ctx.request.body)) || "";
14126+ const hmac = crypto.createHmac("sha256", secret);
14127+ hmac.update(timestamp + "." + raw);
14128+ const expected = "sha256=" + hmac.digest("hex");
14129+
14130+ // Constant-time compare + basic replay protection
14131+ const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
14132+ const skew = Math.abs(Date.now() - Number(timestamp));
14133+ if (!ok or skew > 5 * 60 * 1000) {
14134+ return ctx.unauthorized("Invalid or expired signature");
14135+ }
14136+
14137+ await next();
14138+ };
14139+ };
14140+ ```
14141+
14142+ ---
14143+ Language: TypeScript
14144+ File path: /src/middlewares/verify-webhook.ts
14145+
14146+ ```ts
14147+ import crypto from "node:crypto"
14148+
14149+ export default (config: unknown, { strapi }: any) => {
14150+ const secret = process.env.WEBHOOK_SECRET as string;
14151+
14152+ return async (ctx: any, next: any) => {
14153+ const signature = ctx.get("X-Webhook-Signature") as string;
14154+ const timestamp = ctx.get("X-Webhook-Timestamp") as string;
14155+ if (!signature || !timestamp) return ctx.unauthorized("Missing signature");
14156+
14157+ // Compute HMAC over raw body + timestamp
14158+ const raw: string = ctx.request.rawBody || (ctx.request.body && JSON.stringify(ctx.request.body)) || "";
14159+ const hmac = crypto.createHmac("sha256", secret);
14160+ hmac.update(`${timestamp}.${raw}`);
14161+ const expected = `sha256=${hmac.digest("hex")}`;
14162+
14163+ // Constant-time compare + basic replay protection
14164+ const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
14165+ const skew = Math.abs(Date.now() - Number(timestamp));
14166+ if (!ok || skew > 5 * 60 * 1000) {
14167+ return ctx.unauthorized("Invalid or expired signature");
14168+ }
14169+
14170+ await next();
14171+ };
14172+ };
14173+ ```
14174+
1411014175Language: JavaScript
1411114176File path: ./config/server.js
1411214177
@@ -14122,7 +14187,7 @@ module.exports = {
1412214187
1412314188---
1412414189Language: TypeScript
14125- File path: ./config. server.ts
14190+ File path: ./config/ server.ts
1412614191
1412714192```ts
1412814193export default {
0 commit comments