What this is about
Every answer a server gives is information. If an endpoint answers “we know this address” differently from “we do not know this address”, anyone can exploit that. They send a thousand addresses and read from the answers which ones have an account with you. This is called enumeration: trying things out until you have a list.
The same applies to tenants (“is there a customer nordbau?”), to people and to roles. The problem is then not the access itself, but already the question whether something exists.
So at the important places, CIAS answers in a way that only says that something is not possible. Why it is not possible, CIAS writes to the application log. Only whoever runs the installation reads the log.
The picture
flowchart LR
A["unknown"] --> G{{"check"}}
B["suspended"] --> G
C["unreachable"] --> G
D["invalid key"] --> G
G -- "to the outside" --> R["one answer<br/>403 tenant-not-served"]
G -- "to the inside" --> L[("application log<br/>with the real reason")]
Read it like this: no matter why the tenant gate refuses, the caller sees the same answer. The difference ends up in the log, where an operator needs it and an attacker does not see it.
Situation → visible answer
| Area | Situation | What the caller sees |
|---|---|---|
Self-registration POST /cias/registration/self | new address | 202 accepted |
| address already has an account | 202 accepted, the same body | |
| too many attempts | 429, without saying when it works again | |
Link from the mail POST /cias/registration/verify | link unknown | 404 cias.registration.not-found |
| link expired, the registration is still waiting | 404 cias.registration.not-found, the same answer | |
| link already redeemed | 202 accepted | |
| Filter chain, tenant gate | tenant unknown | 403 cias.authentication.tenant-not-served |
| tenant suspended, closed or outside its validity | 403 cias.authentication.tenant-not-served | |
the key cannot be a tenant at all, for example Nordbau | 403 cias.authentication.tenant-not-served | |
| CIAS unreachable, nothing remembered | 403 cias.authentication.tenant-not-served | |
| attribute values in this tenant cannot be read | 403 cias.authentication.tenant-not-served | |
Administer tenants /cias/admin/tenants | no permission, whichever tenant | 403 cias.tenancy.administration-denied, text not permitted |
| Read, suspend, close, move users | no permission, whichever person | 403 cias.user.administration-denied, text not permitted |
| Administer roles and groups | no permission, whatever the reason | 403 cias.authorization.denied, text not permitted |
| Administer registrations | no permission | 403 cias.registration.not-authorized, text not permitted |
Tenant lookup for services GET /cias/lookup/tenants/{key} | caller does not have the lookup role | 403, exactly as in tenant administration |
| the key cannot be a tenant at all | 404, exactly like an unknown tenant | |
Attribute lookup for services GET /cias/lookup/users/{id}/attributes | caller does not have the lookup role | 403, exactly as in user administration |
| CIAS does not know the person | 200 with empty values, like a person without values | |
CDMS GET /cias/fetch | caller has no reader role | 403 cdms.cias.declaration-denied, without naming the roles that would have been enough |
The variants
When: Someone submits the registration form.
CIAS takes the same path for both addresses: record the registration, create a link, wait. Only the mail differs. A new address is asked to confirm, a known one gets a note about its account. Only whoever owns the mailbox reads the mail.
Result: Always 202 with the same body. See Protecting the public endpoints.
When: Someone redeems a confirmation or invitation link that does not fit (anymore).
The answer does not reveal whether the link never existed or has expired. A link that has already been redeemed, on the other hand, answers with 202. That is not a leak, but consideration for mail programs that open links in advance.
Result: 404 cias.registration.not-found, text no open registration for this link
When: A request with a valid token names a tenant that the tenant gate does not admit.
Every reason gets the same key. Otherwise anyone with a valid token could try company names and so query the customer list. Even a key that cannot be a tenant at all is not rejected with 400, but treated like an unknown one.
Result: 403 cias.authentication.tenant-not-served, text request refused. See Admit the tenant (tenant gate).
When: Someone calls an admin API without the required role.
The refusal names no reason: 403, a key for the area and the text not permitted. Which operation was attempted and why it failed is only in the log. When administering tenants and groups, and when reading, suspending, closing and moving users, CIAS checks the permission before it looks for the target. So whoever may not do it also does not learn whether the tenant, the group or the person exists.
Result: 403 with the key of the area
When: A CDMS node asks a standalone CIAS about a tenant or about attribute values.
These endpoints have roles of their own, separate from administration. Whoever lacks the role gets the same refusal as in administration. Whoever has it gets as little as possible: for a tenant only whether it is served, for a person only their values, never names, addresses or standings.
Result: See the table above
Where answers differ
Not every answer is the same, and it does not have to be. A difference is harmless if it reveals nothing about stored data that is none of the caller’s business:
- A broken request gets 400, for example when a required field is missing. That depends only on what you sent yourself.
- Throttling answers with 429, no matter which address is in the form.
- The tenant lookup for services differentiates on purpose: 200 for a known tenant, 404 for an unknown one. The asking service needs the difference for its log, and it already holds a service token of its own with a role of its own.
- The areas differ from each other. A refusal from tenant administration carries a different key from one from role administration. What stays the same: none of these refusals names its reason.
- Admin APIs for callers with permission say openly what is going on: 404 when something does not exist, 409 when it already exists. Whoever has the permission may know that.
A misconfigured installation does not reveal anything either: if, for example, a role is missing in the registration, the call fails, and the answer does not name which role is missing. That is in the log.
Pitfalls
Next
- Protecting the public endpoints
- Admit the tenant (tenant gate)
- The ceiling: nobody grants more than they have
- In CDMS the same applies to records: Why invisible objects return 404
- Reject when in doubt