@@ -74,6 +74,16 @@ private Request<?> requestWith(String origin, List<String> overrideWhitelist, Bo
7474 return req ;
7575 }
7676
77+ /** An OPTIONS request; a genuine CORS preflight also carries Access-Control-Request-Method. */
78+ private Request <?> optionsRequestWith (String origin , String accessControlRequestMethod ) {
79+ var req = mock (Request .class );
80+ when (req .getPath ()).thenReturn ("/some/path" );
81+ when (req .isOptions ()).thenReturn (true );
82+ when (req .getHeader ("Origin" )).thenReturn (origin );
83+ when (req .getHeader ("Access-Control-Request-Method" )).thenReturn (accessControlRequestMethod );
84+ return req ;
85+ }
86+
7787 @ Test
7888 void noOverride_usesStaticWhitelist () throws Exception {
7989 var vetoer = newVetoer (List .of ("https://allowed.example.com" ));
@@ -160,4 +170,27 @@ void allowMissingOrigin_withOverrideWhitelist() throws Exception {
160170 // non-whitelisted origin still denied
161171 assertFalse (vetoer .isAllowed (requestWith ("https://other.example.com" , List .of ("https://tenant.example.com" ))));
162172 }
173+
174+ @ Test
175+ void corsPreflight_allowedEvenWithNonWhitelistedOrigin () throws Exception {
176+ var vetoer = newVetoer (List .of ("https://allowed.example.com" ), false );
177+
178+ // a preflight carries no credentials, so it can't be a CSRF vector: it is
179+ // let through so the service can answer with the CORS headers, and the
180+ // actual request is vetoed instead
181+ assertTrue (vetoer .isAllowed (optionsRequestWith ("https://not-allowed.example.com" , "POST" )));
182+ }
183+
184+ @ Test
185+ void optionsWithoutPreflightHeaders_isStillChecked () throws Exception {
186+ var vetoer = newVetoer (List .of ("https://allowed.example.com" ), false );
187+
188+ // OPTIONS without Access-Control-Request-Method is not a preflight
189+ assertFalse (vetoer .isAllowed (optionsRequestWith ("https://not-allowed.example.com" , null )));
190+
191+ // OPTIONS without Origin is not a preflight either — it never comes from a
192+ // browser, so it stays subject to allow-missing-origin
193+ assertFalse (vetoer .isAllowed (optionsRequestWith (null , null )));
194+ assertFalse (vetoer .isAllowed (optionsRequestWith (null , "POST" )));
195+ }
163196}
0 commit comments