diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSHandlerTestWildcardOriginCase.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSHandlerTestWildcardOriginCase.java index bea0ee1578301..c1852e10dd84d 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSHandlerTestWildcardOriginCase.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/cors/CORSHandlerTestWildcardOriginCase.java @@ -49,6 +49,60 @@ void corsNotMatchingOrigin() { .header("Access-Control-Allow-Credentials", "false"); } + @Test + void corsSameOriginRequest() { + String origin = "http://localhost:8081"; + given().header("Origin", origin) + .get("/test").then() + .statusCode(200) + .header("Access-Control-Allow-Origin", origin); + } + + @Test + void corsInvalidSameOriginRequest1() { + String origin = "http"; + given().header("Origin", origin) + .get("/test").then() + .statusCode(403) + .header("Access-Control-Allow-Origin", nullValue()); + } + + @Test + void corsInvalidSameOriginRequest2() { + String origin = "http://local"; + given().header("Origin", origin) + .get("/test").then() + .statusCode(403) + .header("Access-Control-Allow-Origin", nullValue()); + } + + @Test + void corsInvalidSameOriginRequest3() { + String origin = "http://localhost"; + given().header("Origin", origin) + .get("/test").then() + .statusCode(403) + .header("Access-Control-Allow-Origin", nullValue()); + } + + @Test + void corsInvalidSameOriginRequest4() { + String origin = "http://localhost:9999"; + given().header("Origin", origin) + .get("/test").then() + .statusCode(403) + .header("Access-Control-Allow-Origin", nullValue()); + } + + @Test + void corsInvalidSameOriginRequest5() { + String origin = "https://localhost:8483"; + given().header("Origin", origin) + .get("/test").then() + .statusCode(403) + .header("Access-Control-Allow-Origin", nullValue()); + } + @Test @DisplayName("Returns false 'Access-Control-Allow-Credentials' header on matching origin '*'") void corsMatchingOriginWithWildcard() { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/cors/CORSFilter.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/cors/CORSFilter.java index c5b476789e188..d1b853176547b 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/cors/CORSFilter.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/cors/CORSFilter.java @@ -1,5 +1,6 @@ package io.quarkus.vertx.http.runtime.cors; +import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -80,7 +81,7 @@ public static List parseAllowedOriginsRegex(Optional> allo * If any regular expression origins are configured, try to match on them. * Regular expressions must begin and end with '/' * - * @param allowedOrigins the configured regex origins. + * @param allowOriginsRegex the configured regex origins. * @param origin the specified origin * @return true if any configured regular expressions match the specified origin, false otherwise */ @@ -179,7 +180,7 @@ public void handle(RoutingContext event) { } boolean allowsOrigin = isConfiguredWithWildcard(corsConfig.origins) || corsConfig.origins.get().contains(origin) - || isOriginAllowedByRegex(allowedOriginsRegex, origin); + || isOriginAllowedByRegex(allowedOriginsRegex, origin) || isSameOrigin(request, origin); if (allowsOrigin) { response.headers().set(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin); @@ -213,4 +214,80 @@ public void handle(RoutingContext event) { } } } + + static boolean isSameOrigin(HttpServerRequest request, String origin) { + //fast path check, when everything is the same + if (origin.startsWith(request.scheme())) { + if (!substringMatch(origin, request.scheme().length(), "://", false)) { + return false; + } + if (substringMatch(origin, request.scheme().length() + 3, request.host(), true)) { + //they are a simple match + return true; + } + return isSameOriginSlowPath(request, origin); + } else { + return false; + } + } + + static boolean isSameOriginSlowPath(HttpServerRequest request, String origin) { + String absUriString = request.absoluteURI(); + //we already know the scheme is correct, as the fast path will reject that + URI baseUri = URI.create(absUriString); + URI originUri = URI.create(origin); + if (!originUri.getPath().isEmpty()) { + //origin should not contain a path component + //just reject it in this case + return false; + } + if (!baseUri.getHost().equals(originUri.getHost())) { + return false; + } + if (baseUri.getPort() == originUri.getPort()) { + return true; + } + if (baseUri.getPort() != -1 && originUri.getPort() != -1) { + //ports are explictly set + return false; + } + if (baseUri.getScheme().equals("http")) { + if (baseUri.getPort() == 80 || baseUri.getPort() == -1) { + if (originUri.getPort() == 80 || originUri.getPort() == -1) { + //port is either unset or 80 + return true; + } + } + } else if (baseUri.getScheme().equals("https")) { + if (baseUri.getPort() == 443 || baseUri.getPort() == -1) { + if (originUri.getPort() == 443 || originUri.getPort() == -1) { + //port is either unset or 443 + return true; + } + } + } + return false; + } + + static boolean substringMatch(String str, int pos, String substring, boolean requireFull) { + int length = str.length(); + int subLength = substring.length(); + int strPos = pos; + int subPos = 0; + if (pos + subLength > length) { + //too long, avoid checking in the loop + return false; + } + for (;;) { + if (subPos == subLength) { + //if we are at the end return the correct value, depending on if we are also at the end of the origin + return !requireFull || strPos == length; + } + if (str.charAt(strPos) != substring.charAt(subPos)) { + return false; + } + strPos++; + subPos++; + } + } } diff --git a/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/cors/CORSFilterTest.java b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/cors/CORSFilterTest.java index 5686242b7eaef..e2348e9efcf7b 100644 --- a/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/cors/CORSFilterTest.java +++ b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/cors/CORSFilterTest.java @@ -2,7 +2,9 @@ import static io.quarkus.vertx.http.runtime.cors.CORSFilter.isConfiguredWithWildcard; import static io.quarkus.vertx.http.runtime.cors.CORSFilter.isOriginAllowedByRegex; +import static io.quarkus.vertx.http.runtime.cors.CORSFilter.isSameOrigin; import static io.quarkus.vertx.http.runtime.cors.CORSFilter.parseAllowedOriginsRegex; +import static io.quarkus.vertx.http.runtime.cors.CORSFilter.substringMatch; import java.util.Arrays; import java.util.Collections; @@ -12,6 +14,9 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import io.vertx.core.http.HttpServerRequest; public class CORSFilterTest { @@ -37,4 +42,46 @@ public void isOriginAllowedByRegexTest() { Assertions.assertEquals(regexList.size(), 1); Assertions.assertTrue(isOriginAllowedByRegex(regexList, "https://abc-123.app.mydomain.com")); } + + @Test + public void sameOriginTest() { + var request = Mockito.mock(HttpServerRequest.class); + Mockito.when(request.scheme()).thenReturn("http"); + Mockito.when(request.host()).thenReturn("localhost"); + Mockito.when(request.absoluteURI()).thenReturn("http://localhost"); + Assertions.assertTrue(isSameOrigin(request, "http://localhost")); + Assertions.assertTrue(isSameOrigin(request, "http://localhost:80")); + Assertions.assertFalse(isSameOrigin(request, "http://localhost:8080")); + Assertions.assertFalse(isSameOrigin(request, "https://localhost")); + Mockito.when(request.host()).thenReturn("localhost:8080"); + Mockito.when(request.absoluteURI()).thenReturn("http://localhost:8080"); + Assertions.assertFalse(isSameOrigin(request, "http://localhost")); + Assertions.assertFalse(isSameOrigin(request, "http://localhost:80")); + Assertions.assertTrue(isSameOrigin(request, "http://localhost:8080")); + Assertions.assertFalse(isSameOrigin(request, "https://localhost:8080")); + Mockito.when(request.scheme()).thenReturn("https"); + Mockito.when(request.host()).thenReturn("localhost"); + Mockito.when(request.absoluteURI()).thenReturn("http://localhost"); + Assertions.assertFalse(isSameOrigin(request, "http://localhost")); + Assertions.assertFalse(isSameOrigin(request, "http://localhost:443")); + Assertions.assertFalse(isSameOrigin(request, "https://localhost:8080")); + Assertions.assertTrue(isSameOrigin(request, "https://localhost")); + Mockito.when(request.host()).thenReturn("localhost:8443"); + Mockito.when(request.absoluteURI()).thenReturn("https://localhost:8443"); + Assertions.assertFalse(isSameOrigin(request, "http://localhost")); + Assertions.assertFalse(isSameOrigin(request, "http://localhost:80")); + Assertions.assertFalse(isSameOrigin(request, "http://localhost:8443")); + Assertions.assertTrue(isSameOrigin(request, "https://localhost:8443")); + + } + + @Test + public void testSubstringMatches() { + Assertions.assertTrue(substringMatch("localhost", 0, "local", false)); + Assertions.assertFalse(substringMatch("localhost", 0, "local", true)); + Assertions.assertFalse(substringMatch("localhost", 1, "local", false)); + Assertions.assertTrue(substringMatch("localhost", 5, "host", false)); + Assertions.assertTrue(substringMatch("localhost", 5, "host", true)); + + } }