Skip to content

Commit d6b0b5c

Browse files
authored
fix(authentication-oauth): Fix OAuth Callback Account Takeover (#3663)
1 parent 163e664 commit d6b0b5c

File tree

2 files changed

+71
-2
lines changed

2 files changed

+71
-2
lines changed

packages/authentication-oauth/src/service.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createDebug } from '@feathersjs/commons'
22
import { HookContext, NextFunction, Params } from '@feathersjs/feathers'
3-
import { FeathersError, GeneralError } from '@feathersjs/errors'
3+
import { FeathersError, GeneralError, NotAuthenticated } from '@feathersjs/errors'
44
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
55
//@ts-ignore
66
import Grant from 'grant/lib/grant'
@@ -115,7 +115,13 @@ export class OAuthService {
115115
redirect
116116
}
117117

118-
const payload = grant?.response || result?.session?.response || result?.state?.response || params.query
118+
const payload = grant?.response || result?.session?.response || result?.state?.response
119+
120+
if (!payload) {
121+
throw new NotAuthenticated(
122+
'No valid oAuth response. You must initiate the oAuth flow from the authorize endpoint.'
123+
)
124+
}
119125
const authentication = {
120126
strategy: name,
121127
...payload

packages/authentication-oauth/test/service.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,69 @@ describe('@feathersjs/authentication-oauth service security', () => {
9090
})
9191
})
9292

93+
describe('Account Takeover via OAuth Callback Query Parameter Forgery', () => {
94+
const port = 9781
95+
const req = axios.create({
96+
withCredentials: true,
97+
maxRedirects: 0
98+
})
99+
let app: Awaited<ReturnType<typeof expressFixture>>
100+
101+
const fetchResponse = async (url: string, headers?: Record<string, string>): Promise<AxiosResponse> => {
102+
try {
103+
return await req.get(url, { headers })
104+
} catch (error: any) {
105+
return error.response
106+
}
107+
}
108+
109+
before(async () => {
110+
app = await expressFixture(port, 5118)
111+
// Create an existing user to demonstrate account takeover
112+
await app.service('users').create({ email: 'admin@example.com' })
113+
})
114+
115+
after(async () => {
116+
await app.teardown()
117+
})
118+
119+
it('should not authenticate when calling callback directly with forged query parameters', async () => {
120+
const host = `http://localhost:${port}`
121+
// Attack: directly hit the callback endpoint with a forged profile in query params
122+
// Without a valid OAuth session, Grant returns no response, so the payload
123+
// falls back to params.query which the attacker controls
124+
const attackUrl = `${host}/oauth/github/callback?profile[foo]=bar&code=fake&state=fake`
125+
126+
const response = await fetchResponse(attackUrl)
127+
128+
// The forged request must NOT return a valid access token.
129+
// Currently the vulnerable code falls back to params.query as the auth payload,
130+
// allowing the attacker to forge authentication and receive a JWT.
131+
const hasAccessToken =
132+
response.data?.accessToken ||
133+
(response.headers.location && response.headers.location.includes('access_token'))
134+
135+
assert.ok(!hasAccessToken, `Forged callback request should not return an access token but got one`)
136+
})
137+
138+
it('should not authenticate when callback is called with a targeted profile id', async () => {
139+
const host = `http://localhost:${port}`
140+
// Attack: forge a specific profile ID to target a known user
141+
const attackUrl = `${host}/oauth/github/callback?profile[id]=12345&code=fake&state=fake`
142+
143+
const response = await fetchResponse(attackUrl)
144+
145+
const hasAccessToken =
146+
response.data?.accessToken ||
147+
(response.headers.location && response.headers.location.includes('access_token'))
148+
149+
assert.ok(
150+
!hasAccessToken,
151+
`Forged callback request with targeted profile ID should not return an access token`
152+
)
153+
})
154+
})
155+
93156
describe('@feathersjs/authentication-oauth service', () => {
94157
const port = 9778
95158
const req = axios.create({

0 commit comments

Comments
 (0)