Skip to content

fix(asana): reject webhook secret overwrite on configured accounts - #615

Merged
yuvrxj-afk merged 3 commits into
corsairdev:mainfrom
geekyvaishnavi:fix/asana-webhook-secret-overwrite
Aug 6, 2026
Merged

fix(asana): reject webhook secret overwrite on configured accounts#615
yuvrxj-afk merged 3 commits into
corsairdev:mainfrom
geekyvaishnavi:fix/asana-webhook-secret-overwrite

Conversation

@geekyvaishnavi

@geekyvaishnavi geekyvaishnavi commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes #597.

The Asana webhook challenge handler persisted any X-Hook-Secret header it received, with no check for an already-configured secret. The webhook URL is public by necessity, so anyone able to reach it could POST their own secret, have it stored as the signing key via ctx.keys.set_webhook_signature, and then forge signed events that taskEvent accepts. It also silently broke the integration, since genuine Asana events continued to be signed with the original secret and started failing verification.

webhooks/challenge.ts — the handler now reads the stored secret before writing:

  • nothing stored → persist and echo it back (first-time registration unchanged)
  • same secret arrives again → echo it back without rewriting storage (Asana retries the handshake if it does not get a timely 200)
  • different secret arrives → 401, nothing written, and the sender's value is not echoed back

The comparison uses crypto.timingSafeEqual rather than ===, matching the existing precedent in core/auth/state.ts. Rejections are logged, since a blocked handshake is otherwise invisible to operators.

Two smaller defects on the same path are fixed here as well: set_webhook_signature was a floating promise, so a storage failure was swallowed and the handler still returned success: true while echoing a secret that was never saved; and the missing-header branch carried no statusCode, defaulting to 500 for what is a client error.

webhooks/challenge.test.ts — new, 8 tests covering match behavior, first-time registration, the overwrite attempt the issue asks for, an overwrite attempt at equal secret length (so the comparison is not merely a length check), handshake retry, missing header, and persistence failure. They fail against the previous handler and pass against this one.

Known limitation (deliberate). Once a secret is configured, this handler never replaces it — including for a legitimate re-registration. A safe rotation path needs per-webhook secret storage: Corsair keeps one account-level webhook_signature, while Asana issues one secret per webhook, so an account with multiple webhooks cannot be represented correctly today. That is a core storage change, out of scope for a plugin PR, and I've opened #NNN to track it. Blocking rotation is the safer failure mode in the meantime — the alternative is the unauthenticated overwrite this PR exists to close.

An earlier revision of this PR cleared the stored secret in webhookManagement.delete to keep re-registration working. Greptile correctly flagged that it would wipe a secret shared by an account's other webhooks, so that change has been dropped and endpoints/webhooks-management.ts is now identical to main.

I deliberately did not tighten match to handshake-shaped requests only. It resembles a fix but is not one — an attacker can trivially send an empty body. The storage check is the actual defense.

Checklist

Before submitting your PR, please verify the following:

  • I have run pnpm lint and all checks pass
  • I have run pnpm typecheck and there are no TypeScript errors
  • I have run pnpm build and all packages build successfully
  • I have run pnpm test and all tests pass
  • I have added or updated tests where applicable
  • I have added or updated necessary documentation

Screenshots / Demos (if applicable)

challenge.test.ts — 8/8 passing

Additional Notes

On the test checkbox. pnpm test at the repo root cannot complete on a local machine: 51 plugins ship credential-gated live-API tests (api.test.ts), and turbo halts at the first one it reaches (@corsair-dev/airtable, which needs AIRTABLE_API_KEY). Scoped results for the package this PR touches:

  • pnpm --filter @corsair-dev/asana test -- challenge.test.ts → 8/8 pass
  • packages/asana/api.test.ts → 39 failures, pre-existing on main, needs ASANA_ACCESS_TOKEN
  • pnpm --filter @corsair-dev/asana typecheck → clean
  • pnpm lint → clean
  • pnpm run validate:plugins → all plugins pass

@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

@geekyvaishnavi is attempting to deploy a commit to the corsair Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the plugin Changes inside a plugin package label Aug 5, 2026
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The Asana webhook challenge now uses timing-safe secret comparison, protects stored secrets from overwrites, persists new secrets before success, and returns explicit error statuses. Tests cover registration, retries, conflicts, missing headers, persistence failures, and response behavior.

Changes

Asana webhook security

Layer / File(s) Summary
Challenge validation and persistence
packages/asana/webhooks/challenge.ts
The handler validates headers, compares stored secrets safely, rejects mismatches with 401, persists first-time secrets, and returns 400 or 500 for failure cases.
Challenge validation coverage
packages/asana/webhooks/challenge.test.ts
Tests cover first registration, matching retries, overwrite attempts, missing headers, persistence failures, response headers, warnings, and key operations.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Asana
  participant ChallengeHandler
  participant SecretStorage

  Asana->>ChallengeHandler: Send X-Hook-Secret
  ChallengeHandler->>SecretStorage: Load stored secret
  SecretStorage-->>ChallengeHandler: Return stored secret
  ChallengeHandler->>ChallengeHandler: Compare secrets
  ChallengeHandler->>SecretStorage: Persist first-time secret
  ChallengeHandler-->>Asana: Return status and secret
