CVE-2023-37277 (published 2023-07-10T17:15Z)

XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. The REST API allows executing all actions via POST requests and accepts `text/plain`, `multipart/form-data` or `application/www-form-urlencoded` as content types which can be sent via regular HTML forms, thus allowing cross-site request forgery. With the interaction of a user with programming rights, this allows remote code execution through script macros and thus impacts the integrity, availability and confidentiality of the whole XWiki installation. For regular cookie-based authentication, the vulnerability is mitigated by SameSite cookie restrictions but as of March 2023, these are not enabled by default in Firefox and Safari. The vulnerability has been patched in XWiki 14.10.8 and 15.2 by requiring a CSRF token header for certain request types that are susceptible to CSRF attacks.\n\n

CVSS v3 base score: 9.6, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-352
cwe_name: Cross-Site Request Forgery (CSRF)
description: The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
extended_description: When a web server is designed to receive a request from a client without any mechanism for verifying that it was intentionally sent, then it might be possible for an attacker to trick a client into making an unintentional request to the web server which will be treated as an authentic request. This can be done via a URL, image load, XMLHttpRequest, etc. and can result in exposure of data or unintended code execution.
         

Fix information:
repo_url: https://github.com/xwiki/xwiki-platform
commit_details: repo_url: https://github.com/xwiki/xwiki-platform
author: Michael Hamann
author_date: 2023-03-15 18:03:37+01:00
msg: XWIKI-20135: Require a CSRF token for some request types in the REST API

* Require a CSRF token in the XWiki-Form-Token header in content types
  allowed in simple requests.
* Add integration tests to check that the check is indeed working.
* Automatically add the CSRF token header in same-origin requests
  initiated from JavaScript.
* Add an integration test to check that the form token is correctly
  added and fetch requests are still working.

Co-authored-by: Marius Dumitru Florea <marius@xwiki.com>
repository: repo_url: https://github.com/xwiki/xwiki-platform
repo_name: xwiki/xwiki-platform
description: The XWiki platform
repo_language: Java
owner: xwiki
file_changes: filename: AllITs.java
change_type: ModificationType.MODIFY
diff: @@ -188,4 +188,10 @@ class NestedSheetIT extends SheetIT
     class NestedScriptAuthorIT extends ScriptAuthorIT
     {
     }
+
+    @Nested
+    @DisplayName("Form Token injection Tests")
+    class NestedFormTokenInjectionIT extends FormTokenInjectionIT
+    {
+    }
 }

programming_language: Scala
filename: FormTokenInjectionIT.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,78 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.flamingo.test.docker;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.xwiki.test.docker.junit5.TestReference;
+import org.xwiki.test.docker.junit5.UITest;
+import org.xwiki.test.ui.TestUtils;
+import org.xwiki.test.ui.po.ViewPage;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+/**
+ * Integration test to verify that the form token is correctly injected in JavaScript requests.
+ *
+ * @version $Id$
+ */
+@UITest
+class FormTokenInjectionIT
+{
+    @Test
+    @Order(1)
+    void simpleRESTPost(TestUtils setup, TestReference reference) throws Exception
+    {
+        setup.loginAsSuperAdmin();
+        setup.deletePage(reference);
+
+        String content = "{{html clean=\"false\"}}"
+            + "<div id='results'></div>"
+            + "<script>"
+            + IOUtils.toString(
+            Objects.requireNonNull(getClass().getResourceAsStream("/FormTokenInjectionIT/testCode.js")),
+            StandardCharsets.UTF_8)
+            + "</script>"
+            + "{{/html}}";
+        setup.createPage(reference, content);
+
+        ViewPage viewPage = setup.gotoPage(reference);
+        String pageContent = viewPage.getContent();
+
+        assertAll(
+            Stream.of(
+                "Simple POST: 201",
+                "Only Request: 201",
+                "Request with init: 201",
+                "Simple with array headers: 201",
+                "Request with init body",
+                "Request Body",
+                "Simple with array headers body"
+            ).map(expected -> (() -> assertThat(pageContent, containsString(expected))))
+        );
+    }
+}

