Cursor Security Review bot on a Sume webhook handler: what to find
Cursor added a Security Review bot on Sep 23. A webhook handler for Sume should pass seven checks: raw body, timestamp window, rotation, empty secret and more.

A review bot checking a Sume webhook handler should confirm seven things: raw-body signing, a timestamp window, rotation-safe comparison, a refused empty secret, job_id idempotency, a fast 2xx after durable storage, and no trust in unverified events. The Cursor changelog lists Rollouts and Security Review bots on Sep 23, available on Teams and Enterprise plans.
Use this list as the brief when an agent writes the handler and a reviewer, human or bot, checks it.
The seven checks
| Check | What the docs say |
|---|---|
| Sign over the raw body | HMAC SHA-256 over <timestamp>.<raw_body>; a re-serialized body will not match. |
| Replay window | Reject timestamps outside your tolerance; five minutes is a reasonable default. |
| Rotation | During rotation the header carries one sume-v1= entry per live secret; accept when any matches. |
| Secret present | The secret is yours, read from the dashboard or GET /v1/webhooks/signing-secret. |
| Idempotency | Use job_id as the key; run webhooks dedupe on request_id. |
| Respond fast | Return 2xx after durably storing; attempts time out after 10 seconds. |
| Keep a fallback | Ten failed attempts leave a failed delivery, so keep status polling. |
A verifier that passes
This Python function refuses an empty secret, checks the timestamp window, and compares every sume-v1= entry in constant time.
import hashlib
import hmac
import time
def verify_sume_webhook(raw_body: bytes, timestamp: str, signature_header: str,
secret: str, tolerance_seconds: int = 300) -> bool:
if not secret:
raise ValueError("webhook signing secret is empty")
try:
ts = int(timestamp)
except ValueError:
return False
if abs(int(time.time()) - ts) > tolerance_seconds:
return False
digest = hmac.new(
secret.encode(), str(ts).encode() + b"." + raw_body, hashlib.sha256
).hexdigest()
expected = ("sume-v1=" + digest).encode()
matched = False
for entry in signature_header.split(","):
if hmac.compare_digest(entry.strip().encode(), expected):
matched = True
return matched
The easy mistakes
Review bots tend to flag the generic problems. These are the ones specific to this signature scheme.
- Parsing the JSON and signing
JSON.stringifyof it. Sign the bytes you received. - Stopping at the first non-matching entry during rotation, which rejects a valid delivery.
- Using a plain
==on the signature rather than a constant-time comparison. - Reading the secret with a default of an empty string, so an unset variable silently disables verification.
- Treating a redelivery as new work. Redeliver sends a fresh timestamp and signature for the same job.
Idempotency and retries
Sume retries network errors and non-2xx responses up to 10 attempts in total, with a fixed delay (30 seconds by default) rather than exponential backoff. The per-attempt timeout is 10 seconds. A handler that does slow work before replying burns its own budget.
Store the event first, answer 2xx, and process afterwards. Use job_id as the key so a retry or a manual redeliver does not repeat the side effect.
Test without a real job
The dashboard Send test action posts a dummy signed webhook.test payload to a URL you type, and it never replays a real job. Use it to prove the signature path. Redeliver, by contrast, re-sends a real job's terminal event, so use it to test the idempotency path.
Sources
Related posts
More in Developers
- Cursor self-hosted machines can't take Sume webhooks on a private URL
Sume rejects localhost, private-network and non-HTTPS webhook URLs. An agent on a self-hosted machine should poll status_url or use a public HTTPS receiver.
- Demand Gen copy limits: 40-character headlines, one at 30 or fewer
Demand Gen allows 40-character headlines (one must be 30 or fewer), 90-character descriptions and 10-60 second videos. A checker script plus the Sume lengths.
- Design a render tool for a stateless MCP server: job ids as arguments
MCP 2026-07-28 removes protocol sessions. A render tool stays correct if its state lives in a job id the client passes back, as Sume jobs do.
- Claude rejects forced tool_choice: steer generate_video by description
Claude Sonnet 5.5 and Opus 5.5 return a 400 for tool_choice any or tool, and thinking cannot be disabled. Steer Sume tool calls with descriptions and a dry run.
Written by Sume