Stop customer A's login from opening customer B's account
By CAPPZ.AI ·
In a multi-tenant SaaS, a correctly signed SAML login from one customer can be accepted as a login into another. CAPPZ SAML Guard now chooses each customer's identity provider and certificates by login URL, checks signatures itself, and comes with a setup wizard.
A business SaaS usually serves hundreds of customers, and each customer brings its own identity provider: one uses Okta, another Microsoft Entra, a third runs ADFS. Every one of them signs SAML logins with its own certificate. The SaaS's job is to check that a login was signed by this customer's identity provider, for this customer's account.
That last part is where multi-tenant SaaS fails most often. The SaaS loads every customer's certificate, checks that the login was signed by one of them, and stops there. The signature is genuine and the cryptography is fine. But a login correctly signed by customer A's identity provider (or by an identity provider an attacker set up as a trial customer) is accepted as a login into customer B. Nothing is forged in the usual sense, which is why this bug survives code review and penetration tests.
Last week we released reconciliation and SAML Guard to catch SaaS logins your identity provider never issued, and to refuse ambiguous SAML in front of applications you host. The open-source CAPPZ SDK, version 15.37, now extends SAML Guard to the SaaS providers themselves: one Guard in front of every customer.
The login URL decides the customer
Each customer gets its own login address (the SAML "ACS URL"), for example https://app.example.com/saml/acs/acme. The Guard reads which address the browser actually posted to, and that alone decides which customer's identity provider, application ID and certificates apply.
It never takes the customer from the login itself: not from the issuer named inside it, the email domain or the RelayState field. All of those are written by whoever produced the login, so trusting them lets the login choose who checks it.
The Guard then refuses a login posted to customer A's address if anything in it belongs to another customer:
- another customer's identity provider as the issuer;
- another customer's application as the audience;
- another customer's login address as the recipient or destination;
- a certificate pinned only for another customer.
These refusals get their own reason, cross-tenant, rather than a generic "wrong issuer". It is the attack, not a typo, so it is counted separately and raises an alert even while the Guard is only observing.
Each customer's certificates, signed for that customer
A certificate is only trusted once the SaaS operator's own key has signed a pin for it. A pin names the customer, the identity provider and an expiry date. Customer B's pin can never verify a login at customer A, even if both use the same identity provider product.
Login requests are tied to customers too. The Guard records which login requests the SaaS sent for which customer, so a response to customer A's request posted at customer B's address is refused.
The signature is checked by the Guard as well
In version 15.36, the Guard left the cryptographic signature check to the SaaS's own SAML library and only made sure the signature pointed at the right element. If that library is the part that's broken, that isn't enough.
The Guard now verifies the XML signature itself, using only the pinned certificates. It computes the digest, verifies RSA or ECDSA signatures with SHA-256 or stronger, and refuses changed bytes and signatures moved from another document. We tested it against an independent implementation (signxml) for logins signed with RSA and ECDSA, on the assertion or on the whole response.
The SaaS's own check stays in place: both must pass. Anything the Guard can't verify exactly is refused, never waved through. That includes rarer XML canonicalization methods and RSA-PSS, and for those you can plug in your own verifier.
Setup in a few commands
Configuration mistakes cause most SAML incidents, so setup is a wizard that writes safe settings and checks them. Run it with no arguments for a guided setup, or one step at a time from scripts:
W="deno run -A packages/cappz-edge-node/tools/saml-wizard.ts --dir ./saml-guard"
$W keygen # your operator key: signs pins and mode changes
$W init --upstream http://127.0.0.1:8080
$W add-tenant --id acme --metadata https://idp.acme.example/metadata \
--acs https://app.example.com/saml/acs/acme --sp-entity https://app.example.com/saml/acme
$W doctor # every configuration rule, with a fix for each problem
$W test --tenant acme --response captured.b64
-
add-tenant reads the customer's identity provider metadata, so nobody retypes certificates. Fetch the metadata over HTTPS, or confirm the fingerprint with the customer.
-
doctor catches:
- two customers sharing a login address;
- a customer with no pinned certificate, or a pin about to expire;
- logins the identity provider starts without a request ("unsolicited") left allowed;
- lifetimes and clock-skew allowances that are too generous;
- signature checking handed back to the SaaS.
Errors block enforcement.
-
rotate adds a customer's new certificate before their identity provider switches over. The doctor warns 30 days ahead.
Observe first, enforce one customer at a time
As with CAPPZ's tools for moving off PHP and off X.509, the Guard starts in shadow mode: the SaaS stays in charge, and the Guard reports what it would have refused, separately for each customer.
Two things raise an alert:
- a login the SaaS accepted but the Guard would have refused;
- any cross-tenant attempt.
saml-wizard status --alert prints only new alerts and exits with an error code, so it drops straight into a five-minute timer, and saml-wizard monitor prints ready-made systemd and cron entries.
When a customer's report shows enough traffic and every finding is explained, saml-wizard enforce --tenant acme switches that customer on. The switch is a record signed by the operator's key and names the report it was based on, so the decision can be audited later. Rolling back is one command, and other customers are unaffected.
For agents and the people they work with
The SDK includes two agent skills, so an AI assistant can work through this with you:
- securing-saml-sso works out which check failed and who can fix it, then compares root-cause fixes (a patch or configuration) with safeguards (reconciliation, Guard in shadow, Guard enforcing). For each one it says what it covers, what it doesn't, what it needs and the effort, then recommends an order.
- monitoring-saml-logins sets up scheduled reconciliation and Guard alerts, and walks through what to do when one fires: contain, scope, find the root cause, record the evidence.
Limits, stated plainly
- The built-in signature check covers exclusive XML canonicalization, which is the default for the major identity providers. Anything else is refused unless you plug in a verifier.
- For encrypted assertions, only the outer response is checked.
- The Guard learns login requests sent by redirect, not by form post.
- The test responses are generated. Production runs against real identity providers come next.
Try it
The SDK is open source under Apache-2.0 at github.com/CAPPZ-ai/cappz-sdk. Read docs/SAML_GUARD.md (it has a configuration table and an incident runbook), then run packages/cappz-edge-node/tools/saml-wizard.ts with Deno 2. No CAPPZ account is needed.
If you run a multi-tenant SaaS, try one test first. Capture a valid login from one customer's identity provider and post it to another customer's login address on a staging system. If you get in, you have the bug. Put the Guard in front in shadow mode and it will show you every time it happens in production.
Topics: saml, multi-tenant saas, single sign-on, cross-tenant, identity security, xml signature, edge node, open source
Version 1 · recorded on the CAPPZ admin chain · content sha256 a55f3ce458e21acd