Wiring your application to a third-party SaaS platform: verifying an inbound webhook against the vendor's own scheme rather than a generalized one, why a valid signature identifies the sender and not the user in the payload, replay windows, one credential per integration and per environment, least-privilege scopes, and treating a bulk export as a data boundary. Use when writing a webhook receiver, an OAuth integration, a SCIM endpoint, or any code that authenticates to or from a SaaS vendor.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add ShieldNet-360/secure-vibe --skill saas-security --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Saas Security?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/shieldnet-360-saas-security-secure-vibe)More formats (shields.io, HTML) on the badges page.
---
name: saas-security
description: "Wiring your application to a third-party SaaS platform: verifying an inbound webhook against the vendor's own scheme rather than a generalized one, why a valid signature identifies the sender and not the user in the payload, replay windows, one credential per integration and per environment, least-privilege scopes, and treating a bulk export as a data boundary. Use when writing a webhook receiver, an OAuth integration, a SCIM endpoint, or any code that authenticates to or from a SaaS vendor."
---
<!-- Native skill bundle for Claude Code. Generated by `secure-vibe dev regenerate`. -->
<!-- Do not edit by hand; the source of truth is skills/saas-security/SKILL.md. -->
# SaaS Integration Security
Wiring your application to a third-party SaaS platform: verifying an inbound webhook against the vendor's own scheme rather than a generalized one, why a valid signature identifies the sender and not the user in the payload, replay windows, one credential per integration and per environment, least-privilege scopes, and treating a bulk export as a data boundary. Use when writing a webhook receiver, an OAuth integration, a SCIM endpoint, or any code that authenticates to or from a SaaS vendor.
## ALWAYS
- Verify an inbound webhook against **that vendor's documented scheme**, and do not generalize from another one. Vendors differ in the header, in the secret, and — the part that breaks a generalized implementation — in the **canonical string** that gets signed. Some sign the raw body, some sign a version prefix plus a timestamp plus the body, some sign the HTTP method and path as well. Read the vendor's page for the vendor you are integrating; a signature routine copied from a different vendor produces a receiver that rejects every legitimate event, and the fix someone reaches for is to turn verification off.
- Verify against the **raw request body**, exactly as received, before any parsing or re-serialization. A framework that decodes JSON and re-encodes it changes whitespace and key order, and the HMAC no longer matches — which again ends with verification being disabled rather than fixed.
- Check the **timestamp** the vendor sends and reject requests outside a short window, then reject **replays** inside it by recording delivery or event ids. A signature is valid forever; the window narrows the replay opportunity and the id store closes it.
- Compare signatures in **constant time**. `crypto-misuse` owns the comparison API for each language.
- Treat the signature as proof of **who sent the payload, not who the payload is about**. This is the distinction that matters most in this domain: a verified Zoom or Calendly or HubSpot event genuinely came from that vendor, and the `email`, `from`, or custom fields inside it may still be attacker-chosen — anyone who can book a meeting or submit a form controls them. Resolve the subject to a canonical id server-side. `auth-security` owns the general rule for producer-asserted identity.
- Give each integration its **own credential**, and each environment its own. A refresh token, connected app, or service account shared between dev and prod puts production credentials inside dev's blast radius, and a credential shared between integrations gives each the union of what any of them needs. Name service accounts for their purpose so a later reviewer can tell what revoking one would break.
- Request the **narrowest scope you actually call**, and prefer a read-only variant where the platform offers one. Scopes are granted once, at consent, and nobody revisits them; a scope requested speculatively is a permanent grant.
- Treat a **bulk export** of employee, customer or billing records as a data boundary: log the authenticated principal, the query, the row count and the destination, and alert on volume that departs from that integration's own baseline. For HRIS and ERP systems this is the primary control — the integration is authorized, and the question is how much it took.
- Authenticate a **SCIM or directory-sync endpoint** you expose, rate-limit it, and audit every user and group write. Note what the major identity providers actually send: a static OAuth bearer token, configured once in their admin console. Client certificates are generally not on offer, so the practical controls are a high-entropy token you can rotate, a source-address restriction where the IdP publishes ranges, and treating every write as an audit event.
## NEVER
- Disable signature verification to make an integration work. Almost every webhook spoofing incident is an integration that shipped with the check off because it was failing in staging. If it is failing, the canonical string or the raw-body rule is what is wrong.
- Hard-code a SaaS token, OAuth client secret, webhook signing key, or service-account JSON in source, a container image, a mobile binary, or client-side JavaScript. These formats are mass-scanned on public registries within minutes of a push. `secret-detection` owns the patterns and the placeholders.
- Wire an **incoming webhook URL that posts into a channel more trusted than its callers**. If a CI bot can post to `#secops`, a CI compromise is direct phishing of the people who would investigate it. The webhook URL is a bearer credential with no identity attached — anyone holding it posts as that integration.
- Share one **person-bound token** across services. It carries that human's privileges, dies when they leave, and leaks through their laptop.
- Trust third-party code that runs **inside** the SaaS platform — a marketplace app, a scripting extension, an automation add-on — without review. It executes with the privileges of whoever installed it, inside the tenant, and neither your CI nor your dependency scanner can see it. `supply-chain-security` owns the review posture.
- Grant a **super-admin or org-wide scope** to anything other than a narrowly-owned automation. Most integrations need a read-only directory scope; the admin scope is requested because it makes the first call work.
## KNOWN FALSE POSITIVES
- **Publishable** identifiers are meant to be public: an OAuth client id for a mobile or SPA client, a published app's service-account email, a workspace or tenant id. The matching client secret and key file are not.
- Vendor **example credentials** in documentation match detection patterns deliberately. The surrounding documentation context is the signal, not the prefix.
- A webhook receiver that returns `200` to an event it decided not to act on is correct: vendors retry non-2xx, and an authorization decision is not a delivery failure. Log the refusal instead of signalling it in the status.
- A customer-supplied **destination** URL is the intended feature of an outbound webhook system. It still needs the private-address rules — `ssrf-prevention` owns where an outbound fetch may land.
## Reference files
Read these only when the task calls for them.
- `references/verifying-findings.md`
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!