programming_language: Java
filename: testCode.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,61 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+require(['xwiki-meta'], function (xm) {
+  const postURL = xm.restURL + '/comments';
+  const resultDiv = document.getElementById('results');
+
+  async function addComments() {
+    // Wait after each request as the REST API is not thread-safe (object ids aren't properly incremented and integrity
+    // constraints are violated, see also https://jira.xwiki.org/browse/XWIKI-13473).
+    await fetch(postURL, {
+      method: "POST",
+      body: "Simple POST"
+    }).then(response => resultDiv.textContent += 'Simple POST: ' + response.status);
+
+    await fetch(new Request(postURL, {
+      method: "POST",
+      body: "Request Body",
+      headers: {"Accept": "application/json"}
+    })).then(async response => {
+      resultDiv.textContent += 'Only Request: ' + response.status;
+      return response.json();
+    }).then(comment => resultDiv.textContent += comment.text);
+
+    await fetch(new Request(postURL), {
+      method: "POST",
+      body: "Request with init body",
+      headers: {"Accept": "application/json"}
+    }).then(response => {
+      resultDiv.textContent += 'Request with init: ' + response.status;
+      return response.json();
+    }).then(comment => resultDiv.textContent += comment.text);
+
+    await fetch(postURL, {
+      method: "POST",
+      body: "Simple with array headers body",
+      headers: [["Accept", "application/json"]]
+    }).then(response => {
+      resultDiv.textContent += 'Simple with array headers: ' + response.status;
+      return response.json();
+    }).then(comment => resultDiv.textContent += comment.text);
+  }
+
+  addComments();
+});
\ No newline at end of file

programming_language: JavaScript
filename: XWikiFilter.java
change_type: ModificationType.MODIFY
diff: @@ -19,16 +19,25 @@
  */
 package org.xwiki.rest.internal;
 
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.exception.ExceptionUtils;
 import org.restlet.Context;
 import org.restlet.Request;
 import org.restlet.Response;
 import org.restlet.data.Header;
+import org.restlet.data.MediaType;
+import org.restlet.data.Status;
 import org.restlet.engine.header.HeaderConstants;
+import org.restlet.ext.servlet.ServletUtils;
 import org.restlet.routing.Filter;
 import org.restlet.util.Series;
 import org.xwiki.component.manager.ComponentLookupException;
 import org.xwiki.component.manager.ComponentManager;
+import org.xwiki.csrf.CSRFToken;
 import org.xwiki.model.reference.EntityReferenceSerializer;
 
 import com.xpn.xwiki.XWikiContext;
