4242import org .springframework .boot .test .web .server .LocalServerPort ;
4343import org .springframework .boot .webmvc .test .autoconfigure .AutoConfigureMockMvc ;
4444import org .springframework .test .web .servlet .MockMvc ;
45+ import org .springframework .test .web .servlet .request .RequestPostProcessor ;
4546
4647/**
4748 * Tests for the browser origins allowed to call the REST API and open the {@code /run_live}
@@ -61,17 +62,21 @@ class DefaultOrigins {
6162 @ ValueSource (strings = {REMOTE_ORIGIN , "http://localhost:4200" })
6263 public void restApi_fromOtherOrigin_isRejected (String origin , @ Autowired MockMvc mockMvc )
6364 throws Exception {
65+ // The DNS-rebinding guard does not apply to requests over the network, so CORS decides.
6466 mockMvc
65- .perform (get ("/list-apps" ).header (ORIGIN , origin ))
67+ .perform (get ("/list-apps" ).header (ORIGIN , origin ). with ( overNetwork ()) )
6668 .andExpect (status ().isForbidden ())
6769 .andExpect (header ().doesNotExist (ACCESS_CONTROL_ALLOW_ORIGIN ));
6870 }
6971
7072 @ Test
7173 public void restApi_fromSameOrigin_isAllowed (@ Autowired MockMvc mockMvc ) throws Exception {
72- // Same-origin requests skip the origin list, so the bundled UI works from any host name .
74+ // Same-origin requests skip the origin list, so any host name on the network works .
7375 mockMvc
74- .perform (get ("http://devbox:8000/list-apps" ).header (ORIGIN , "http://devbox:8000" ))
76+ .perform (
77+ get ("http://devbox:8000/list-apps" )
78+ .header (ORIGIN , "http://devbox:8000" )
79+ .with (overNetwork ()))
7580 .andExpect (status ().isOk ());
7681 }
7782
@@ -82,7 +87,8 @@ public void restApi_behindTlsProxy_isAllowed(@Autowired MockMvc mockMvc) throws
8287 .perform (
8388 get ("http://app.example.com/list-apps" )
8489 .header ("X-Forwarded-Proto" , "https" )
85- .header (ORIGIN , "https://app.example.com" ))
90+ .header (ORIGIN , "https://app.example.com" )
91+ .with (overNetwork ()))
8692 .andExpect (status ().isOk ());
8793 }
8894
@@ -93,7 +99,8 @@ public void restApi_behindTlsProxy_fromRemoteOrigin_isRejected(@Autowired MockMv
9399 .perform (
94100 get ("http://app.example.com/list-apps" )
95101 .header ("X-Forwarded-Proto" , "https" )
96- .header (ORIGIN , REMOTE_ORIGIN ))
102+ .header (ORIGIN , REMOTE_ORIGIN )
103+ .with (overNetwork ()))
97104 .andExpect (status ().isForbidden ());
98105 }
99106
@@ -105,7 +112,8 @@ public void restApi_behindTlsProxy_fromPlainHttpOrigin_isRejected(@Autowired Moc
105112 .perform (
106113 get ("http://app.example.com/list-apps" )
107114 .header ("X-Forwarded-Proto" , "https" )
108- .header (ORIGIN , "http://app.example.com" ))
115+ .header (ORIGIN , "http://app.example.com" )
116+ .with (overNetwork ()))
109117 .andExpect (status ().isForbidden ());
110118 }
111119
@@ -115,7 +123,8 @@ public void preflight_fromRemoteOrigin_isRejected(@Autowired MockMvc mockMvc) th
115123 .perform (
116124 options ("/apps/test-app/users/test-user/sessions/test-session" )
117125 .header (ORIGIN , REMOTE_ORIGIN )
118- .header (ACCESS_CONTROL_REQUEST_METHOD , "DELETE" ))
126+ .header (ACCESS_CONTROL_REQUEST_METHOD , "DELETE" )
127+ .with (overNetwork ()))
119128 .andExpect (status ().isForbidden ())
120129 .andExpect (header ().doesNotExist (ACCESS_CONTROL_ALLOW_ORIGIN ));
121130 }
@@ -133,12 +142,13 @@ public void runLive_fromOtherOrigin_isRejected(String origin, @LocalServerPort i
133142
134143 @ Test
135144 public void runLive_behindTlsProxy_completesHandshake (@ LocalServerPort int port ) {
145+ // A same-machine TLS proxy at https://127.0.0.1; only forwarded headers make this match.
136146 WebSocket .Builder handshake =
137147 httpClient
138148 .newWebSocketBuilder ()
139- .header (ORIGIN , "https://app.example.com " )
149+ .header (ORIGIN , "https://127.0.0.1 " )
140150 .header ("X-Forwarded-Proto" , "https" )
141- .header ("X-Forwarded-Host" , "app.example.com " );
151+ .header ("X-Forwarded-Host" , "127.0.0.1 " );
142152
143153 connect (port , handshake ).abort ();
144154 }
@@ -153,6 +163,20 @@ public void runLive_behindTlsProxy_fromPlainHttpOrigin_isRejected(@LocalServerPo
153163
154164 assertHandshakeForbidden (port , handshake );
155165 }
166+
167+ @ Test
168+ public void runLive_reboundOriginBehindHostRewritingProxy_isRejected (
169+ @ LocalServerPort int port ) {
170+ // A rebound page reaching a same-machine proxy that forwards the page's own host.
171+ WebSocket .Builder handshake =
172+ httpClient
173+ .newWebSocketBuilder ()
174+ .header (ORIGIN , "https://evil.example" )
175+ .header ("X-Forwarded-Proto" , "https" )
176+ .header ("X-Forwarded-Host" , "evil.example" );
177+
178+ assertHandshakeForbidden (port , handshake );
179+ }
156180 }
157181
158182 @ Nested
@@ -166,12 +190,13 @@ public void restApi_behindTlsProxy_isRejected(@Autowired MockMvc mockMvc) throws
166190 .perform (
167191 get ("http://app.example.com/list-apps" )
168192 .header ("X-Forwarded-Proto" , "https" )
169- .header (ORIGIN , "https://app.example.com" ))
193+ .header (ORIGIN , "https://app.example.com" )
194+ .with (overNetwork ()))
170195 .andExpect (status ().isForbidden ());
171196 }
172197 }
173198
174- /** With the CORS filter mapped away from /run_live, only the handshake's origin check runs . */
199+ /** With the CORS filter mapped away from /run_live, the handshake's own origin check decides . */
175200 @ Nested
176201 @ SpringBootTest (
177202 webEnvironment = WebEnvironment .RANDOM_PORT ,
@@ -184,8 +209,9 @@ public void runLive_fromSameOrigin_completesHandshake(@LocalServerPort int port)
184209 }
185210
186211 @ Test
187- public void runLive_fromRemoteOrigin_isRejected (@ LocalServerPort int port ) {
188- assertHandshakeForbidden (port , REMOTE_ORIGIN );
212+ public void runLive_fromOtherLocalPort_isRejected (@ LocalServerPort int port ) {
213+ // A loopback origin passes the DNS-rebinding guard, so only the handshake check refuses it.
214+ assertHandshakeForbidden (port , "http://localhost:4200" );
189215 }
190216 }
191217
@@ -211,7 +237,8 @@ public void restApi_behindTlsProxy_isAllowed(@Autowired MockMvc mockMvc) throws
211237 .perform (
212238 get ("http://app.example.com/list-apps" )
213239 .header ("X-Forwarded-Proto" , "https" )
214- .header (ORIGIN , "https://app.example.com" ))
240+ .header (ORIGIN , "https://app.example.com" )
241+ .with (overNetwork ()))
215242 .andExpect (status ().isOk ());
216243 }
217244
@@ -222,7 +249,8 @@ public void runLive_fromConfiguredOrigin_completesHandshake(@LocalServerPort int
222249
223250 @ Test
224251 public void runLive_fromUnlistedOrigin_isRejected (@ LocalServerPort int port ) {
225- assertHandshakeForbidden (port , REMOTE_ORIGIN );
252+ // A loopback origin passes the DNS-rebinding guard, so the configured list must refuse it.
253+ assertHandshakeForbidden (port , "http://localhost:4200" );
226254 }
227255 }
228256
@@ -247,6 +275,14 @@ public void runLive_fromRemoteOrigin_completesHandshake(@LocalServerPort int por
247275 }
248276 }
249277
278+ /** Delivers a MockMvc request on a routable address, as a LAN host or remote proxy does. */
279+ private static RequestPostProcessor overNetwork () {
280+ return request -> {
281+ request .setLocalAddr ("10.0.0.5" );
282+ return request ;
283+ };
284+ }
285+
250286 private static WebSocket openRunLive (int port , String origin ) {
251287 return connect (port , httpClient .newWebSocketBuilder ().header (ORIGIN , origin ));
252288 }
0 commit comments