fix(webhooks): close DNS rebinding, time out deliveries, sign with timestamp
- Deliveries connect through a lookup hook that checks the address actually dialled, so a host that resolves to a private address after registration (DNS rebinding) or a tampered row cannot reach internal services. Redirects are still not followed. - More blocked ranges (CGNAT/100.64/10 incl. cloud metadata, multicast, reserved, benchmarking, NAT64, 6to4, site-local, IPv4-mapped IPv6); only ports 80/443, no credentials in the URL, https in production. - 5 s timeout, response body discarded; dispatch no longer waits for receivers, so a hanging endpoint cannot stall the user's request. - Signature covers "timestamp.deliveryId.body" with X-ApiCore-Timestamp and X-ApiCore-Delivery headers, so receivers can reject replays. - Signing secrets are stored AES-256-GCM-encrypted; older plain rows still work. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
ebeb499a18
commit
79d20ee64e
8 files changed
+281
-72
No files matched your search
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
decryptMfaSecret,
|
||||
encryptMfaSecret,
|
||||
} from '../auth/mfa/mfa-crypto.util';
|
||||
|
||||
// Webhook signing secrets must stay reversible (the server signs with them),
|
||||
// so they are stored AES-256-GCM-encrypted like TOTP secrets. Rows written
|
||||
// before this carry the plain secret and no prefix - still readable.
|
||||
const PREFIX = 'enc:v1:';
|
||||
|
||||
export function sealWebhookSecret(secret: string): string {
|
||||
return `${PREFIX}${encryptMfaSecret(secret)}`;
|
||||
}
|
||||
|
||||
export function openWebhookSecret(stored: string): string {
|
||||
return stored.startsWith(PREFIX)
|
||||
? decryptMfaSecret(stored.slice(PREFIX.length))
|
||||
: stored;
|
||||
}
|
||||
Reference in new issue
Block a user