At the start of 2026, we asked Cure53 to spend two weeks auditing our SCIM plugin and the directory sync layer.
SCIM is a standard for automating the provisioning and deprovisioning of users between systems. It allows identity providers like Azure AD or Okta to create, update, and disable accounts in applications without manual intervention. In practice, it’s the plumbing that keeps user directories in sync across tools. Check out the admin guide for more information.
The audit yielded twelve findings, you can read the incident report for the detailed breakdown. This post focuses on something else: sharing potential learnings with you, as those patterns may show up in your codebase too (none are unique to SCIM or to PHP).
The four patterns that came up {#the-four-patterns-that-came-up}
Across the findings, four themes emerged. They’re general lessons for anyone building software systems..
1. If you compare secrets, do it in constant time {#1.-if-you-compare-secrets,-do-it-in-constant-time}
We had a token comparison that used PHP’s ===. It’s the obvious choice and the wrong one.
The problem is timing. A standard comparison stops at the first mismatch, which means execution time varies depending on how many bytes match. With enough measurements, that difference becomes observable over the network.
The fix is straightforward: use functions designed for this purpose, like hash_equals() or password_verify(), which take constant time regardless of where the mismatch occurs.
The broader takeaway: anywhere you compare a user-supplied secret to a stored value, you should assume timing matters. If your codebase contains comparisons against variables named token, secret, key, or signature, it’s worth checking how they’re implemented.
2. Strong primitives don’t survive weak links {#2.-strong-primitives-don’t-survive-weak-links}
One of our token generators mixed random_bytes() with mt_rand().
Individually, random_bytes() is cryptographically secure. mt_rand() is not. Together they degrade to the weaker one.
In our case, we generated a large amount of entropy first, then used mt_rand() to select a substring. That reduced the effective security of the token to something attackers could realistically brute-force, especially combined with unsalted hashing.
The fix was simpler and stronger: generate a fixed number of random bytes and encode them directly (e.g. base64url).
The lesson here is subtle but important: cryptographic strength is not compositional.
3. Race conditions hide behind “perfectly fine” code {#3.-race-conditions-hide-behind-“perfectly-fine”-code}
Several findings had the same structure:
- Check if something exists
- If not, create it
That works in a single-threaded world. It breaks under concurrency.
Two requests arriving at the same time can both pass the check before either writes. The result: duplicate users, inconsistent state, or lost updates.
The fixes live at the database level:
- Enforce uniqueness with constraints
- Use transactions and locking where appropriate
We applied both, depending on the case.
The key insight is this: correctness under unit tests is not the same as correctness under concurrency. If your tests don’t simulate parallel requests, they’re not actually validating the guarantee your code is trying to make.
4. Error messages can leak more than you think {#4.-error-messages-can-leak-more-than-you-think}
Some of our API errors included details like usernames or internal IDs. Helpful for debugging, but also useful for anyone trying to enumerate users.
It’s an easy thing to overlook but someone reviewing your system adversarially may read your error messages very differently than you do.
Why we share this & acknowledgements {#why-we-share-this-&-acknowledgements}
The audit improved our codebase. But just as importantly, it keeps sharpening how we think about building and reviewing security-sensitive systems. We believe sharing these lessons openly is part of building better software as a community.
We would like to thank Cure53’s for their work. Each finding came with a clear reproducer and actionable recommendations, which made the fixes straightforward to implement. As with our previous audits, we’re publishing the results in full. Have a look at the report. It's thorough and practical and, like us, you may learn a thing or two!