@@ -42,6 +51,17 @@
  */
 public class XWikiFilter extends Filter
 {
+    /**
+     * Content-types that are allowed in a
+     * <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests">simple request</a> that don't
+     * trigger a CORS preflight request in browsers.
+     */
+    private static final List<String> SIMPLE_CONTENT_TYPES = List.of(
+        "application/x-www-form-urlencoded", "multipart/form-data", "text/plain"
+    );
+
+    private static final String FORM_TOKEN_HEADER = "XWiki-Form-Token";
+
     /**
      * Constructor.
      * 
@@ -58,6 +78,13 @@ protected int beforeHandle(Request request, Response response)
         ComponentManager componentManager =
             (ComponentManager) getContext().getAttributes().get(Constants.XWIKI_COMPONENT_MANAGER);
         XWikiContext xwikiContext = Utils.getXWikiContext(componentManager);
+        CSRFToken csrfToken = null;
+
+        try {
+            csrfToken = componentManager.getInstance(CSRFToken.class);
+        } catch (ComponentLookupException e) {
+            getLogger().warning("Failed to lookup CSRF token validator: " + ExceptionUtils.getRootCauseMessage(e));
+        }
 
         try {
             EntityReferenceSerializer<String> serializer =
@@ -75,11 +102,42 @@ protected int beforeHandle(Request request, Response response)
             }
             responseHeaders.add("XWiki-User", serializer.serialize(xwikiContext.getUserReference()));
             responseHeaders.add("XWiki-Version", xwikiContext.getWiki().getVersion());
+
+            if (csrfToken != null) {
+                responseHeaders.add(FORM_TOKEN_HEADER, csrfToken.getToken());
+            }
         } catch (ComponentLookupException e) {
             getLogger()
                 .warning("Failed to lookup the entity reference serializer: " + ExceptionUtils.getRootCauseMessage(e));
         }
 
-        return CONTINUE;
+        int result = CONTINUE;
+
+        HttpServletRequest servletRequest = ServletUtils.getRequest(Request.getCurrent());
+
+        // Require a CSRF token for requests that browsers allow through HTML forms and across origins.
+        // See https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS for more information.
+        // Compare to the method from the servlet request to avoid the automatic conversion from POST to PUT request.
+        // Check for a prefix match to make sure it matches regardless of the supplied parameters (like charset).
+        if ("POST".equals(servletRequest.getMethod()) && SIMPLE_CONTENT_TYPES.stream().anyMatch(expectedType ->
+            StringUtils.startsWith(StringUtils.lowerCase(servletRequest.getContentType()), expectedType)))
+        {
+            Series<Header> requestHeaders = request.getHeaders();
+            String formToken = requestHeaders.getFirstValue(FORM_TOKEN_HEADER);
+
+            // Skip the main request handler but allow cleanup if either the CSRF validator failed or the token is
+            // invalid.
+            if (csrfToken == null) {
+                response.setStatus(Status.SERVER_ERROR_INTERNAL);
+                response.setEntity("Failed to lookup the CSRF token validator.", MediaType.TEXT_PLAIN);
+                result = SKIP;
+            } else if (!csrfToken.isTokenValid(formToken)) {
+                response.setStatus(Status.CLIENT_ERROR_FORBIDDEN);
+                response.setEntity("Invalid or missing form token.", MediaType.TEXT_PLAIN);
+                result = SKIP;
+            }
+        }
+
+        return result;
     }
 }

programming_language: Java
filename: AttachmentsResourceIT.java
change_type: ModificationType.MODIFY
diff: @@ -319,6 +319,8 @@ public void testPOSTAttachment() throws Exception
         PostMethod postMethod = new PostMethod(attachmentsUri);
         MultipartRequestEntity mpre = new MultipartRequestEntity(parts, postMethod.getParams());
         postMethod.setRequestEntity(mpre);
+        postMethod.setRequestHeader("XWiki-Form-Token", getFormToken(TestUtils.SUPER_ADMIN_CREDENTIALS.getUserName(),
+            TestUtils.SUPER_ADMIN_CREDENTIALS.getPassword()));
         httpClient.executeMethod(postMethod);
         Assert.assertEquals(getHttpMethodInfo(postMethod), HttpStatus.SC_CREATED, postMethod.getStatusCode());
 

programming_language: Java
filename: CommentsResourceIT.java
change_type: ModificationType.MODIFY
diff: @@ -128,6 +128,31 @@ public void testPOSTCommentWithTextPlain() throws Exception
         Assert.assertEquals(numberOfComments + 1, comments.getComments().size());
     }
 
+    @Test
+    public void testPOSTCommentWithTextPlainNoCSRF() throws Exception
+    {
+        String commentsUri = buildURI(CommentsResource.class, getWiki(), this.spaces, this.pageName).toString();
+
+        GetMethod getMethod = executeGet(commentsUri);
+        Assert.assertEquals(getHttpMethodInfo(getMethod), HttpStatus.SC_OK, getMethod.getStatusCode());
+
+        Comments comments = (Comments) unmarshaller.unmarshal(getMethod.getResponseBodyAsStream());
+
+        int numberOfComments = comments.getComments().size();
+
+        PostMethod postMethod = executePost(commentsUri, "Comment", MediaType.TEXT_PLAIN,
+            TestUtils.SUPER_ADMIN_CREDENTIALS.getUserName(), TestUtils.SUPER_ADMIN_CREDENTIALS.getPassword(), null);
+        Assert.assertEquals(getHttpMethodInfo(postMethod), HttpStatus.SC_FORBIDDEN, postMethod.getStatusCode());
+        Assert.assertEquals("Invalid or missing form token.", postMethod.getResponseBodyAsString());
+
+        getMethod = executeGet(commentsUri);
+        Assert.assertEquals(getHttpMethodInfo(getMethod), HttpStatus.SC_OK, getMethod.getStatusCode());
+
+        comments = (Comments) unmarshaller.unmarshal(getMethod.getResponseBodyAsStream());
+
+        Assert.assertEquals(numberOfComments, comments.getComments().size());
+    }
+
     @Test
     public void testGETComment() throws Exception
     {

programming_language: Java
filename: ObjectsResourceIT.java
change_type: ModificationType.MODIFY
diff: @@ -520,6 +520,37 @@ public void testPOSTObjectFormUrlEncoded() throws Exception
         Assert.assertEquals(TAG_VALUE, getProperty(object, "tags").getValue());
     }
 
+    @Test
+    public void testPOSTObjectFormUrlEncodedNoCSRF() throws Exception
+    {
+        final String tagValue = "TAG";
+        NameValuePair[] nameValuePairs = new NameValuePair[2];
+        String className = "XWiki.TagClass";
+        nameValuePairs[0] = new NameValuePair("className", className);
+        nameValuePairs[1] = new NameValuePair("property#tags", tagValue);
+
+        String objectGetURI = buildURI(ObjectsResource.class, getWiki(), this.spaces, this.pageName, className);
+
+        // Count objects before to ensure nothing is added on the failed request.
+        GetMethod getMethod = executeGet(objectGetURI);
+        Assert.assertEquals(getHttpMethodInfo(getMethod), HttpStatus.SC_OK, getMethod.getStatusCode());
+        Objects objects = (Objects) unmarshaller.unmarshal(getMethod.getResponseBodyAsStream());
+        int numObjects = objects.getObjectSummaries().size();
+
+        PostMethod postMethod = executePostForm(
+            buildURI(ObjectsResource.class, getWiki(), this.spaces, this.pageName), nameValuePairs,
+            TestUtils.SUPER_ADMIN_CREDENTIALS.getUserName(), TestUtils.SUPER_ADMIN_CREDENTIALS.getPassword(), null);
+        Assert.assertEquals(getHttpMethodInfo(postMethod), HttpStatus.SC_FORBIDDEN, postMethod.getStatusCode());
+        Assert.assertEquals("Invalid or missing form token.", postMethod.getResponseBodyAsString());
+
+        getMethod = executeGet(objectGetURI);
+        Assert.assertEquals(getHttpMethodInfo(getMethod), HttpStatus.SC_OK, getMethod.getStatusCode());
+
+        objects = (Objects) unmarshaller.unmarshal(getMethod.getResponseBodyAsStream());
+        Assert.assertEquals(numObjects, objects.getObjectSummaries().size());
+    }
+
+
     @Test
     public void testPUTPropertyFormUrlEncoded() throws Exception
     {

programming_language: Java
filename: PageResourceIT.java
change_type: ModificationType.MODIFY
diff: @@ -576,6 +576,36 @@ public void testPOSTPageFormUrlEncoded() throws Exception
         Assert.assertEquals(TITLE, modifiedPage.getTitle());
     }
 
+    @Test
+    public void testPOSTPageFormUrlEncodedNoCSRF() throws Exception
+    {
+        final String CONTENT = String.format("This is a content (%d)", System.currentTimeMillis());
+        final String TITLE = String.format("Title (%s)", UUID.randomUUID().toString());
+
+        Page originalPage = getFirstPage();
+
+        Link link = getFirstLinkByRelation(originalPage, Relations.SELF);
+        Assert.assertNotNull(link);
+
+        NameValuePair[] nameValuePairs = new NameValuePair[2];
+        nameValuePairs[0] = new NameValuePair("title", TITLE);
+        nameValuePairs[1] = new NameValuePair("content", CONTENT);
+
+        PostMethod postMethod = executePostForm(String.format("%s?method=PUT", link.getHref()), nameValuePairs,
+            TestUtils.SUPER_ADMIN_CREDENTIALS.getUserName(), TestUtils.SUPER_ADMIN_CREDENTIALS.getPassword(), null);
+        Assert.assertEquals(getHttpMethodInfo(postMethod), HttpStatus.SC_FORBIDDEN, postMethod.getStatusCode());
+        Assert.assertEquals("Invalid or missing form token.", postMethod.getResponseBodyAsString());
+
+        // Assert that the page hasn't been modified.
+        GetMethod getMethod = executeGet(link.getHref());
+        Assert.assertEquals(getHttpMethodInfo(getMethod), HttpStatus.SC_OK, getMethod.getStatusCode());
+
+        Page modifiedPage = (Page) this.unmarshaller.unmarshal(getMethod.getResponseBodyAsStream());
+
+        Assert.assertEquals(originalPage.getContent(), modifiedPage.getContent());
+        Assert.assertEquals(originalPage.getTitle(), modifiedPage.getTitle());
+    }
+
     @Test
     public void testPUTPageSyntax() throws Exception
     {

programming_language: Java
filename: AbstractHttpIT.java
change_type: ModificationType.MODIFY
diff: @@ -281,8 +281,21 @@ protected PostMethod executePost(String uri, InputStream is, String userName, St
         return postMethod;
     }
 
+    protected String getFormToken(String userName, String password) throws Exception
+    {
+        GetMethod getMethod = executeGet(getFullUri(WikisResource.class), userName, password);
+        Assert.assertEquals(getHttpMethodInfo(getMethod), HttpStatus.SC_OK, getMethod.getStatusCode());
+        return getMethod.getResponseHeader("XWiki-Form-Token").getValue();
+    }
+
     protected PostMethod executePost(String uri, String string, String mediaType, String userName, String password)
         throws Exception
+    {
+        return executePost(uri, string, mediaType, userName, password, getFormToken(userName, password));
+    }
+
+    protected PostMethod executePost(String uri, String string, String mediaType, String userName, String password,
+        String formToken) throws Exception
     {
         HttpClient httpClient = new HttpClient();
         httpClient.getState().setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(userName, password));
@@ -290,6 +303,9 @@ protected PostMethod executePost(String uri, String string, String mediaType, St
 
         PostMethod postMethod = new PostMethod(uri);
         postMethod.addRequestHeader("Accept", MediaType.APPLICATION_XML.toString());
+        if (formToken != null) {
+            postMethod.addRequestHeader("XWiki-Form-Token", formToken);
+        }
 
         RequestEntity entity = new StringRequestEntity(string, mediaType, "UTF-8");
         postMethod.setRequestEntity(entity);
@@ -301,6 +317,12 @@ protected PostMethod executePost(String uri, String string, String mediaType, St
 
     protected PostMethod executePostForm(String uri, NameValuePair[] nameValuePairs, String userName, String password)
         throws Exception
+    {
+        return executePostForm(uri, nameValuePairs, userName, password, getFormToken(userName, password));
+    }
+
+    protected PostMethod executePostForm(String uri, NameValuePair[] nameValuePairs, String userName, String password,
+        String formToken) throws Exception
     {
         HttpClient httpClient = new HttpClient();
         httpClient.getState().setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(userName, password));
@@ -309,6 +331,9 @@ protected PostMethod executePostForm(String uri, NameValuePair[] nameValuePairs,
         PostMethod postMethod = new PostMethod(uri);
         postMethod.addRequestHeader("Accept", MediaType.APPLICATION_XML.toString());
         postMethod.addRequestHeader("Content-type", MediaType.APPLICATION_WWW_FORM.toString());
+        if (formToken != null) {
+            postMethod.addRequestHeader("XWiki-Form-Token", formToken);
+        }
 
         postMethod.setRequestBody(nameValuePairs);
 

programming_language: Java
filename: xwiki.js
change_type: ModificationType.MODIFY
diff: @@ -1882,7 +1882,8 @@ document.observe("xwiki:dom:loaded", function() {
   };
 
   /**
-   * Overwrite the XMLHttpRequest#open() method in order to add our load listener on all requests made from this page.
+   * Overwrite the XMLHttpRequest#open() method in order to inject the form token and to add our load listener on all
+   * requests made from this page.
    */
   var interceptXMLHttpRequest = function() {
     var originalOpen = window.XMLHttpRequest.prototype.open;
@@ -1890,18 +1891,48 @@ document.observe("xwiki:dom:loaded", function() {
       this.addEventListener('load', function() {
         handleResponseHeaders(this.getResponseHeader.bind(this));
       });
-      return originalOpen.apply(this, arguments);
+      const result = originalOpen.apply(this, arguments);
+      // Send the form token on same-origin requests only to prevent leaking the token to third-parties.
+      if (arguments.length >= 2 && window.location.origin === (new URL(arguments[1], window.location.href)).origin) {
+        // Make sure this is really safe in case this should be called in some unexpected situation.
+        const formToken = document?.documentElement?.dataset?.xwikiFormToken;
+        if (formToken) {
+          this.setRequestHeader("XWiki-Form-Token", formToken);
+        }
+      }
+      return result;
     };
   };
 
   /**
-   * Overwrite the fetch function in order to add our own response callback on all fetch requests made from this page.
+   * Overwrite the fetch function in order to inject the form token and add our own response callback on all fetch
+   * requests made from this page.
    */
   var interceptFetch = function() {
     var originalFetch = window.fetch;
     if (originalFetch) {
       window.fetch = function() {
-        return originalFetch.apply(this, arguments).then(function(response) {
+        // Inject the form token.
+        let modifiedArguments = arguments;
+        // Make sure this is really safe in case this should be called in some unexpected situation.
+        const formToken = document?.documentElement?.dataset?.xwikiFormToken;
+        if (formToken) {
+          let request = null;
+          // Convert the arguments to a request, as Request expects the same arguments as fetch() but provides
+          // convenient ways to modify the headers (and fetch accepts a request as parameter).
+          if (arguments.length === 1 && arguments[0] instanceof Request) {
+            request = arguments[0];
+          } else if (arguments.length) {
+            request = new Request(...arguments);
+          }
+          // Only handle expected cases and same-origin requests to prevent leaking the token to third-parties,
+          // leave the arguments alone otherwise.
+          if (request !== null && window.location.origin === (new URL(request.url, window.location.href).origin)) {
+            request.headers.append("XWiki-Form-Token", formToken);
+            modifiedArguments = [request];
+          }
+        }
+        return originalFetch.apply(this, modifiedArguments).then(function(response) {
           handleResponseHeaders(response.headers.get.bind(response.headers));
           return response;
         });

programming_language: JavaScript