VYPR
Critical severityNVD Advisory· Published Jul 10, 2023· Updated Nov 8, 2024

XWiki Platform vulnerable to cross-site request forgery (CSRF) via the REST API

CVE-2023-37277

Description

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.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.xwiki.platform:xwiki-platform-rest-serverMaven
>= 1.8, < 14.10.814.10.8
com.xpn.xwiki.platform:xwiki-core-rest-serverMaven
>= 1.8, < 14.10.814.10.8
com.xpn.xwiki.platform:xwiki-restMaven
>= 1.8, < 14.10.814.10.8
org.xwiki.platform:xwiki-platform-rest-serverMaven
>= 15.0-rc-1, < 15.215.2

Affected products

1

Patches

1
4c175405faa0

XWIKI-20135: Require a CSRF token for some request types in the REST API

https://github.com/xwiki/xwiki-platformMichael HamannMar 15, 2023via ghsa
10 files changed · +352 5
  • xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-test/xwiki-platform-flamingo-skin-test-docker/src/test/it/org/xwiki/flamingo/test/docker/AllITs.java+6 0 modified
    @@ -188,4 +188,10 @@ class NestedSheetIT extends SheetIT
         class NestedScriptAuthorIT extends ScriptAuthorIT
         {
         }
    +
    +    @Nested
    +    @DisplayName("Form Token injection Tests")
    +    class NestedFormTokenInjectionIT extends FormTokenInjectionIT
    +    {
    +    }
     }
    
  • xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-test/xwiki-platform-flamingo-skin-test-docker/src/test/it/org/xwiki/flamingo/test/docker/FormTokenInjectionIT.java+78 0 added
    @@ -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))))
    +        );
    +    }
    +}
    
  • xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-test/xwiki-platform-flamingo-skin-test-docker/src/test/resources/FormTokenInjectionIT/testCode.js+61 0 added
    @@ -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
    
  • xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/src/main/java/org/xwiki/rest/internal/XWikiFilter.java+59 1 modified
    @@ -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;
         }
     }
    
  • xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-test/xwiki-platform-rest-test-tests/src/test/it/org/xwiki/rest/test/AttachmentsResourceIT.java+2 0 modified
    @@ -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());
     
    
  • xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-test/xwiki-platform-rest-test-tests/src/test/it/org/xwiki/rest/test/CommentsResourceIT.java+25 0 modified
    @@ -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
         {
    
  • xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-test/xwiki-platform-rest-test-tests/src/test/it/org/xwiki/rest/test/framework/AbstractHttpIT.java+25 0 modified
    @@ -281,15 +281,31 @@ 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));
             httpClient.getParams().setAuthenticationPreemptive(true);
     
             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);
     
    
  • xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-test/xwiki-platform-rest-test-tests/src/test/it/org/xwiki/rest/test/ObjectsResourceIT.java+31 0 modified
    @@ -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
         {
    
  • xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-test/xwiki-platform-rest-test-tests/src/test/it/org/xwiki/rest/test/PageResourceIT.java+30 0 modified
    @@ -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
         {
    
  • xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-war/src/main/webapp/resources/js/xwiki/xwiki.js+35 4 modified
    @@ -1882,26 +1882,57 @@ 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;
         window.XMLHttpRequest.prototype.open = 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;
             });
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.