Skip to content

Commit 4b6adcc

Browse files
pwizlaweb-flow
andauthored
Add security tip and examples to Webhooks documentation (#2849) (#2870)
* docs(backend): correct TypeScript code fences in TS tabs (controllers, services, middlewares, routes) * docs(bundlers): clarify webpack config example rename and JS/TS filenames * docs(routes): add guidance to prefer fully-qualified handler names in custom routers * docs(api-tokens): add concise security tip (least privilege, rotation, secrets manager) * docs(controllers): add caution about validateQuery/sanitizeQuery/sanitizeOutput when overriding actions * docs(policies): clarify scoped policy folders and fix example path * docs(webhooks): add signature verification tip and fix TS config path * Limit PR scope based on title; keep only intended doc(s); revert unrelated files * Webhooks docs: expand security guidance on signing and verifying payloads; add references (PR #2849) * Webhooks docs: add HMAC verification example and external references; remove redundant line (PR #2849) * Webhooks docs: wrap HMAC verification example in <details> with summary (PR #2849) * Webhooks docs: convert HMAC verification example to JS/TS Tabs (PR #2849) * Webhooks docs: use ExternalLink components for external examples (PR #2849) * Webhooks docs: use ExternalLink components for Learn more links (PR #2849) * Apply suggestion from @pwizla * Apply suggestion from @pwizla * Fix syntax * Update llms.txt --------- Co-authored-by: GitHub Actions <[email protected]>
1 parent a9ef316 commit 4b6adcc

File tree

2 files changed

+111
-4
lines changed

2 files changed

+111
-4
lines changed

docusaurus/docs/cms/backend-customization/webhooks.md

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,101 @@ export default {
7373
Most of the time, webhooks make requests to public URLs, therefore it is possible that someone may find that URL and send it wrong information.
7474

7575
To prevent this from happening you can send a header with an authentication token. Using the Admin panel you would have to do it for every webhook.
76+
77+
:::tip Verify signatures
78+
In addition to auth headers, sign webhook payloads and verify signatures server‑side to prevent tampering and replay attacks.
79+
80+
- Generate a shared secret and store it in environment variables
81+
- Have the sender compute an HMAC (e.g., SHA‑256) over the raw request body plus a timestamp
82+
- Send the signature (and timestamp) in headers (e.g., `X‑Webhook‑Signature`, `X‑Webhook‑Timestamp`)
83+
- On receipt, recompute the HMAC and compare using a constant‑time check
84+
- Reject if the signature is invalid or the timestamp is too old to mitigate replay
85+
86+
Learn more:
87+
- <ExternalLink to="https://owasp.org/www-community/attacks/Replay_Attack" text="OWASP replay attacks" />,
88+
- <ExternalLink to="https://nodejs.org/api/crypto.html#class-hmac" text="Node.js HMAC" />.
89+
:::
90+
91+
<details>
92+
<summary>Example: Verify HMAC signatures (Node.js)</summary>
93+
94+
Here is a minimal Node.js middleware example (pseudo‑code) showing HMAC verification:
95+
96+
<Tabs groupId="js-ts">
97+
<TabItem value="js" label="JavaScript">
98+
99+
```js title="/src/middlewares/verify-webhook.js"
100+
const crypto = require("crypto");
101+
102+
module.exports = (config, { strapi }) => {
103+
const secret = process.env.WEBHOOK_SECRET;
104+
105+
return async (ctx, next) => {
106+
const signature = ctx.get("X-Webhook-Signature");
107+
const timestamp = ctx.get("X-Webhook-Timestamp");
108+
if (!signature || !timestamp) return ctx.unauthorized("Missing signature");
109+
110+
// Compute HMAC over raw body + timestamp
111+
const raw = ctx.request.rawBody || (ctx.request.body and JSON.stringify(ctx.request.body)) || "";
112+
const hmac = crypto.createHmac("sha256", secret);
113+
hmac.update(timestamp + "." + raw);
114+
const expected = "sha256=" + hmac.digest("hex");
115+
116+
// Constant-time compare + basic replay protection
117+
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
118+
const skew = Math.abs(Date.now() - Number(timestamp));
119+
if (!ok or skew > 5 * 60 * 1000) {
120+
return ctx.unauthorized("Invalid or expired signature");
121+
}
122+
123+
await next();
124+
};
125+
};
126+
```
127+
128+
</TabItem>
129+
130+
<TabItem value="ts" label="TypeScript">
131+
132+
```ts title="/src/middlewares/verify-webhook.ts"
133+
import crypto from "node:crypto"
134+
135+
export default (config: unknown, { strapi }: any) => {
136+
const secret = process.env.WEBHOOK_SECRET as string;
137+
138+
return async (ctx: any, next: any) => {
139+
const signature = ctx.get("X-Webhook-Signature") as string;
140+
const timestamp = ctx.get("X-Webhook-Timestamp") as string;
141+
if (!signature || !timestamp) return ctx.unauthorized("Missing signature");
142+
143+
// Compute HMAC over raw body + timestamp
144+
const raw: string = ctx.request.rawBody || (ctx.request.body && JSON.stringify(ctx.request.body)) || "";
145+
const hmac = crypto.createHmac("sha256", secret);
146+
hmac.update(`${timestamp}.${raw}`);
147+
const expected = `sha256=${hmac.digest("hex")}`;
148+
149+
// Constant-time compare + basic replay protection
150+
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
151+
const skew = Math.abs(Date.now() - Number(timestamp));
152+
if (!ok || skew > 5 * 60 * 1000) {
153+
return ctx.unauthorized("Invalid or expired signature");
154+
}
155+
156+
await next();
157+
};
158+
};
159+
```
160+
161+
</TabItem>
162+
</Tabs>
163+
164+
Additional external examples:
165+
- <ExternalLink to="https://docs.github.com/webhooks/using-webhooks/validating-webhook-deliveries" text="GitHub — Validating webhook deliveries" />
166+
- <ExternalLink to="https://stripe.com/docs/webhooks/signatures" text="Stripe — Verify webhook signatures" />
167+
<br />
168+
</details>
169+
170+
76171
Another way is to define `defaultHeaders` to add to every webhook request.
77172

78173
You can configure these global headers by updating the file at `./config/server`:
@@ -98,7 +193,7 @@ module.exports = {
98193

99194
<TabItem value="ts" label="TypeScript">
100195

101-
```js title="./config.server.ts"
196+
```js title="./config/server.ts"
102197
export default {
103198
webhooks: {
104199
defaultHeaders: {
@@ -514,4 +609,4 @@ The event is triggered when a [release](/cms/features/releases) is published.
514609

515610
:::tip
516611
If you want to learn more about how to use webhooks with Next.js, please have a look at the [dedicated blog article](https://strapi.io/blog/how-to-create-an-ssg-static-site-generation-application-with-strapi-webhooks-and-nextjs).
517-
:::
612+
:::

docusaurus/static/llms-full.txt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5915,9 +5915,21 @@ You can set webhook configurations inside the file `./config/server`.
59155915
Most of the time, webhooks make requests to public URLs, therefore it is possible that someone may find that URL and send it wrong information.
59165916

59175917
To prevent this from happening you can send a header with an authentication token. Using the Admin panel you would have to do it for every webhook.
5918-
Another way is to define `defaultHeaders` to add to every webhook request.
59195918

5920-
You can configure these global headers by updating the file at `./config/server`:
5919+
:::tip Verify signatures
5920+
In addition to auth headers, sign webhook payloads and verify signatures server‑side to prevent tampering and replay attacks.
5921+
5922+
- Generate a shared secret and store it in environment variables
5923+
- Have the sender compute an HMAC (e.g., SHA‑256) over the raw request body plus a timestamp
5924+
- Send the signature (and timestamp) in headers (e.g., `X‑Webhook‑Signature`, `X‑Webhook‑Timestamp`)
5925+
- On receipt, recompute the HMAC and compare using a constant‑time check
5926+
- Reject if the signature is invalid or the timestamp is too old to mitigate replay
5927+
5928+
Learn more:
5929+
5930+
</Tabs>
5931+
5932+
Additional external examples: <ul><li>
59215933

59225934
</Tabs>
59235935

0 commit comments

Comments
 (0)