Loading

Possibly related PRs

Suggested reviewers: yuvrxj-afk

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes prevent secret overwrites, preserve first-time registration, support matching retries, and add overwrite coverage required by issue #597.
Out of Scope Changes check ✅ Passed The changes are limited to Asana webhook challenge handling and its tests, which directly support issue #597.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: preventing webhook secret overwrites for configured Asana accounts.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR protects configured Asana accounts from unauthenticated webhook-secret replacement and handles persistence failures before acknowledging a challenge.

  • Reads the stored account secret and rejects mismatched challenge values.
  • Accepts same-secret handshake retries without rewriting storage.
  • Adds explicit client and persistence error responses.
  • Adds focused tests for registration, overwrite rejection, retries, missing headers, and storage failures.

Confidence Score: 4/5

The PR should not merge until the outstanding concurrent first-registration race is resolved or explicitly accepted by maintainers.

The overwrite guard closes the ordinary replacement path, but its separate read and write still allow overlapping public challenge requests to observe an empty value and race to install different account-level signing keys.

Files Needing Attention: packages/asana/webhooks/challenge.ts

Important Files Changed

Filename Overview
packages/asana/webhooks/challenge.ts Adds stored-secret comparison, overwrite rejection, awaited persistence, and explicit failure responses.
packages/asana/webhooks/challenge.test.ts Adds focused unit coverage for the challenge handler’s normal, rejection, retry, and persistence-failure paths.

Reviews (2): Last reviewed commit: "fix(asana): drop delete-path secret clea..." | Re-trigger Greptile

Comment thread packages/asana/endpoints/webhooks-management.ts Outdated
Comment thread packages/asana/webhooks/challenge.ts
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Plugin PR scorecard — packages/asana

Check Status Notes
R1 — Scope: plugin files only
R2 — Tests with assertions
R3 — Description complete
R3 — Linked issue / claim
R4 — Demo video / recording

Rules: PLUGIN_PR_RULES.md · re-runs on every push

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Hey @geekyvaishnavi, thanks for the contribution! 🏴‍☠️ Before a maintainer reviews, please fix the items below — the review re-runs automatically on your next push.

Must fix

  • P1 packages/asana/endpoints/webhooks-management.ts:38Account-wide secret cleared too early
    When an account has multiple active Asana webhooks, deleting one webhook unconditionally clears the account-level webhook_signature shared by the remaining webhooks, causing their subsequent event deliveries to fail while resolving the signing key.

Knowledge Base Used: The provider-plugin package pattern

  • P1 packages/asana/webhooks/challenge.ts:42Secret registration remains non-atomic
    If a malicious challenge and Asana's legitimate first handshake overlap, both requests can read an empty key before independently writing different secrets; the last write wins, so an attacker can replace the verification key, break genuine event verification, and forge events using the stored key.

How this was verified: The public handler performs separate asynchronous get and set operations, while the key manager provides no atomic compare-and-set across competing requests.

If anything remains after your next push, a bot commit will clean it up; a maintainer always does the final review and merge.

