@@ -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+
93156describe ( '@feathersjs/authentication-oauth service' , ( ) => {
94157 const port = 9778
95158 const req = axios . create ( {
0 commit comments