CVE-2025-48370
Description
auth-js is an isomorphic Javascript library for Supabase Auth. Prior to version 2.70.0, the library functions getUserById, deleteUser, updateUserById, listFactors and deleteFactor did not require the user supplied values to be valid UUIDs. This could lead to a URL path traversal, resulting in the wrong API function being called. Implementations that follow security best practice and validate user controlled inputs, such as the userId are not affected by this. This issue has been patched in version 2.70.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@supabase/auth-jsnpm | < 2.70.0 | 2.70.0 |
Affected products
1Patches
2b64fae2e49ea1bcb76e479e5feat: validate uuid and sign out scope parameters to functions (#1063)
4 files changed · +55 −19
src/GoTrueAdminApi.ts+21 −2 modified@@ -5,7 +5,7 @@ import { _request, _userResponse, } from './lib/fetch' -import { resolveFetch } from './lib/helpers' +import { resolveFetch, validateUUID } from './lib/helpers' import { AdminUserAttributes, GenerateLinkParams, @@ -19,6 +19,8 @@ import { AuthMFAAdminListFactorsParams, AuthMFAAdminListFactorsResponse, PageParams, + SIGN_OUT_SCOPES, + SignOutScope, } from './lib/types' import { AuthError, isAuthError } from './lib/errors' @@ -59,8 +61,14 @@ export default class GoTrueAdminApi { */ async signOut( jwt: string, - scope: 'global' | 'local' | 'others' = 'global' + scope: SignOutScope = SIGN_OUT_SCOPES[0] ): Promise<{ data: null; error: AuthError | null }> { + if (SIGN_OUT_SCOPES.indexOf(scope) < 0) { + throw new Error( + `@supabase/auth-js: Parameter scope must be one of ${SIGN_OUT_SCOPES.join(', ')}` + ) + } + try { await _request(this.fetch, 'POST', `${this.url}/logout?scope=${scope}`, { headers: this.headers, @@ -219,6 +227,8 @@ export default class GoTrueAdminApi { * This function should only be called on a server. Never expose your `service_role` key in the browser. */ async getUserById(uid: string): Promise<UserResponse> { + validateUUID(uid) + try { return await _request(this.fetch, 'GET', `${this.url}/admin/users/${uid}`, { headers: this.headers, @@ -241,6 +251,8 @@ export default class GoTrueAdminApi { * This function should only be called on a server. Never expose your `service_role` key in the browser. */ async updateUserById(uid: string, attributes: AdminUserAttributes): Promise<UserResponse> { + validateUUID(uid) + try { return await _request(this.fetch, 'PUT', `${this.url}/admin/users/${uid}`, { body: attributes, @@ -266,6 +278,8 @@ export default class GoTrueAdminApi { * This function should only be called on a server. Never expose your `service_role` key in the browser. */ async deleteUser(id: string, shouldSoftDelete = false): Promise<UserResponse> { + validateUUID(id) + try { return await _request(this.fetch, 'DELETE', `${this.url}/admin/users/${id}`, { headers: this.headers, @@ -286,6 +300,8 @@ export default class GoTrueAdminApi { private async _listFactors( params: AuthMFAAdminListFactorsParams ): Promise<AuthMFAAdminListFactorsResponse> { + validateUUID(params.userId) + try { const { data, error } = await _request( this.fetch, @@ -311,6 +327,9 @@ export default class GoTrueAdminApi { private async _deleteFactor( params: AuthMFAAdminDeleteFactorParams ): Promise<AuthMFAAdminDeleteFactorResponse> { + validateUUID(params.userId) + validateUUID(params.id) + try { const data = await _request( this.fetch,
src/lib/helpers.ts+9 −1 modified@@ -1,6 +1,6 @@ import { API_VERSION_HEADER_NAME, BASE64URL_REGEX } from './constants' import { AuthInvalidJwtError } from './errors' -import { base64UrlToUint8Array, stringFromBase64URL, stringToBase64URL } from './base64url' +import { base64UrlToUint8Array, stringFromBase64URL } from './base64url' import { JwtHeader, JwtPayload, SupportedStorage } from './types' export function expiresAt(expiresIn: number) { @@ -357,3 +357,11 @@ export function getAlgorithm(alg: 'RS256' | 'ES256'): RsaHashedImportParams | Ec throw new Error('Invalid alg claim') } } + +const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ + +export function validateUUID(str: string) { + if (!UUID_REGEX.test(str)) { + throw new Error('@supabase/auth-js: Expected parameter to be UUID but is not') + } +}
src/lib/types.ts+3 −0 modified@@ -1279,3 +1279,6 @@ export interface JWK { kid?: string [key: string]: any } + +export const SIGN_OUT_SCOPES = ['global', 'local', 'others'] as const +export type SignOutScope = typeof SIGN_OUT_SCOPES[number]
test/GoTrueApi.test.ts+22 −16 modified@@ -16,7 +16,7 @@ import { import type { GenerateLinkProperties, User } from '../src/lib/types' const INVALID_EMAIL = 'xx:;x@x.x' -const INVALID_USER_ID = 'invalid-uuid' +const NON_EXISTANT_USER_ID = '83fd9e20-7a80-46e4-bf29-a86e3d6bbf66' describe('GoTrueAdminApi', () => { describe('User creation', () => { @@ -152,7 +152,7 @@ describe('GoTrueAdminApi', () => { }) test('getUserById() returns AuthError when user id is invalid', async () => { - const { error, data } = await serviceRoleApiClient.getUserById(INVALID_USER_ID) + const { error, data } = await serviceRoleApiClient.getUserById(NON_EXISTANT_USER_ID) expect(error).not.toBeNull() expect(data.user).toBeNull() @@ -283,7 +283,7 @@ describe('GoTrueAdminApi', () => { }) test('deleteUser() returns AuthError when user id is invalid', async () => { - const { error, data } = await serviceRoleApiClient.deleteUser(INVALID_USER_ID) + const { error, data } = await serviceRoleApiClient.deleteUser(NON_EXISTANT_USER_ID) expect(error).not.toBeNull() expect(data.user).toBeNull() @@ -479,7 +479,7 @@ describe('GoTrueAdminApi', () => { test('listUsers() returns AuthError when page is invalid', async () => { const { error, data } = await serviceRoleApiClient.listUsers({ page: -1, - perPage: 10 + perPage: 10, }) expect(error).not.toBeNull() @@ -489,8 +489,8 @@ describe('GoTrueAdminApi', () => { describe('Update User', () => { test('updateUserById() returns AuthError when user id is invalid', async () => { - const { error, data } = await serviceRoleApiClient.updateUserById(INVALID_USER_ID, { - email: 'new@email.com' + const { error, data } = await serviceRoleApiClient.updateUserById(NON_EXISTANT_USER_ID, { + email: 'new@email.com', }) expect(error).not.toBeNull() @@ -513,7 +513,7 @@ describe('GoTrueAdminApi', () => { expect(uid).toBeTruthy() const { error: enrollError } = await authClientWithSession.mfa.enroll({ - factorType: 'totp' + factorType: 'totp', }) expect(enrollError).toBeNull() @@ -526,35 +526,41 @@ describe('GoTrueAdminApi', () => { const factorId = data?.factors[0].id expect(factorId).toBeDefined() - const { data: deletedData, error: deletedError } = await serviceRoleApiClient.mfa.deleteFactor({ - userId: uid, - id: factorId! - }) + const { data: deletedData, error: deletedError } = + await serviceRoleApiClient.mfa.deleteFactor({ + userId: uid, + id: factorId!, + }) expect(deletedError).toBeNull() expect(deletedData).not.toBeNull() const deletedId = (deletedData as any)?.data?.id console.log('deletedId:', deletedId) expect(deletedId).toEqual(factorId) - const { data: latestData, error: latestError } = await serviceRoleApiClient.mfa.listFactors({ userId: uid }) + const { data: latestData, error: latestError } = await serviceRoleApiClient.mfa.listFactors({ + userId: uid, + }) expect(latestError).toBeNull() expect(latestData).not.toBeNull() expect(Array.isArray(latestData?.factors)).toBe(true) expect(latestData?.factors.length).toEqual(0) }) - test('mfa.listFactors returns AuthError for invalid user', async () => { - const { data, error } = await serviceRoleApiClient.mfa.listFactors({ userId: INVALID_USER_ID }) + const { data, error } = await serviceRoleApiClient.mfa.listFactors({ + userId: NON_EXISTANT_USER_ID, + }) expect(data).toBeNull() expect(error).not.toBeNull() }) test('mfa.deleteFactors returns AuthError for invalid user', async () => { - const { data, error } = await serviceRoleApiClient.mfa.deleteFactor({ userId: INVALID_USER_ID , id: '1' }) + const { data, error } = await serviceRoleApiClient.mfa.deleteFactor({ + userId: NON_EXISTANT_USER_ID, + id: NON_EXISTANT_USER_ID, + }) expect(data).toBeNull() expect(error).not.toBeNull() }) - }) })
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-8r88-6cj9-9fh5ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-48370ghsaADVISORY
- github.com/supabase/auth-js/commit/1bcb76e479e51cd9bca2d7732d0bf3199e07a693nvdWEB
- github.com/supabase/auth-js/pull/1063nvdWEB
- github.com/supabase/auth-js/security/advisories/GHSA-8r88-6cj9-9fh5nvdWEB
News mentions
0No linked articles in our index yet.