-
Notifications
You must be signed in to change notification settings - Fork 214
fix(zohomail): validate handshake signature before persisting secret #616
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,22 +27,38 @@ export const handshake: ZohoMailWebhooks['handshake'] = { | |
| }; | ||
| } | ||
|
|
||
| let existingSecret: string | undefined; | ||
| try { | ||
| await ctx.keys.set_webhook_signature(hookSecret); | ||
| existingSecret = (await ctx.keys.get_webhook_signature()) ?? undefined; | ||
|
Comment on lines
+30
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the key-store contract and look for conditional-write support.
ast-grep outline packages/zohomail/webhooks/challenge.ts --items all
rg -n -C 12 '\b(get_webhook_signature|set_webhook_signature)\b' packages
rg -n -C 8 '\b(compareAndSet|compare_and_set|createIfAbsent|create_if_absent|transaction|serializ|mutex|lock|version)\b' packagesRepository: corsairdev/corsair Length of output: 50375 Authorization Bypass (CWE-367): Time-of-check Time-of-use (TOCTOU) Race Condition Reachability: External · Exploitability: Moderate Make first-time webhook-secret setup atomic. When two requests read an empty secret before either write, an unsigned request can overwrite the legitimate secret. Use a create-if-absent or compare-and-set write and add concurrent setup coverage. 🤖 Prompt for AI Agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If two first-time handshakes for the same tenant overlap, both can observe that no secret exists and proceed without ownership verification, allowing a later unsigned write to replace the legitimate secret and break or take control of subsequent webhook authentication. How this was verified: The handler performs independent read and write operations, while the key manager serializes only writes and the webhook path provides no per-tenant lock around the ownership decision. |
||
| } catch (error) { | ||
| console.warn( | ||
| '[corsair:zohomail] Failed to persist webhook secret:', | ||
| '[corsair:zohomail] Failed to retrieve existing webhook secret:', | ||
| error, | ||
| ); | ||
| return { | ||
| success: false, | ||
| statusCode: 500, | ||
| error: 'Failed to persist webhook secret', | ||
| error: 'Failed to retrieve existing webhook secret', | ||
| }; | ||
| } | ||
|
|
||
| if (existingSecret === hookSecret) { | ||
| return { | ||
| success: true, | ||
| data: { hookSecret }, | ||
| }; | ||
| } | ||
|
|
||
| const signature = getZohoWebhookSignature(headers); | ||
| if (signature) { | ||
|
|
||
| if (existingSecret) { | ||
| if (!signature) { | ||
| return { | ||
| success: false, | ||
| statusCode: 401, | ||
| error: 'Cannot overwrite existing secret without a valid signature', | ||
| }; | ||
| } | ||
| const rawBody = request.rawBody; | ||
| if (!rawBody) { | ||
| return { | ||
|
|
@@ -51,13 +67,45 @@ export const handshake: ZohoMailWebhooks['handshake'] = { | |
| error: 'Missing raw body for signature verification', | ||
| }; | ||
| } | ||
| if (!verifyZohoWebhookSignature(rawBody, hookSecret, signature)) { | ||
| if (!verifyZohoWebhookSignature(rawBody, existingSecret, signature)) { | ||
| return { | ||
| success: false, | ||
| statusCode: 401, | ||
| error: 'Invalid signature', | ||
| }; | ||
| } | ||
| } else { | ||
| if (signature) { | ||
| const rawBody = request.rawBody; | ||
| if (!rawBody) { | ||
| return { | ||
| success: false, | ||
| statusCode: 401, | ||
| error: 'Missing raw body for signature verification', | ||
| }; | ||
| } | ||
| if (!verifyZohoWebhookSignature(rawBody, hookSecret, signature)) { | ||
| return { | ||
| success: false, | ||
| statusCode: 401, | ||
| error: 'Invalid signature', | ||
| }; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| try { | ||
| await ctx.keys.set_webhook_signature(hookSecret); | ||
| } catch (error) { | ||
| console.warn( | ||
| '[corsair:zohomail] Failed to persist webhook secret:', | ||
| error, | ||
| ); | ||
| return { | ||
| success: false, | ||
| statusCode: 500, | ||
| error: 'Failed to persist webhook secret', | ||
| }; | ||
| } | ||
|
|
||
| return { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: corsairdev/corsair
Length of output: 156
🏁 Script executed:
Repository: corsairdev/corsair
Length of output: 38802
🏁 Script executed:
Repository: corsairdev/corsair
Length of output: 31373
Assert that a repeat handshake does not persist the secret.
Spy on
corsair.zohomail.keys.set_webhook_signatureafter the first handshake and before the retry. Assert that the retry makes no calls.🤖 Prompt for AI Agents