Skip to content

Commit bd2ff85

Browse files
committed
fix(security): originVetoer skips only genuine CORS preflights, not all
OPTIONS
1 parent eb66ceb commit bd2ff85

2 files changed

Lines changed: 47 additions & 0 deletions

File tree

security/src/main/java/org/restheart/security/authorizers/OriginVetoer.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,20 @@ public void init() {
116116

117117
@Override
118118
public boolean isAllowed(final Request<?> request) {
119+
// Allow genuine CORS preflight through — the service handles CORS headers.
120+
// A preflight is issued by the browser and carries no credentials, so it
121+
// can't be a CSRF vector; the actual request (POST, GET, etc.) is checked
122+
// instead. Vetoing it would be pointless anyway: browsers fail a preflight
123+
// whose status is not 2xx, no matter which CORS headers it carries.
124+
// Plain OPTIONS requests (no Origin, no Access-Control-Request-Method) are
125+
// not preflights and stay subject to the check.
126+
if (request.isOptions()
127+
&& request.getHeader("Origin") != null
128+
&& request.getHeader("Access-Control-Request-Method") != null) {
129+
LOGGER.debug("originVetoer: CORS preflight accepted without checking the Origin header");
130+
return true;
131+
}
132+
119133
if (ignoreLists != null && ignoreLists.match(request.getPath()) != null) {
120134
LOGGER.debug("originVetoer: request is accepted since path is in ignore list");
121135
return true;

security/src/test/java/org/restheart/security/authorizers/OriginVetoerTest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)