Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions webhooks/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,23 @@ Every v1 webhook request includes an `x-topgg-signature` header in the format `t
import crypto from 'crypto';

function verifyWebhook(rawBody: string, signature: string, secret: string): boolean {
const [tPart, v1Part] = signature.split(',');
const parts = signature.split(',');
if (parts.length !== 2) return false;
const [tPart, v1Part] = parts;
const timestamp = tPart.split('=')[1];
const receivedSig = v1Part.split('=')[1];
if (!timestamp || !receivedSig) return false;
Comment on lines +48 to +59

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation is fragile and not forward-compatible:\n\n1. Strict length check (parts.length !== 2): If Top.gg adds new fields to the signature header in the future (e.g., for versioning, metadata, or additional signatures), this check will fail and break the webhook integration for users copying this code.\n2. Order dependency: It assumes t is always the first part and v1 is the second part. If the order is reversed or changed, parsing will fail.\n\nParsing the header by splitting and matching keys in a loop is much more robust, order-independent, and forward-compatible.

  const parts = signature.split(',');\n  let timestamp: string | undefined;\n  let receivedSig: string | undefined;\n\n  for (const part of parts) {\n    const [key, ...valueParts] = part.split('=');\n    const value = valueParts.join('=');\n    if (key === 't') timestamp = value;\n    if (key === 'v1') receivedSig = value;\n  }\n\n  if (!timestamp || !receivedSig) return false;

const ts = parseInt(timestamp, 10);
const now = Math.floor(Date.now() / 1000);
if (isNaN(ts) || Math.abs(now - ts) > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(receivedSig));
const expectedBuf = Buffer.from(expected);
const receivedBuf = Buffer.from(receivedSig);
if (expectedBuf.length !== receivedBuf.length) return false;
return crypto.timingSafeEqual(expectedBuf, receivedBuf);
}
```

Expand Down