@@ -19,40 +19,186 @@ describe('node', () => {
1919 describe ( 'createRequest' , ( ) => {
2020 describe ( 'x-forwarded-for' , ( ) => {
2121 it ( 'parses client IP from single-value x-forwarded-for header' , ( ) => {
22- const result = createRequest ( {
23- ...mockNodeRequest ,
24- headers : {
25- 'x-forwarded-for' : '1.1.1.1' ,
22+ const result = createRequest (
23+ {
24+ ...mockNodeRequest ,
25+ headers : {
26+ host : 'example.com' ,
27+ 'x-forwarded-for' : '1.1.1.1' ,
28+ } ,
2629 } ,
27- } ) ;
30+ { allowedDomains : [ { hostname : 'example.com' } ] } ,
31+ ) ;
2832 assert . equal ( result [ Symbol . for ( 'astro.clientAddress' ) ] , '1.1.1.1' ) ;
2933 } ) ;
3034
3135 it ( 'parses client IP from multi-value x-forwarded-for header' , ( ) => {
32- const result = createRequest ( {
33- ...mockNodeRequest ,
34- headers : {
35- 'x-forwarded-for' : '1.1.1.1,8.8.8.8' ,
36+ const result = createRequest (
37+ {
38+ ...mockNodeRequest ,
39+ headers : {
40+ host : 'example.com' ,
41+ 'x-forwarded-for' : '1.1.1.1,8.8.8.8' ,
42+ } ,
3643 } ,
37- } ) ;
44+ { allowedDomains : [ { hostname : 'example.com' } ] } ,
45+ ) ;
3846 assert . equal ( result [ Symbol . for ( 'astro.clientAddress' ) ] , '1.1.1.1' ) ;
3947 } ) ;
4048
4149 it ( 'parses client IP from multi-value x-forwarded-for header with spaces' , ( ) => {
50+ const result = createRequest (
51+ {
52+ ...mockNodeRequest ,
53+ headers : {
54+ host : 'example.com' ,
55+ 'x-forwarded-for' : ' 1.1.1.1, 8.8.8.8, 8.8.8.2' ,
56+ } ,
57+ } ,
58+ { allowedDomains : [ { hostname : 'example.com' } ] } ,
59+ ) ;
60+ assert . equal ( result [ Symbol . for ( 'astro.clientAddress' ) ] , '1.1.1.1' ) ;
61+ } ) ;
62+
63+ it ( 'fallbacks to remoteAddress when no x-forwarded-for header is present' , ( ) => {
64+ const result = createRequest (
65+ {
66+ ...mockNodeRequest ,
67+ headers : {
68+ host : 'example.com' ,
69+ } ,
70+ } ,
71+ { allowedDomains : [ { hostname : 'example.com' } ] } ,
72+ ) ;
73+ assert . equal ( result [ Symbol . for ( 'astro.clientAddress' ) ] , '2.2.2.2' ) ;
74+ } ) ;
75+
76+ it ( 'ignores x-forwarded-for when no allowedDomains is configured (default)' , ( ) => {
4277 const result = createRequest ( {
4378 ...mockNodeRequest ,
4479 headers : {
45- 'x-forwarded-for' : ' 1.1.1.1, 8.8.8.8, 8.8.8.2' ,
80+ host : 'example.com' ,
81+ 'x-forwarded-for' : '1.1.1.1' ,
4682 } ,
4783 } ) ;
84+ // Without allowedDomains, x-forwarded-for should NOT be trusted
85+ // Falls back to socket remoteAddress
86+ assert . equal ( result [ Symbol . for ( 'astro.clientAddress' ) ] , '2.2.2.2' ) ;
87+ } ) ;
88+
89+ it ( 'ignores x-forwarded-for when allowedDomains is empty' , ( ) => {
90+ const result = createRequest (
91+ {
92+ ...mockNodeRequest ,
93+ headers : {
94+ host : 'example.com' ,
95+ 'x-forwarded-for' : '1.1.1.1' ,
96+ } ,
97+ } ,
98+ { allowedDomains : [ ] } ,
99+ ) ;
100+ // Empty allowedDomains means no proxy trust, use socket address
101+ assert . equal ( result [ Symbol . for ( 'astro.clientAddress' ) ] , '2.2.2.2' ) ;
102+ } ) ;
103+
104+ it ( 'trusts x-forwarded-for when host matches allowedDomains' , ( ) => {
105+ const result = createRequest (
106+ {
107+ ...mockNodeRequest ,
108+ headers : {
109+ host : 'example.com' ,
110+ 'x-forwarded-for' : '1.1.1.1' ,
111+ } ,
112+ } ,
113+ { allowedDomains : [ { hostname : 'example.com' } ] } ,
114+ ) ;
115+ // Host matches allowedDomains, so x-forwarded-for is trusted
48116 assert . equal ( result [ Symbol . for ( 'astro.clientAddress' ) ] , '1.1.1.1' ) ;
49117 } ) ;
50118
51- it ( 'fallbacks to remoteAddress when no x-forwarded-for header is present' , ( ) => {
119+ it ( 'ignores x-forwarded-for when host does not match allowedDomains' , ( ) => {
120+ const result = createRequest (
121+ {
122+ ...mockNodeRequest ,
123+ headers : {
124+ host : 'attacker.com' ,
125+ 'x-forwarded-for' : '1.1.1.1' ,
126+ } ,
127+ } ,
128+ { allowedDomains : [ { hostname : 'example.com' } ] } ,
129+ ) ;
130+ // Host does not match allowedDomains, so x-forwarded-for is NOT trusted
131+ assert . equal ( result [ Symbol . for ( 'astro.clientAddress' ) ] , '2.2.2.2' ) ;
132+ } ) ;
133+
134+ it ( 'trusts x-forwarded-for when x-forwarded-host matches allowedDomains' , ( ) => {
135+ const result = createRequest (
136+ {
137+ ...mockNodeRequest ,
138+ headers : {
139+ 'x-forwarded-host' : 'example.com' ,
140+ 'x-forwarded-for' : '1.1.1.1' ,
141+ } ,
142+ } ,
143+ { allowedDomains : [ { hostname : 'example.com' } ] } ,
144+ ) ;
145+ // X-Forwarded-Host validated against allowedDomains, so XFF is trusted
146+ assert . equal ( result [ Symbol . for ( 'astro.clientAddress' ) ] , '1.1.1.1' ) ;
147+ } ) ;
148+
149+ it ( 'trusts multi-value x-forwarded-for when host matches allowedDomains' , ( ) => {
150+ const result = createRequest (
151+ {
152+ ...mockNodeRequest ,
153+ headers : {
154+ host : 'example.com' ,
155+ 'x-forwarded-for' : '1.1.1.1, 8.8.8.8' ,
156+ } ,
157+ } ,
158+ { allowedDomains : [ { hostname : 'example.com' } ] } ,
159+ ) ;
160+ assert . equal ( result [ Symbol . for ( 'astro.clientAddress' ) ] , '1.1.1.1' ) ;
161+ } ) ;
162+
163+ it ( 'falls back to remoteAddress when host matches allowedDomains but no x-forwarded-for' , ( ) => {
164+ const result = createRequest (
165+ {
166+ ...mockNodeRequest ,
167+ headers : {
168+ host : 'example.com' ,
169+ } ,
170+ } ,
171+ { allowedDomains : [ { hostname : 'example.com' } ] } ,
172+ ) ;
173+ assert . equal ( result [ Symbol . for ( 'astro.clientAddress' ) ] , '2.2.2.2' ) ;
174+ } ) ;
175+
176+ it ( 'prevents IP spoofing: attacker cannot override clientAddress without allowedDomains' , ( ) => {
177+ // Simulates an attacker injecting x-forwarded-for to spoof 127.0.0.1
52178 const result = createRequest ( {
53179 ...mockNodeRequest ,
54- headers : { } ,
180+ headers : {
181+ host : 'example.com' ,
182+ 'x-forwarded-for' : '127.0.0.1' ,
183+ } ,
55184 } ) ;
185+ // Without allowedDomains, the spoofed IP must be ignored
186+ assert . equal ( result [ Symbol . for ( 'astro.clientAddress' ) ] , '2.2.2.2' ) ;
187+ } ) ;
188+
189+ it ( 'prevents IP spoofing: attacker cannot override clientAddress when host does not match' , ( ) => {
190+ // Simulates attacker sending direct request with XFF and mismatched host
191+ const result = createRequest (
192+ {
193+ ...mockNodeRequest ,
194+ headers : {
195+ host : 'evil.com' ,
196+ 'x-forwarded-for' : '127.0.0.1' ,
197+ } ,
198+ } ,
199+ { allowedDomains : [ { hostname : 'example.com' } ] } ,
200+ ) ;
201+ // Host doesn't match allowedDomains, so XFF is not trusted
56202 assert . equal ( result [ Symbol . for ( 'astro.clientAddress' ) ] , '2.2.2.2' ) ;
57203 } ) ;
58204 } ) ;
0 commit comments