SECURITY WIN

Phase Security Assessment

Identified 9 security vulnerabilities including critical authorization flaws from double-negative logic errors bypassing permission checks, type errors in authorization function calls, missing IDOR checks on credential retrieval and lease operations, insecure token transmission in GitLab OAuth adapter, and cross-organization payment method deletion. All 9 reported items were remediated across multiple PRs (#722-731).
January 202614 min read
Faizan
Phase Authorization BypassIDOR Credential Theft Type Confusion

Executive Summary

This security assessment was conducted using Kolega.dev's automated security remediation platform, which combines traditional security scanning (SAST, SCA, secrets detection) with proprietary AI-powered deep code analysis. Our two-tier detection approach identified vulnerabilities that standard tools miss, including complex logic flaws and cross-service injection vectors.

Our analysis of the Phase repository identified 9 vulnerabilities through Kolega.dev Deep Code Scan (Tier 2) that warrant attention.

Vulnerability Overview

ID

Title

PR/Ticket

V1

Missing Access Control Check for User Token Authentication

PR #731

V2

No Authorization Checks on Credential Retrieval

PR #730

V3

CreateEnvironmentKeyMutation - Double-Negative Logic Error Bypasses Authorization

PR #723

V4

Insecure Token Transmission in URL Parameters - GitLab Adapter

PR #722

V5

ReadSecretMutation - Insufficient Authorization (Organization-Level Only)

PR #724

V6

CreatePersonalSecretMutation - Type Error in Authorization Function Call

PR #726

V7

DeletePersonalSecretMutation - Type Error in Authorization Function Call

PR #726

V8

Authorization Check Uses Wrong Organization Reference

PR #727

V9

Missing IDOR Authorization Check on Lease Operations

PR #729

Responsible Disclosure Timeline

Kolega.dev follows responsible disclosure practices. We coordinated privately through Phase's official security reporting channel.

7 January 2026

Initial report sent to Phase by email to security@phase.dev

7 January 2026

Due to email failure, re-sent report to Phase by email to nimish@phase.dev, info@phase.dev, rohan@phase.dev

12 January 2026

Response from Phase confirming 9 of the reported items were remediated and merged through several PRs.


Vulnerabilities Detail

V1: Missing Access Control Check for User Token Authentication

CWE: CWE-284 (Improper Access Control)
Location: backend/api/auth.py:115-126

Description
When token_type is 'User', the code authenticates the user but the exception handling (line 125-126) uses a bare Exception block that catches all errors including access control failures.

Evidence
The except block at line 125 catches ANY exception including the AuthenticationFailed raised at line 122. If access is denied, the exception is caught and converted to 'User not found', which is incorrect error handling and masks authorization failures.

Impact
Authorization failures are masked as authentication failures. More critically, if an exception is raised by get_org_member_from_user_token returning False (line 126), org_member will be False, causing AttributeError on line 119 which is then caught and misreported.

Remediation
Replace bare Exception with specific exception handling. Catch only DoesNotExist and ValueError. Let AuthenticationFailed propagate naturally. Test that access denied scenarios return proper 403 status codes.


V2: No Authorization Checks on Credential Retrieval

CWE: CWE-639 (Authorization Bypass Through User-Controlled Key)
Location: backend/api/utils/syncing/auth.py:38-58

Description
The get_credentials() function retrieves provider credentials by credential_id without any authorization checks. It directly queries the database and returns decrypted credentials for any provided credential_id, allowing privilege escalation and credential theft.

Evidence
No permission check exists to verify if the caller is authorized to access the credential. The function is called directly from multiple syncing modules without any authorization middleware. An authenticated user with knowledge of another user's credential_id could retrieve their third-party service credentials.

Impact
An attacker can enumerate credential IDs and retrieve plaintext third-party service tokens (GitHub, GitLab, AWS, Vercel, etc.), leading to unauthorized access to all integrated services and potential data theft or malicious modifications.

Remediation
Add authorization checks in get_credentials() to verify the caller owns or has permission to access the credential. Implement organization-level permission checks.


V3: CreateEnvironmentKeyMutation - Double-Negative Logic Error Bypasses Authorization

CWE: CWE-284 (Improper Access Control) + CWE-391 (Unchecked Error Condition)
Location: backend/backend/graphene/mutations/environment.py:330-336

Description
Critical logic error in authorization check: 'if not user_id is not None' creates a double negative that evaluates incorrectly. This allows users to bypass the permission check and create environment keys for any user they specify, leading to unauthorized access elevation.

Evidence
The condition 'not user_id is not None' is logically flawed. In Python: (not X is not Y) is parsed as (not X) is (not Y), not as (not (X is not Y)). This means when user_id exists, the condition becomes: (False is True) which is False, so the entire if block is skipped entirely, bypassing the permission check.

Impact
Any authenticated user can create environment keys for any other user without permission checks. This enables privilege escalation and unauthorized access to confidential environments.

Remediation
Fix double negative: Change 'if not user_id is not None' to 'if user_id is not None'. Then verify the user_has_permission() check is called. Pattern: if user_id is not None and not member_can_access_org(user_id, env.app.organisation.id): raise error


V4: Insecure Token Transmission in URL Parameters - GitLab Adapter

CWE: CWE-598 (Use of GET Request with Sensitive Query Strings)
Location: backend/api/authentication/adapters/gitlab.py:61

Description
The GitLab OAuth2 adapter passes the access token as a URL parameter in GET requests. The code makes a GET request with params={'access_token': token.token}, which exposes the sensitive access token in the URL. Access tokens should be transmitted in Authorization headers.

Evidence
Tokens passed as URL parameters are logged in browser history, server logs, referrer headers, and proxy logs. This violates OAuth2 and security best practices. The token should be in the Authorization header.

Impact
Access tokens can be exposed through browser history, server logs, HTTP referrer headers, or proxy logs. An attacker with access to logs could steal tokens and impersonate users.

Remediation
Change the request to use an Authorization header instead: headers = {'Authorization': f'Bearer {token.token}'}. Use requests.get(self.profile_url, headers=headers).


V5: ReadSecretMutation - Insufficient Authorization (Organization-Level Only)

CWE: CWE-639 (Authorization Bypass Through User-Controlled Key)
Location: backend/backend/graphene/mutations/environment.py:1052-1060

Description
Authorization check only validates organization membership, not environment or app access. Any organization member can read ANY secret in the organization, even those they should not have access to.

Evidence
Check at line 1052 only validates organization membership with user_is_org_member(). No verification of: (1) user_can_access_environment(), (2) user_has_permission() for 'read' on 'Secrets', or (3) presence in EnvironmentKey table. Contrast with resolve_secrets() in types.py line 548-556 which correctly checks both environment AND secret permissions.

Impact
Privilege escalation: users with limited app/environment access can read all organization secrets by guessing/brute-forcing secret IDs. Violates least privilege principle.

Remediation
Add environment-level access check: if not user_can_access_environment(info.context.user.userId, secret.environment.id): raise GraphQLError(...). Also add: if not user_has_permission(info.context.user, 'read', 'Secrets', org, True): raise error


V6: CreatePersonalSecretMutation - Type Error in Authorization Function Call

CWE: CWE-366 (Race Condition within a Thread) + CWE-841 (Improper Enforcement of Message Integrity)
Location: backend/backend/graphene/mutations/environment.py:1087

Description
Type mismatch in authorization check: passes User object instead of user ID to user_can_access_environment(). This causes type error or unexpected behavior in authorization validation, potentially bypassing the permission check.

Evidence
Function signature expects user_id (string/int) but receives User object. This type mismatch can cause: (1) TypeError that gets caught and ignored, (2) Falsy object comparison that passes validation, or (3) Different permission logic path. Correct call should be: user_can_access_environment(info.context.user.userId, secret.environment.id)

Impact
Authorization bypass allowing users to create personal secrets (overrides) for environments they don't have access to. Combined with CreateEnvironmentKeyMutation flaw, enables full privilege escalation.

Remediation
Change line 1087 to: if not user_can_access_environment(info.context.user.userId, secret.environment.id):. Apply same fix to DeletePersonalSecretMutation at line 1114.


V7: DeletePersonalSecretMutation - Type Error in Authorization Function Call

CWE: CWE-366 (Race Condition within a Thread)
Location: backend/backend/graphene/mutations/environment.py:1114

Description
Same type error as CreatePersonalSecretMutation: passes User object instead of user ID to user_can_access_environment().

Evidence
Same issue as finding #3 - User object passed instead of userId. Expected function signature: user_can_access_environment(user_id: str, env_id: str)

Impact
Authorization bypass in delete operation for personal secrets. Users can delete personal overrides for any secret regardless of environment access.

Remediation
Change to: user_can_access_environment(info.context.user.userId, secret.environment.id)


V8: Authorization Check Uses Wrong Organization Reference

CWE: CWE-639 (Authorization Bypass Through User-Controlled Key)
Location: backend/ee/billing/graphene/mutations/stripe.py:96-110

Description
The DeletePaymentMethodMutation retrieves the organization but does not properly validate that the payment method belongs to the correct organization. An attacker could delete payment methods from other organizations.

Evidence
While there is an authorization check for the organization (line 99), the code does not verify that the payment_method_id actually belongs to the organization before detaching it. An attacker knowing valid payment method IDs from other organizations could detach them by calling this mutation with a different organization_id they have access to.

Impact
An authenticated user with 'update' permission on 'Billing' for organization A could delete payment methods from organization B by passing organization B's ID and a payment method ID they discovered from organization B.

Remediation
Verify that the payment method belongs to the target organization's Stripe customer before detaching: payment_method = stripe.PaymentMethod.retrieve(payment_method_id); assert payment_method.customer == org.stripe_customer_id


V9: Missing IDOR Authorization Check on Lease Operations

CWE: CWE-639 (Authorization Bypass Through User-Controlled Key)
Location: backend/ee/integrations/secrets/dynamic/rest/views.py:215-219

Description
The _get_lease_or_404 method retrieves a lease by ID without verifying that the requesting user has access to the lease's parent secret or environment. A user can directly query any lease ID and retrieve it.

Evidence
The method performs no authorization checks. The _assert_can_act_on_lease method is called in DELETE and PUT handlers but only checks if the user is the lease owner OR has permission on DynamicSecretLeases. A user without lease permissions but with basic app access could bypass the owner check by providing another user's lease ID. The logic at line 225-230 allows access if the user IS the lease owner but doesn't verify access to the secret itself.

Impact
An attacker can read, renew, or revoke leases belonging to other users in the same organization. This allows credential theft, privilege escalation, and denial of service attacks on other users' temporary credentials.

Remediation
Add authorization check in _get_lease_or_404 to verify the user has access to the lease's parent secret's environment. Fetch the lease with environment filtering: 'DynamicSecretLease.objects.filter(secret__environment__in=user_accessible_envs).get(id=lease_id)'



Simple 3 click setup.

Deploy Kolega.dev.

Find and fix your technical debt.