diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index df01d12bb6..fa0055ad7e 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -8,6 +8,12 @@
+
+ XMLHttpRequest: the CORS preflight response is checked like in real browsers; requests with
+ credentials need the exact origin and Access-Control-Allow-Credentials, the wildcard '*' is not
+ supported for them and methods other than GET, HEAD and POST have to be listed
+ in Access-Control-Allow-Methods.
+
Content-Security-Policy frame-ancestors: no NullPointerException if the url of the parent frame
is not a network url (e.g. data:); about:blank frames inherit the origin of their parent.
diff --git a/src/main/java/org/htmlunit/HttpHeader.java b/src/main/java/org/htmlunit/HttpHeader.java
index bb2e388324..c297af7f37 100644
--- a/src/main/java/org/htmlunit/HttpHeader.java
+++ b/src/main/java/org/htmlunit/HttpHeader.java
@@ -142,6 +142,8 @@ public final class HttpHeader {
public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
/** Access-Control-Allow-Headers. */
public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
+ /** Access-Control-Allow-Methods. */
+ public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
/** Ping-From. */
public static final String PING_FROM = "Ping-From";
diff --git a/src/main/java/org/htmlunit/javascript/host/xml/XMLHttpRequest.java b/src/main/java/org/htmlunit/javascript/host/xml/XMLHttpRequest.java
index 040a8f5115..d1735a3736 100644
--- a/src/main/java/org/htmlunit/javascript/host/xml/XMLHttpRequest.java
+++ b/src/main/java/org/htmlunit/javascript/host/xml/XMLHttpRequest.java
@@ -117,6 +117,7 @@
* @author Lai Quang Duong
* @author Sven Strickroth
* @author Ashutosh Panda
+ * @author Vidya m
*
* @see MDN Documentation
*/
@@ -1395,10 +1396,19 @@ private static boolean isPreflight(final WebRequest request) {
return false;
}
- private static boolean isPreflightAuthorized(final WebRequest request, final WebResponse preflightResponse) {
- final String originHeader = preflightResponse.getResponseHeaderValue(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN);
- if (!ALLOW_ORIGIN_ALL.equals(originHeader)
- && !request.getAdditionalHeaders().get(HttpHeader.ORIGIN).equals(originHeader)) {
+ private boolean isPreflightAuthorized(final WebRequest request, final WebResponse preflightResponse) {
+ // the preflight response has to pass the CORS check too; for a request with credentials
+ // this requires the exact origin and Access-Control-Allow-Credentials
+ if (!isCorsAllowed(preflightResponse, request.getAdditionalHeaders().get(HttpHeader.ORIGIN))) {
+ return false;
+ }
+
+ // the wildcard is not supported for requests with credentials
+ final boolean wildcard = !isWithCredentials();
+
+ final HttpMethod method = request.getHttpMethod();
+ if (method != HttpMethod.GET && method != HttpMethod.HEAD && method != HttpMethod.POST
+ && !isPreflightMethodAllowed(method.name(), preflightResponse, wildcard)) {
return false;
}
@@ -1409,7 +1419,7 @@ private static boolean isPreflightAuthorized(final WebRequest request, final Web
if (HttpHeader.ACCESS_CONTROL_ALLOW_HEADERS.equalsIgnoreCase(pair.getName())) {
String value = pair.getValue();
if (value != null) {
- if (ALLOW_ORIGIN_ALL.equals(value)) {
+ if (wildcard && ALLOW_ORIGIN_ALL.equals(value)) {
// all headers are allowed
return true;
}
@@ -1435,6 +1445,31 @@ private static boolean isPreflightAuthorized(final WebRequest request, final Web
return true;
}
+ /**
+ * Checks whether the given method is listed in the Access-Control-Allow-Methods header
+ * of the preflight response (the method names are case-sensitive).
+ *
+ * @param method the method name
+ * @param preflightResponse the preflight response
+ * @param wildcard whether the wildcard '*' allows all methods
+ * @return {@code true} if the method is allowed
+ */
+ private static boolean isPreflightMethodAllowed(final String method, final WebResponse preflightResponse,
+ final boolean wildcard) {
+ for (final NameValuePair pair : preflightResponse.getResponseHeaders()) {
+ if (HttpHeader.ACCESS_CONTROL_ALLOW_METHODS.equalsIgnoreCase(pair.getName())
+ && pair.getValue() != null) {
+ for (final String part : StringUtils.splitAtComma(pair.getValue())) {
+ final String allowed = part.trim();
+ if (method.equals(allowed) || (wildcard && ALLOW_ORIGIN_ALL.equals(allowed))) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
/**
* Checks whether the given header name requires a preflight request.
*
diff --git a/src/test/java/org/htmlunit/javascript/host/xml/XMLHttpRequestCORSTest.java b/src/test/java/org/htmlunit/javascript/host/xml/XMLHttpRequestCORSTest.java
index 051fa23619..49c6968a31 100644
--- a/src/test/java/org/htmlunit/javascript/host/xml/XMLHttpRequestCORSTest.java
+++ b/src/test/java/org/htmlunit/javascript/host/xml/XMLHttpRequestCORSTest.java
@@ -49,6 +49,7 @@
* @author Ronald Brill
* @author Frank Danek
* @author Lai Quang Duong
+ * @author Vidya m
*/
public class XMLHttpRequestCORSTest extends WebDriverTestCase {
@@ -721,6 +722,201 @@ public void preflight_wildcard_allow_headers() throws Exception {
verifyTitle2(getWebDriver(), getExpectedAlerts());
}
+ /**
+ * @throws Exception if the test fails.
+ */
+ @Test
+ @Alerts({"4", "200", "PUT null"})
+ public void preflight_put() throws Exception {
+ doPreflightTestReceived(false, "PUT", "http://localhost:" + PORT, null, "PUT", "X-PING");
+ }
+
+ /**
+ * A method that is not CORS-safelisted has to be listed in Access-Control-Allow-Methods.
+ * @throws Exception if the test fails.
+ */
+ @Test
+ @Alerts({"NetworkError/DOMException", "4", "0", "nothing"})
+ public void preflight_put_methodNotAllowed() throws Exception {
+ doPreflightTestReceived(false, "PUT", "http://localhost:" + PORT, null, "POST, GET, OPTIONS", "X-PING");
+ }
+
+ /**
+ * @throws Exception if the test fails.
+ */
+ @Test
+ @Alerts({"4", "200", "PUT null"})
+ public void preflight_put_wildcardMethods() throws Exception {
+ doPreflightTestReceived(false, "PUT", "http://localhost:" + PORT, null, "*", "X-PING");
+ }
+
+ /**
+ * @throws Exception if the test fails.
+ */
+ @Test
+ @Alerts({"4", "200", "PUT cookie=sweet"})
+ public void preflight_put_withCredentials() throws Exception {
+ doPreflightTestReceived(true, "PUT", "http://localhost:" + PORT, "true", "PUT", "X-PING");
+ }
+
+ /**
+ * The wildcard origin is not allowed for requests with credentials, the request must not be sent.
+ * @throws Exception if the test fails.
+ */
+ @Test
+ @Alerts({"NetworkError/DOMException", "4", "0", "nothing"})
+ public void preflight_put_withCredentials_wildcardOrigin() throws Exception {
+ doPreflightTestReceived(true, "PUT", "*", "true", "PUT", "X-PING");
+ }
+
+ /**
+ * A request with credentials needs Access-Control-Allow-Credentials in the preflight response.
+ * @throws Exception if the test fails.
+ */
+ @Test
+ @Alerts({"NetworkError/DOMException", "4", "0", "nothing"})
+ public void preflight_put_withCredentials_noAllowCredentials() throws Exception {
+ doPreflightTestReceived(true, "PUT", "http://localhost:" + PORT, null, "PUT", "X-PING");
+ }
+
+ /**
+ * The wildcard methods are not allowed for requests with credentials.
+ * @throws Exception if the test fails.
+ */
+ @Test
+ @Alerts({"NetworkError/DOMException", "4", "0", "nothing"})
+ public void preflight_put_withCredentials_wildcardMethods() throws Exception {
+ doPreflightTestReceived(true, "PUT", "http://localhost:" + PORT, "true", "*", "X-PING");
+ }
+
+ /**
+ * The wildcard headers are not allowed for requests with credentials.
+ * @throws Exception if the test fails.
+ */
+ @Test
+ @Alerts({"NetworkError/DOMException", "4", "0", "nothing"})
+ public void preflight_withCredentials_wildcardHeaders() throws Exception {
+ doPreflightTestReceived(true, "GET", "http://localhost:" + PORT, "true", null, "*");
+ }
+
+ /**
+ * Sends a request that needs a preflight and logs afterwards the request that
+ * reached the server ('nothing' if the request was not sent).
+ */
+ private void doPreflightTestReceived(final boolean withCredentials, final String method,
+ final String allowOrigin, final String allowCredentials,
+ final String allowMethods, final String allowHeaders) throws Exception {
+ final String html = DOCTYPE_HTML
+ + "\n"
+ + "\n"
+ + "\n"
+ + "";
+
+ PreflightReceivedServlet.ACCESS_CONTROL_ALLOW_ORIGIN_ = allowOrigin;
+ PreflightReceivedServlet.ACCESS_CONTROL_ALLOW_CREDENTIALS_ = allowCredentials;
+ PreflightReceivedServlet.ACCESS_CONTROL_ALLOW_METHODS_ = allowMethods;
+ PreflightReceivedServlet.ACCESS_CONTROL_ALLOW_HEADERS_ = allowHeaders;
+ PreflightReceivedServlet.RECEIVED_ = null;
+
+ final Map> servlets2 = new HashMap<>();
+ servlets2.put("/preflight2", PreflightReceivedServlet.class);
+ startWebServer2(".", servlets2);
+
+ final List responseHeader = new ArrayList<>();
+ responseHeader.add(new NameValuePair("Set-Cookie", "cookie=sweet"));
+
+ final URL url = new URL(URL_FIRST, "/preflight1");
+ getMockWebConnection().setResponse(url, html,
+ 200, "OK", "text/html;charset=ISO-8859-1", ISO_8859_1, responseHeader);
+
+ loadPage2(url, null);
+ verifyTitle2(getWebDriver(), getExpectedAlerts());
+ }
+
+ /**
+ * Preflight CORS scenario Servlet; remembers the method and the cookie of the request
+ * that was sent after the preflight and answers the 'received' GET request with them.
+ */
+ public static class PreflightReceivedServlet extends HttpServlet {
+ private static String ACCESS_CONTROL_ALLOW_ORIGIN_;
+ private static String ACCESS_CONTROL_ALLOW_CREDENTIALS_;
+ private static String ACCESS_CONTROL_ALLOW_METHODS_;
+ private static String ACCESS_CONTROL_ALLOW_HEADERS_;
+ private static String RECEIVED_;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void doOptions(final HttpServletRequest request, final HttpServletResponse response) {
+ setCorsHeaders(response);
+ if (ACCESS_CONTROL_ALLOW_METHODS_ != null) {
+ response.setHeader("Access-Control-Allow-Methods", ACCESS_CONTROL_ALLOW_METHODS_);
+ }
+ if (ACCESS_CONTROL_ALLOW_HEADERS_ != null) {
+ response.setHeader("Access-Control-Allow-Headers", ACCESS_CONTROL_ALLOW_HEADERS_);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
+ if ("received".equals(request.getQueryString())) {
+ response.setHeader("Access-Control-Allow-Origin", "*");
+ response.setContentType(MimeType.TEXT_PLAIN);
+ response.getWriter().write(RECEIVED_ == null ? "nothing" : RECEIVED_);
+ return;
+ }
+ received(request, response);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void doPut(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
+ received(request, response);
+ }
+
+ private static void received(final HttpServletRequest request, final HttpServletResponse response)
+ throws IOException {
+ RECEIVED_ = request.getMethod() + " " + request.getHeader(HttpHeader.COOKIE);
+ setCorsHeaders(response);
+ response.setContentType(MimeType.TEXT_PLAIN);
+ response.getWriter().write("ok");
+ }
+
+ private static void setCorsHeaders(final HttpServletResponse response) {
+ if (ACCESS_CONTROL_ALLOW_ORIGIN_ != null) {
+ response.setHeader("Access-Control-Allow-Origin", ACCESS_CONTROL_ALLOW_ORIGIN_);
+ }
+ if (ACCESS_CONTROL_ALLOW_CREDENTIALS_ != null) {
+ response.setHeader("Access-Control-Allow-Credentials", ACCESS_CONTROL_ALLOW_CREDENTIALS_);
+ }
+ }
+ }
+
/**
* @throws Exception if the test fails.
*/