CodamAIDocs
Topicdone

Rejections that reveal nothing

Unknown and suspended look the same, public endpoints always answer the same way, admin rejections are identical. Why this way nobody can probe for information.

Variants
form: new and known addresslink: unknown or expiredtenant: unknown, suspended or unreachableadministration: no permissionlookups for services

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

AreaSituationWhat the caller sees
Self-registration POST /cias/registration/selfnew address202 accepted
address already has an account202 accepted, the same body
too many attempts429, without saying when it works again
Link from the mail POST /cias/registration/verifylink unknown404 cias.registration.not-found
link expired, the registration is still waiting404 cias.registration.not-found, the same answer
link already redeemed202 accepted
Filter chain, tenant gatetenant unknown403 cias.authentication.tenant-not-served
tenant suspended, closed or outside its validity403 cias.authentication.tenant-not-served
the key cannot be a tenant at all, for example Nordbau403 cias.authentication.tenant-not-served
CIAS unreachable, nothing remembered403 cias.authentication.tenant-not-served
attribute values in this tenant cannot be read403 cias.authentication.tenant-not-served
Administer tenants /cias/admin/tenantsno permission, whichever tenant403 cias.tenancy.administration-denied, text not permitted
Read, suspend, close, move usersno permission, whichever person403 cias.user.administration-denied, text not permitted
Administer roles and groupsno permission, whatever the reason403 cias.authorization.denied, text not permitted
Administer registrationsno permission403 cias.registration.not-authorized, text not permitted
Tenant lookup for services GET /cias/lookup/tenants/{key}caller does not have the lookup role403, exactly as in tenant administration
the key cannot be a tenant at all404, exactly like an unknown tenant
Attribute lookup for services GET /cias/lookup/users/{id}/attributescaller does not have the lookup role403, exactly as in user administration
CIAS does not know the person200 with empty values, like a person without values
CDMS GET /cias/fetchcaller has no reader role403 cdms.cias.declaration-denied, without naming the roles that would have been enough

The variants

Where identical answers protect

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

Sources in the code and the knowledge base
  • CIAS/cias-registration – RegistrationController (constant body, 202), RegistrationService (register: same path for known and new addresses, verify), RegistrationExceptionHandler (not-found, not-authorized, misconfigured, throttled without Retry-After, disabled)
  • CIAS/cias-authentication – TenantGate.admit (UNKNOWN, NOT_SERVED, LOOKUP_UNAVAILABLE, invalid key), JwtSessionFilter.refuse, TokenParser.admit, RequestAdmission
  • CIAS/cias-tenancy – TenantAdministrationService.requireAdministrator, TenantExceptionHandler (REFUSED, operation logged), TenantLookupController (invalid key = 404, same refusal as administration)
  • CIAS/cias-user – UserService.requireAdministrator, UserExceptionHandler, AttributeLookupController (unknown person = 200 with empty list)
  • CIAS/cias-authorization – AuthorizationExceptionHandler (REFUSED cias.authorization.denied), GroupService, RoleCatalogService
  • CDMS/cdms-authorization – CiasApi.getCiasProfile (refusal without role names)
  • CIAS/cias-registration/docs/adr – ADR-012, ADR-014; CIAS/cias-authentication/docs/adr – ADR-021
Search