@@ -2,6 +2,151 @@ import { strict as assert } from 'assert'
22import { expressFixture , TestOAuthStrategy } from './utils/fixture'
33import { AuthenticationService } from '@feathersjs/authentication'
44
5+ describe ( '@feathersjs/authentication-oauth/strategy security' , ( ) => {
6+ let app : Awaited < ReturnType < typeof expressFixture > >
7+ let authService : AuthenticationService
8+ let strategy : TestOAuthStrategy
9+
10+ before ( async ( ) => {
11+ app = await expressFixture ( 9779 , 5116 )
12+ authService = app . service ( 'authentication' )
13+ strategy = authService . getStrategy ( 'github' ) as TestOAuthStrategy
14+ } )
15+
16+ after ( async ( ) => {
17+ await app . teardown ( )
18+ } )
19+
20+ describe ( 'open redirect via URL authority injection' , ( ) => {
21+ beforeEach ( ( ) => {
22+ app . get ( 'authentication' ) . oauth . origins = [ 'https://target.com' ]
23+ } )
24+
25+ afterEach ( ( ) => {
26+ delete app . get ( 'authentication' ) . oauth . origins
27+ } )
28+
29+ it ( 'should reject redirect parameter containing @ character' , async ( ) => {
30+ // Attack: ?redirect=@attacker.com would result in https://target.com@attacker.com
31+ // which browsers parse as username "target.com" and host "attacker.com"
32+ await assert . rejects (
33+ ( ) =>
34+ strategy . getRedirect (
35+ { accessToken : 'testing' } ,
36+ {
37+ redirect : '@attacker.com' ,
38+ headers : {
39+ referer : 'https://target.com/login'
40+ }
41+ }
42+ ) ,
43+ {
44+ name : 'NotAuthenticated'
45+ }
46+ )
47+ } )
48+
49+ it ( 'should reject redirect parameter containing // for protocol-relative URLs' , async ( ) => {
50+ // Attack: ?redirect=//attacker.com would result in https://target.com//attacker.com
51+ // which some parsers might interpret as protocol-relative URL
52+ await assert . rejects (
53+ ( ) =>
54+ strategy . getRedirect (
55+ { accessToken : 'testing' } ,
56+ {
57+ redirect : '//attacker.com' ,
58+ headers : {
59+ referer : 'https://target.com/login'
60+ }
61+ }
62+ ) ,
63+ {
64+ name : 'NotAuthenticated'
65+ }
66+ )
67+ } )
68+
69+ it ( 'should reject redirect with backslash characters' , async ( ) => {
70+ // Some browsers treat backslash as forward slash
71+ await assert . rejects (
72+ ( ) =>
73+ strategy . getRedirect (
74+ { accessToken : 'testing' } ,
75+ {
76+ redirect : '\\\\attacker.com' ,
77+ headers : {
78+ referer : 'https://target.com/login'
79+ }
80+ }
81+ ) ,
82+ {
83+ name : 'NotAuthenticated'
84+ }
85+ )
86+ } )
87+ } )
88+
89+ describe ( 'origin validation bypass via startsWith' , ( ) => {
90+ beforeEach ( ( ) => {
91+ app . get ( 'authentication' ) . oauth . origins = [ 'https://target.com' ]
92+ } )
93+
94+ afterEach ( ( ) => {
95+ delete app . get ( 'authentication' ) . oauth . origins
96+ } )
97+
98+ it ( 'should reject referer from domain that shares prefix with allowed origin' , async ( ) => {
99+ // Attack: attacker registers target.com.attacker.com
100+ // startsWith('https://target.com') would incorrectly return true
101+ await assert . rejects (
102+ ( ) =>
103+ strategy . getRedirect (
104+ { accessToken : 'testing' } ,
105+ {
106+ headers : {
107+ referer : 'https://target.com.attacker.com/login'
108+ }
109+ }
110+ ) ,
111+ {
112+ message : 'Referer "https://target.com.attacker.com/login" is not allowed.'
113+ }
114+ )
115+ } )
116+
117+ it ( 'should reject referer with extra subdomain-like prefix' , async ( ) => {
118+ // Another variant: target.com-evil.attacker.com
119+ await assert . rejects (
120+ ( ) =>
121+ strategy . getRedirect (
122+ { accessToken : 'testing' } ,
123+ {
124+ headers : {
125+ referer : 'https://target.com-evil.attacker.com/login'
126+ }
127+ }
128+ ) ,
129+ {
130+ message : 'Referer "https://target.com-evil.attacker.com/login" is not allowed.'
131+ }
132+ )
133+ } )
134+
135+ it ( 'should accept exact origin match with path' , async ( ) => {
136+ // Legitimate use case should still work
137+ const redirect = await strategy . getRedirect (
138+ { accessToken : 'testing' } ,
139+ {
140+ headers : {
141+ referer : 'https://target.com/some/path'
142+ }
143+ }
144+ )
145+ assert . equal ( redirect , 'https://target.com#access_token=testing' )
146+ } )
147+ } )
148+ } )
149+
5150describe ( '@feathersjs/authentication-oauth/strategy' , ( ) => {
6151 let app : Awaited < ReturnType < typeof expressFixture > >
7152 let authService : AuthenticationService
0 commit comments