@github-actions github-actions Bot added the bot:round-1 Review bot posted consolidated findings label Aug 5, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/asana/endpoints/webhooks-management.ts`:
- Line 38: Update the post-delete cleanup flow around set_webhook_signature in
the webhook deletion handler so a storage failure after successful upstream
deletion is durably retryable or recoverable through a cleanup-only path.
Preserve the successful upstream deletion state, ensure a later challenge with a
new secret can recover without a stale secret causing 401, and add coverage for
storage failure after deletion.

In `@packages/asana/webhooks/challenge.ts`:
- Around line 40-63: Make the first-time registration flow in the webhook
challenge handler atomic by replacing the separate
get_webhook_signature/set_webhook_signature sequence with the storage layer’s
set-if-absent or compare-and-set operation. Treat registration as successful
only when no value existed or the existing value equals hookSecret; reject
conflicting concurrent values with the existing unauthorized response. Add a
test that interleaves two concurrent requests and verifies only the matching
secret is accepted and persisted.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e1dc98d5-597c-4b6b-96dd-6036bf354933

📥 Commits

Reviewing files that changed from the base of the PR and between 74ecc92 and 1109dc8.

📒 Files selected for processing (3)
  • packages/asana/endpoints/webhooks-management.ts
  • packages/asana/webhooks/challenge.test.ts
  • packages/asana/webhooks/challenge.ts

Comment thread packages/asana/endpoints/webhooks-management.ts Outdated
Comment on lines +40 to +63
const storedSecret = await ctx.keys.get_webhook_signature();

if (storedSecret) {
// A different value belongs to no handshake Corsair
// started, so refuse it and do not echo the sender's value back.
if (!secretsMatch(storedSecret, hookSecret)) {
return {
success: false,
statusCode: 401,
error: 'Webhook signing secret is already configured',
};
}

return {
success: true,
responseHeaders: {
'X-Hook-Secret': storedSecret,
},
data: { hookSecret: storedSecret },
};
}

try {
await ctx.keys.set_webhook_signature(hookSecret);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 whether the key abstraction provides an atomic conditional-write API.
rg -n -C 5 '\b(get_webhook_signature|set_webhook_signature)\s*\(' packages

# Inspect key-store implementations for transactions, conditional writes, or locking.
rg -n -i -C 4 'webhook_signature|compare.?and.?set|set.?if.?absent|transaction|atomic|mutex' packages

Repository: corsairdev/corsair

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the concrete key-manager interface and implementation without scanning unrelated plugin code.
rg -n -S -g '*.ts' \
  'set_webhook_signature|get_webhook_signature|createAccountKeyManager|AccountKeyManager' \
  packages/corsair packages/cli packages/asana \
  | head -200

Repository: corsairdev/corsair

Length of output: 6729


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -S -g '*.ts' \
  'set_webhook_signature|get_webhook_signature|createAccountKeyManager|AccountKeyManager' \
  packages/corsair packages/cli packages/asana \
  | head -200

Repository: corsairdev/corsair

Length of output: 6729


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline packages/corsair/core/auth/key-manager.ts
sed -n '1,40p;200,235p;260,330p;430,570p' packages/corsair/core/auth/key-manager.ts

printf '\n--- existing concurrent-write test ---\n'
sed -n '1,180p' packages/corsair/tests/key-manager-concurrent-writes.test.ts

printf '\n--- challenge implementation and tests ---\n'
sed -n '1,140p' packages/asana/webhooks/challenge.ts
sed -n '1,160p' packages/asana/webhooks/challenge.test.ts

Repository: corsairdev/corsair

Length of output: 17507


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Determine whether each request can receive a separate account key manager and
# whether the write chain is scoped only to one manager instance.
sed -n '450,510p' packages/corsair/core/client/index.ts
sed -n '285,315p;500,565p' packages/corsair/core/auth/key-manager.ts
rg -n -S -g '*.ts' 'challenge\.handler|webhooks.*challenge|handler:\s*challenge|keys:\s*accountKeyManager' packages/corsair packages/asana

Repository: corsairdev/corsair

Length of output: 5539


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '450,510p' packages/corsair/core/client/index.ts
sed -n '285,315p;500,565p' packages/corsair/core/auth/key-manager.ts
rg -n -S -g '*.ts' \
  'challenge\.handler|webhooks.*challenge|handler:\s*challenge|keys:\s*accountKeyManager' \
  packages/corsair packages/asana

Repository: corsairdev/corsair

Length of output: 5539


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check account caching and update behavior before finalizing the concurrency finding.
sed -n '330,455p' packages/corsair/core/auth/key-manager.ts
sed -n '1,120p' packages/corsair/core/auth/key-manager.ts

Repository: corsairdev/corsair

Length of output: 6675


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '330,455p' packages/corsair/core/auth/key-manager.ts
sed -n '1,120p' packages/corsair/core/auth/key-manager.ts

Repository: corsairdev/corsair

Length of output: 6675


Other (CWE-362): Concurrent Execution using Shared Resource with Improper Synchronization ('Race Condition')

Reachability: External

Reachability path
● Entry
  packages/asana/webhooks/challenge.test.ts
│
▼
● Sink
  packages/asana/webhooks/challenge.ts

Make first-time secret registration atomic.

The per-manager write chain does not make the read-and-write operation conditional. Concurrent handlers can both observe no stored secret, then overwrite each other with different values. Use a storage-level set-if-absent or compare-and-set operation. Accept an existing value only when it matches hookSecret. Add an interleaved concurrent-request test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/asana/webhooks/challenge.ts` around lines 40 - 63, Make the
first-time registration flow in the webhook challenge handler atomic by
replacing the separate get_webhook_signature/set_webhook_signature sequence with
the storage layer’s set-if-absent or compare-and-set operation. Treat
registration as successful only when no value existed or the existing value
equals hookSecret; reject conflicting concurrent values with the existing
unauthorized response. Add a test that interleaves two concurrent requests and
verifies only the matching secret is accepted and persisted.

@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
www Skipped Skipped Aug 6, 2026 9:22am

Request Review

@Dhirenderchoudhary

Copy link
Copy Markdown
Collaborator

@geekyvaishnavi Please have a look into greptile reviews

@geekyvaishnavi

geekyvaishnavi commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@geekyvaishnavi Please have a look into greptile reviews

@Dhirenderchoudhary
Gone through both. Dropped the deleteWebhook clear (288dd04), replied inline on both findings. The atomicity one I've left open: it needs a compare-and-set the key manager doesn't have, so it can't be fixed in a plugin PR would like your call on whether that blocks this.

@yuvrxj-afk

Copy link
Copy Markdown
Collaborator

@greptileai

@github-actions github-actions Bot added the bot:round-2 Review bot pushed an automated fix label Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

Remaining findings are being fixed by a bot commit — it will be re-reviewed automatically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:round-1 Review bot posted consolidated findings bot:round-2 Review bot pushed an automated fix plugin Changes inside a plugin package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(asana): webhook challenge handler can overwrite an existing webhook signing secret

4 participants