Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

<body>
<release version="5.8.0" date="xxxx xx, 2026" description="Bugfixes">
<action type="fix" dev="Vidya m">
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.
</action>
<action type="fix" dev="rbri">
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.
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/htmlunit/HttpHeader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
45 changes: 40 additions & 5 deletions src/main/java/org/htmlunit/javascript/host/xml/XMLHttpRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
* @author Lai Quang Duong
* @author Sven Strickroth
* @author Ashutosh Panda
* @author Vidya m
*
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest">MDN Documentation</a>
*/
Expand Down Expand Up @@ -1395,10 +1396,19 @@
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();

Check warning on line 1407 in src/main/java/org/htmlunit/javascript/host/xml/XMLHttpRequest.java

View workflow job for this annotation

GitHub Actions / PMD

[PMD] reported by reviewdog 🐶 Local variable `wildcard` is not declared at start of block Raw Output: {"level":"warning","locations":[{"physicalLocation":{"artifactLocation":{"uri":"file:///home/runner/work/htmlunit/htmlunit/src/main/java/org/htmlunit/javascript/host/xml/XMLHttpRequest.java"},"region":{"endColumn":54,"endLine":1407,"startColumn":23,"startLine":1407}}}],"message":{"text":"Local variable `wildcard` is not declared at start of block"},"ruleId":"LocalVariableDeclarationShouldBeAtStartOfBlock","ruleIndex":3101}

Check warning on line 1407 in src/main/java/org/htmlunit/javascript/host/xml/XMLHttpRequest.java

View workflow job for this annotation

GitHub Actions / PMD

[PMD] reported by reviewdog 🐶 Local variable `wildcard` is declared with initialization Raw Output: {"level":"warning","locations":[{"physicalLocation":{"artifactLocation":{"uri":"file:///home/runner/work/htmlunit/htmlunit/src/main/java/org/htmlunit/javascript/host/xml/XMLHttpRequest.java"},"region":{"endColumn":54,"endLine":1407,"startColumn":23,"startLine":1407}}}],"message":{"text":"Local variable `wildcard` is declared with initialization"},"ruleId":"LocalVariableDeclarationShouldBeAtStartOfBlock","ruleIndex":3100}

final HttpMethod method = request.getHttpMethod();

Check warning on line 1409 in src/main/java/org/htmlunit/javascript/host/xml/XMLHttpRequest.java

View workflow job for this annotation

GitHub Actions / PMD

[PMD] reported by reviewdog 🐶 Local variable `method` is not declared at start of block Raw Output: {"level":"warning","locations":[{"physicalLocation":{"artifactLocation":{"uri":"file:///home/runner/work/htmlunit/htmlunit/src/main/java/org/htmlunit/javascript/host/xml/XMLHttpRequest.java"},"region":{"endColumn":58,"endLine":1409,"startColumn":26,"startLine":1409}}}],"message":{"text":"Local variable `method` is not declared at start of block"},"ruleId":"LocalVariableDeclarationShouldBeAtStartOfBlock","ruleIndex":2862}

Check warning on line 1409 in src/main/java/org/htmlunit/javascript/host/xml/XMLHttpRequest.java

View workflow job for this annotation

GitHub Actions / PMD

[PMD] reported by reviewdog 🐶 Local variable `method` is declared with initialization Raw Output: {"level":"warning","locations":[{"physicalLocation":{"artifactLocation":{"uri":"file:///home/runner/work/htmlunit/htmlunit/src/main/java/org/htmlunit/javascript/host/xml/XMLHttpRequest.java"},"region":{"endColumn":58,"endLine":1409,"startColumn":26,"startLine":1409}}}],"message":{"text":"Local variable `method` is declared with initialization"},"ruleId":"LocalVariableDeclarationShouldBeAtStartOfBlock","ruleIndex":142}
if (method != HttpMethod.GET && method != HttpMethod.HEAD && method != HttpMethod.POST
&& !isPreflightMethodAllowed(method.name(), preflightResponse, wildcard)) {
return false;
}

Expand All @@ -1409,7 +1419,7 @@
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;
}
Expand All @@ -1435,6 +1445,31 @@
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();

Check warning on line 1463 in src/main/java/org/htmlunit/javascript/host/xml/XMLHttpRequest.java

View workflow job for this annotation

GitHub Actions / PMD

[PMD] reported by reviewdog 🐶 Local variable `allowed` is declared with initialization Raw Output: {"level":"warning","locations":[{"physicalLocation":{"artifactLocation":{"uri":"file:///home/runner/work/htmlunit/htmlunit/src/main/java/org/htmlunit/javascript/host/xml/XMLHttpRequest.java"},"region":{"endColumn":55,"endLine":1463,"startColumn":34,"startLine":1463}}}],"message":{"text":"Local variable `allowed` is declared with initialization"},"ruleId":"LocalVariableDeclarationShouldBeAtStartOfBlock","ruleIndex":3096}
if (method.equals(allowed) || (wildcard && ALLOW_ORIGIN_ALL.equals(allowed))) {
return true;
}
}
}
}
return false;
}

/**
* Checks whether the given header name requires a preflight request.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
* @author Ronald Brill
* @author Frank Danek
* @author Lai Quang Duong
* @author Vidya m
*/
public class XMLHttpRequestCORSTest extends WebDriverTestCase {

Expand Down Expand Up @@ -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
+ "<html><head>\n"
+ "<script>\n"
+ LOG_TITLE_FUNCTION
+ "function test() {\n"
+ " var url = 'http://' + window.location.hostname + ':" + PORT2 + "/preflight2';\n"
+ " var xhr = new XMLHttpRequest();\n"
+ " try {\n"
+ " xhr.open('" + method + "', url, false);\n"
+ " xhr.withCredentials = " + withCredentials + ";\n"
+ " xhr.setRequestHeader('X-PING', 'ping');\n"
+ " xhr.send();\n"
+ " } catch(e) { logEx(e) }\n"
+ " log(xhr.readyState);\n"
+ " log(xhr.status);\n"

+ " var received = new XMLHttpRequest();\n"
+ " received.open('GET', url + '?received', false);\n"
+ " received.send();\n"
+ " log(received.responseText);\n"
+ "}\n"
+ "</script>\n"
+ "</head>\n"
+ "<body onload='test()'></body></html>";

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<String, Class<? extends Servlet>> servlets2 = new HashMap<>();
servlets2.put("/preflight2", PreflightReceivedServlet.class);
startWebServer2(".", servlets2);

final List<NameValuePair> 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.
*/
Expand Down
Loading