Skip to content

Commit 79b41a6

Browse files
committed
feat: improved insecure getSession() warning
1 parent 9748dd9 commit 79b41a6

File tree

4 files changed

+194
-43
lines changed

4 files changed

+194
-43
lines changed

src/GoTrueClient.ts

+5-15
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
supportsLocalStorage,
3737
parseParametersFromURL,
3838
getCodeChallengeAndMethod,
39+
userNotAvailableProxy,
3940
} from './lib/helpers'
4041
import { localStorageAdapter, memoryLocalStorageAdapter } from './lib/local-storage'
4142
import { polyfillGlobalThis } from './lib/polyfills'
@@ -1120,21 +1121,10 @@ export default class GoTrueClient {
11201121

11211122
if (!hasExpired) {
11221123
if (this.storage.isServer) {
1123-
let suppressWarning = this.suppressGetSessionWarning
1124-
const proxySession: Session = new Proxy(currentSession, {
1125-
get: (target: any, prop: string, receiver: any) => {
1126-
if (!suppressWarning && prop === 'user') {
1127-
// only show warning when the user object is being accessed from the server
1128-
console.warn(
1129-
'Using the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.'
1130-
)
1131-
suppressWarning = true // keeps this proxy instance from logging additional warnings
1132-
this.suppressGetSessionWarning = true // keeps this client's future proxy instances from warning
1133-
}
1134-
return Reflect.get(target, prop, receiver)
1135-
},
1136-
})
1137-
currentSession = proxySession
1124+
currentSession.user = userNotAvailableProxy(
1125+
currentSession.user,
1126+
'User object comes from insecure storage and may not be authentic. Call getUser() instead to prevent security issues.'
1127+
)
11381128
}
11391129

11401130
return { data: { session: currentSession }, error: null }

src/lib/helpers.ts

+64-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
11
import { API_VERSION_HEADER_NAME } from './constants'
2-
import { SupportedStorage } from './types'
2+
import { SupportedStorage, User } from './types'
33

44
export function expiresAt(expiresIn: number) {
55
const timeNow = Math.round(Date.now() / 1000)
66
return timeNow + expiresIn
77
}
88

99
export function uuid() {
10+
if (
11+
'crypto' in globalThis &&
12+
typeof globalThis.crypto === 'object' &&
13+
'randomUUID' in globalThis.crypto &&
14+
typeof globalThis.crypto.randomUUID === 'function'
15+
) {
16+
try {
17+
return globalThis.crypto.randomUUID()
18+
} catch (e: any) {
19+
// ignore and fall back to below implementation, as crypto.randomUUID()
20+
// only works in secure contexts
21+
}
22+
}
23+
1024
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
1125
const r = (Math.random() * 16) | 0,
1226
v = c == 'x' ? r : (r & 0x3) | 0x8
@@ -344,3 +358,52 @@ export function parseResponseAPIVersion(response: Response) {
344358
return null
345359
}
346360
}
361+
362+
function hasOwnProperty(target: any, property: any) {
363+
if (!(target instanceof Object) || target === null) {
364+
return false
365+
}
366+
367+
if ('hasOwn' in Object && typeof (Object as any).hasOwn === 'function') {
368+
return (Object as any).hasOwn(target, property)
369+
}
370+
371+
return Object.prototype.hasOwnProperty.call(target, property)
372+
}
373+
374+
const WARNING_SYMBOL = Symbol('WARNING')
375+
const PROPERTY_WARNINGS_SHOWN: { [reason: string]: boolean } = {}
376+
377+
export function userNotAvailableProxy(target: User, reason: string): User {
378+
const warning = `@supabase/auth-js: Accessing any property of this object is insecure. Reason: ${reason}` // this shows the warning in console.log, Inspector or other object dumps
379+
380+
;(target as any)[WARNING_SYMBOL] = warning
381+
382+
return new Proxy(target, {
383+
get: (target: User, prop: PropertyKey, receiver: any) => {
384+
if (prop === 'toString') {
385+
return () => `WARNING: ${warning} -- ${Reflect.get(target, prop, receiver).call(target)}`
386+
}
387+
388+
if (
389+
!PROPERTY_WARNINGS_SHOWN[reason] &&
390+
typeof prop === 'string' &&
391+
hasOwnProperty(target, prop)
392+
) {
393+
PROPERTY_WARNINGS_SHOWN[reason] = true
394+
395+
console.warn(
396+
`@supabase/auth-js: Accessing the "${prop}" (or any other) property of the user object is not secure. Reason: ${reason}`
397+
)
398+
}
399+
400+
const value = Reflect.get(target, prop, receiver)
401+
402+
if (typeof value === 'object' && value && !value[WARNING_SYMBOL]) {
403+
value[WARNING_SYMBOL] = warning
404+
}
405+
406+
return value
407+
},
408+
})
409+
}

test/GoTrueClient.test.ts

+22-26
Original file line numberDiff line numberDiff line change
@@ -958,7 +958,7 @@ describe('GoTrueClient with storageisServer = true', () => {
958958
expect(warnings.length).toEqual(0)
959959
})
960960

961-
test('getSession() emits insecure warning, once per server client, when user object is accessed', async () => {
961+
test('getSession() emits insecure warning, once per server client, when property on user object is accessed', async () => {
962962
const storage = memoryLocalStorageAdapter({
963963
[STORAGE_KEY]: JSON.stringify({
964964
access_token: 'jwt.accesstoken.signature',
@@ -981,29 +981,26 @@ describe('GoTrueClient with storageisServer = true', () => {
981981
data: { session },
982982
} = await client.getSession()
983983

984-
const user = session?.user // accessing the user object from getSession should emit a warning the first time
985-
expect(user).not.toBeNull()
986-
expect(warnings.length).toEqual(1)
987-
expect(
988-
warnings[0][0].startsWith(
989-
'Using the user object as returned from supabase.auth.getSession() '
990-
)
991-
).toEqual(true)
992-
993-
const user2 = session?.user // accessing the user object further should not emit a warning
994-
expect(user2).not.toBeNull()
995-
expect(warnings.length).toEqual(1)
996-
997-
const {
998-
data: { session: session2 },
999-
} = await client.getSession() // create new proxy instance
1000-
1001-
const user3 = session2?.user // accessing the user object in subsequent proxy instances, for this client, should not emit a warning
1002-
expect(user3).not.toBeNull()
1003-
expect(warnings.length).toEqual(1)
984+
expect(session?.user).not.toBeNull()
985+
expect(session?.user?.id).toMatchInlineSnapshot(`"random-user-id"`)
986+
expect(warnings).toMatchInlineSnapshot(`
987+
Array [
988+
Array [
989+
"@supabase/auth-js: Accessing the \\"id\\" (or any other) property of the user object is not secure. Reason: User object comes from insecure storage and may not be authentic. Call getUser() instead to prevent security issues.",
990+
],
991+
]
992+
`)
993+
expect(session?.user?.app_metadata).not.toBeNull()
994+
expect(warnings).toMatchInlineSnapshot(`
995+
Array [
996+
Array [
997+
"@supabase/auth-js: Accessing the \\"id\\" (or any other) property of the user object is not secure. Reason: User object comes from insecure storage and may not be authentic. Call getUser() instead to prevent security issues.",
998+
],
999+
]
1000+
`)
10041001
})
10051002

1006-
test('getSession emits no warnings if getUser is called prior', async () => {
1003+
test('getSession() emits no warnings if getUser() is called prior', async () => {
10071004
const client = new GoTrueClient({
10081005
url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON,
10091006
autoRefreshToken: false,
@@ -1021,15 +1018,14 @@ describe('GoTrueClient with storageisServer = true', () => {
10211018
error,
10221019
} = await client.getUser() // should suppress any warnings
10231020
expect(error).toBeNull()
1024-
expect(user).not.toBeNull()
10251021

10261022
const {
10271023
data: { session },
10281024
} = await client.getSession()
10291025

1030-
const sessionUser = session?.user // accessing the user object from getSession shouldn't emit a warning
1031-
expect(sessionUser).not.toBeNull()
1032-
expect(warnings.length).toEqual(0)
1026+
// accessing the user object from getSession shouldn't emit a warning
1027+
expect(session?.user?.id).not.toBeNull()
1028+
expect(warnings).toMatchInlineSnapshot(`Array []`)
10331029
})
10341030

10351031
test('saveSession should overwrite the existing session', async () => {

test/helpers.test.ts

+103-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { parseParametersFromURL, parseResponseAPIVersion } from '../src/lib/helpers'
1+
import { User } from '../src/lib/types'
2+
import {
3+
parseParametersFromURL,
4+
parseResponseAPIVersion,
5+
userNotAvailableProxy,
6+
uuid,
7+
} from '../src/lib/helpers'
28

39
describe('parseParametersFromURL', () => {
410
it('should parse parameters from a URL with query params only', () => {
@@ -71,3 +77,99 @@ describe('parseResponseAPIVersion', () => {
7177
})
7278
})
7379
})
80+
81+
describe('uuid', () => {
82+
if ('crypto' in globalThis) {
83+
// nodejs 18, 20 don't have crypto
84+
85+
it('should generate a uuid when crypto.randomUUID() throws an error', () => {
86+
const originalRandomUUID = crypto.randomUUID
87+
88+
try {
89+
crypto.randomUUID = () => {
90+
throw new Error('Fail for test')
91+
}
92+
93+
expect(typeof uuid()).toEqual('string')
94+
} finally {
95+
crypto.randomUUID = originalRandomUUID
96+
}
97+
})
98+
99+
it('should generate a uuid with crypto.randomUUID()', () => {
100+
const originalRandomUUID = crypto.randomUUID
101+
102+
try {
103+
crypto.randomUUID = () => {
104+
return 'random-uuid'
105+
}
106+
107+
expect(uuid()).toEqual('random-uuid')
108+
} finally {
109+
crypto.randomUUID = originalRandomUUID
110+
}
111+
})
112+
}
113+
114+
it('should generate a uuid', () => {
115+
expect(typeof uuid()).toEqual('string')
116+
})
117+
})
118+
119+
describe('userNotAvailableProxy', () => {
120+
it('should show a warning when calling toString()', () => {
121+
expect(`${userNotAvailableProxy({} as User, 'REASON-0')}`).toMatchInlineSnapshot(
122+
`"WARNING: @supabase/auth-js: Accessing any property of this object is insecure. Reason: REASON-0 -- [object Object]"`
123+
)
124+
})
125+
126+
it('should show a warning when calling toString()', () => {
127+
const originalWarn = console.warn
128+
129+
try {
130+
let numberOfWarnings = 0
131+
console.warn = (...args: any[]) => {
132+
expect(args).toMatchInlineSnapshot(`
133+
Array [
134+
"@supabase/auth-js: Accessing the \\"id\\" (or any other) property of the user object is not secure. Reason: REASON-1",
135+
]
136+
`)
137+
numberOfWarnings += 1
138+
}
139+
140+
const object = userNotAvailableProxy(
141+
{
142+
id: 'user-id',
143+
created_at: new Date(0).toISOString(),
144+
aud: 'audience',
145+
app_metadata: {},
146+
user_metadata: {},
147+
},
148+
'REASON-1'
149+
)
150+
151+
expect(`${object}`).toMatchInlineSnapshot(
152+
`"WARNING: @supabase/auth-js: Accessing any property of this object is insecure. Reason: REASON-1 -- [object Object]"`
153+
)
154+
155+
expect(object.id).toMatchInlineSnapshot(`"user-id"`)
156+
expect(object.created_at).toMatchInlineSnapshot(`"1970-01-01T00:00:00.000Z"`)
157+
expect(object.aud).toMatchInlineSnapshot(`"audience"`)
158+
expect(object.app_metadata).toMatchInlineSnapshot(`
159+
Object {
160+
Symbol(WARNING): "@supabase/auth-js: Accessing any property of this object is insecure. Reason: REASON-1",
161+
}
162+
`)
163+
expect(object.user_metadata).toMatchInlineSnapshot(`
164+
Object {
165+
Symbol(WARNING): "@supabase/auth-js: Accessing any property of this object is insecure. Reason: REASON-1",
166+
}
167+
`)
168+
expect(object.email).toMatchInlineSnapshot(`undefined`)
169+
170+
expect(numberOfWarnings).toEqual(1)
171+
} finally {
172+
console.warn = originalWarn
173+
}
174+
})
175+
})

0 commit comments

Comments
 (0)