VYPR
Low severityOSV Advisory· Published May 27, 2025· Updated Apr 27, 2026

CVE-2025-48370

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.

PackageAffected versionsPatched versions
@supabase/auth-jsnpm
< 2.70.02.70.0

Affected products

1
  • Range: rc2.63.0-rc.11, rc2.63.0-rc.rc.11, rc2.63.0-rc.rc.rc.11, …

Patches

2
1bcb76e479e5

feat: validate uuid and sign out scope parameters to functions (#1063)

https://github.com/supabase/auth-jsStojan DimitrovskiMay 12, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.