@@ -202,7 +202,11 @@ describe('@feathersjs/authentication-oauth/strategy security', () => {
202202 }
203203 ) ,
204204 {
205- message : 'Referer "https://target.com.attacker.com/login" is not allowed.'
205+ message :
206+ 'Referer origin "https://target.com.attacker.com" is not allowed. ' +
207+ 'Configured origins: https://target.com. ' +
208+ 'Use a full origin (scheme + host + port when non-default). ' +
209+ 'Loopback hosts (localhost, 127.0.0.1, ::1, 0.0.0.0) match any port.'
206210 }
207211 )
208212 } )
@@ -220,7 +224,11 @@ describe('@feathersjs/authentication-oauth/strategy security', () => {
220224 }
221225 ) ,
222226 {
223- message : 'Referer "https://target.com-evil.attacker.com/login" is not allowed.'
227+ message :
228+ 'Referer origin "https://target.com-evil.attacker.com" is not allowed. ' +
229+ 'Configured origins: https://target.com. ' +
230+ 'Use a full origin (scheme + host + port when non-default). ' +
231+ 'Loopback hosts (localhost, 127.0.0.1, ::1, 0.0.0.0) match any port.'
224232 }
225233 )
226234 } )
@@ -238,6 +246,157 @@ describe('@feathersjs/authentication-oauth/strategy security', () => {
238246 assert . equal ( redirect , 'https://target.com#access_token=testing' )
239247 } )
240248 } )
249+
250+ describe ( 'loopback origin port matching (#3684)' , ( ) => {
251+ afterEach ( ( ) => {
252+ delete app . get ( 'authentication' ) . oauth . origins
253+ } )
254+
255+ it ( 'should allow any port on localhost when configured without a port' , async ( ) => {
256+ app . get ( 'authentication' ) . oauth . origins = [ 'http://localhost' ]
257+
258+ const redirect = await strategy . getRedirect (
259+ { accessToken : 'testing' } ,
260+ {
261+ headers : {
262+ referer : 'http://localhost:5173/login'
263+ }
264+ }
265+ )
266+
267+ // Redirect must use the referer port, not the config string
268+ assert . equal ( redirect , 'http://localhost:5173#access_token=testing' )
269+ } )
270+
271+ it ( 'should allow a different loopback port than the configured one' , async ( ) => {
272+ app . get ( 'authentication' ) . oauth . origins = [ 'http://localhost:3030' ]
273+
274+ const redirect = await strategy . getRedirect (
275+ { accessToken : 'testing' } ,
276+ {
277+ headers : {
278+ referer : 'http://localhost:3000/app'
279+ }
280+ }
281+ )
282+
283+ assert . equal ( redirect , 'http://localhost:3000#access_token=testing' )
284+ } )
285+
286+ it ( 'should allow any port on 127.0.0.1' , async ( ) => {
287+ app . get ( 'authentication' ) . oauth . origins = [ 'http://127.0.0.1:8080' ]
288+
289+ const redirect = await strategy . getRedirect (
290+ { accessToken : 'testing' } ,
291+ {
292+ headers : {
293+ referer : 'http://127.0.0.1:5173/'
294+ }
295+ }
296+ )
297+
298+ assert . equal ( redirect , 'http://127.0.0.1:5173#access_token=testing' )
299+ } )
300+
301+ it ( 'should allow any port on IPv6 loopback' , async ( ) => {
302+ app . get ( 'authentication' ) . oauth . origins = [ 'http://[::1]' ]
303+
304+ const redirect = await strategy . getRedirect (
305+ { accessToken : 'testing' } ,
306+ {
307+ headers : {
308+ referer : 'http://[::1]:4173/path'
309+ }
310+ }
311+ )
312+
313+ assert . equal ( redirect , 'http://[::1]:4173#access_token=testing' )
314+ } )
315+
316+ it ( 'should allow any port on 0.0.0.0' , async ( ) => {
317+ app . get ( 'authentication' ) . oauth . origins = [ 'http://0.0.0.0:3030' ]
318+
319+ const redirect = await strategy . getRedirect (
320+ { accessToken : 'testing' } ,
321+ {
322+ headers : {
323+ referer : 'http://0.0.0.0:5173/app'
324+ }
325+ }
326+ )
327+
328+ assert . equal ( redirect , 'http://0.0.0.0:5173#access_token=testing' )
329+ } )
330+
331+ it ( 'should not treat localhost and 127.0.0.1 as the same host' , async ( ) => {
332+ app . get ( 'authentication' ) . oauth . origins = [ 'http://localhost' ]
333+
334+ await assert . rejects (
335+ ( ) =>
336+ strategy . getRedirect (
337+ { accessToken : 'testing' } ,
338+ {
339+ headers : {
340+ referer : 'http://127.0.0.1:3000/login'
341+ }
342+ }
343+ ) ,
344+ {
345+ message :
346+ 'Referer origin "http://127.0.0.1:3000" is not allowed. ' +
347+ 'Configured origins: http://localhost. ' +
348+ 'Use a full origin (scheme + host + port when non-default). ' +
349+ 'Loopback hosts (localhost, 127.0.0.1, ::1, 0.0.0.0) match any port.'
350+ }
351+ )
352+ } )
353+
354+ it ( 'should still require exact port match for non-loopback hosts' , async ( ) => {
355+ app . get ( 'authentication' ) . oauth . origins = [ 'https://app.example.com' ]
356+
357+ await assert . rejects (
358+ ( ) =>
359+ strategy . getRedirect (
360+ { accessToken : 'testing' } ,
361+ {
362+ headers : {
363+ referer : 'https://app.example.com:8443/login'
364+ }
365+ }
366+ ) ,
367+ {
368+ message :
369+ 'Referer origin "https://app.example.com:8443" is not allowed. ' +
370+ 'Configured origins: https://app.example.com. ' +
371+ 'Use a full origin (scheme + host + port when non-default). ' +
372+ 'Loopback hosts (localhost, 127.0.0.1, ::1, 0.0.0.0) match any port.'
373+ }
374+ )
375+ } )
376+
377+ it ( 'should require matching scheme on loopback' , async ( ) => {
378+ app . get ( 'authentication' ) . oauth . origins = [ 'https://localhost' ]
379+
380+ await assert . rejects (
381+ ( ) =>
382+ strategy . getRedirect (
383+ { accessToken : 'testing' } ,
384+ {
385+ headers : {
386+ referer : 'http://localhost:3000/login'
387+ }
388+ }
389+ ) ,
390+ {
391+ message :
392+ 'Referer origin "http://localhost:3000" is not allowed. ' +
393+ 'Configured origins: https://localhost. ' +
394+ 'Use a full origin (scheme + host + port when non-default). ' +
395+ 'Loopback hosts (localhost, 127.0.0.1, ::1, 0.0.0.0) match any port.'
396+ }
397+ )
398+ } )
399+ } )
241400} )
242401
243402describe ( '@feathersjs/authentication-oauth/strategy' , ( ) => {
@@ -359,7 +518,11 @@ describe('@feathersjs/authentication-oauth/strategy', () => {
359518 }
360519 ) ,
361520 {
362- message : 'Referer "https://example.com" is not allowed.'
521+ message :
522+ 'Referer origin "https://example.com" is not allowed. ' +
523+ 'Configured origins: https://feathersjs.com, https://feathers.cloud. ' +
524+ 'Use a full origin (scheme + host + port when non-default). ' +
525+ 'Loopback hosts (localhost, 127.0.0.1, ::1, 0.0.0.0) match any port.'
363526 }
364527 )
365528 } )
0 commit comments