CVE-2022-36097 (Other, published 2022-09-08T21:15Z)
Description: value: XWiki Platform Attachment UI provides a macro to easily upload and select attachments for XWiki Platform, a generic wiki platform. Starting with version 14.0-rc-1 and prior to 14.4-rc-1, it's possible to store JavaScript in an attachment name, which will be executed by anyone trying to move the corresponding attachment. This issue has been patched in XWiki 14.4-rc-1. As a workaround, one may copy `moveStep1.vm` to `webapp/xwiki/templates/moveStep1.vm` and replace vulnerable code with code from the patch.
Severity: nan (CVSS v3 base score: 6.1)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/xwiki/xwiki-platform
Files changed:
xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/ApplicationResources.properties
xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/move.vm
xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/moveStatus.vm
xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/moveStep1.vm
xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/refactoring_macros.vm
xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/MovePageTest.java
xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/MoveStatusPagesTest.java
xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/MoveStep1PageTest.java
xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/RefactoringMacrosPageTest.java
Commit message: XWIKI-19667: The move attachment form is missing escaping and some translations


Vulnerable code:
        #error($services.localization.render('attachment.move.emptyName'))
          #error($services.localization.render('attachment.move.targetNotWritable',
            [${escapetool.xml($targetLocation)}]))
            #displayException($services.localization.render('attachment.job.creation.error'), $moveJobException)
  <h2>$services.localization.render('attachment.move.status.label')</h2>
  <p class="text-muted small">$services.localization.render('attachment.move.status.hint',
  [$xwiki.getUserName($moveJobRequest.userReference), $xwiki.formatDate($moveJobStatus.startDate)])</p>
    #outputMoveJobStatusJSON($moveJobStatus)
  $services.localization.render('attachment.move.status.notFound')
#set($titleToDisplay = $services.localization.render('attachment.move.title', 
  [$attachment.name, $escapetool.xml($doc.plainTitle), $doc.getURL()]))
        <dt>
            $services.localization.render('attachment.move.autoRedirect.label')
        <dd>
          <span class="xHint">$services.localization.render('attachment.move.autoRedirect.hint')</span>
        <dt>
          <label for="targetAttachmentName">$services.localization.render('attachment.move.newName.label')</label>
          <span class="xHint">$!services.localization.render('attachment.move.newName.hint')</span>
          <input type="text" id="targetAttachmentNameTitle" name="targetAttachmentName" value="$escapetool.xml($attachment.name)"
                 placeholder="$!services.localization.render('attachment.move.newName.placeholder')" /> 
          <label for="$!{options.id}Title">$services.localization.render('attachment.move.newLocation.label')</label>
          <span class="xHint">$!services.localization.render('attachment.move.newLocation.hint')</span>
                  <h5 class="modal-title">Modal title</h5>
                  <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
                  <button type="button" class="btn btn-primary">Select</button>
      <input type="submit" value="$services.localization.render('attachment.move.submit')" class="button" />
      <a class="secondary button" href="$doc.getURL()">$services.localization.render('cancel')</a>
  <label>$services.localization.render('attachment.move.source.label')</label>
  <span class="xHint">$services.localization.render('attachment.move.source.hint')</span>
    $services.localization.render('attachment.move.links.label')
    $services.localization.render('attachment.move.links.hint', [$backLinksCount])
        assertEquals("error: attachment.move.targetNotWritable [Space.Target]",
        DocumentReference targetDocumentReference = new DocumentReference("xwiki", "Space", "Target");
        this.request.put("targetLocation", "Space.Target");
        assertEquals(
            "error: attachment.move.alreadyExists [attachment.txt, Space.Target, /xwiki/bin/view/Space/Target]",

Fixed code:
attachment.move.targetLocation.modal.title=Select New Attachment Location
attachment.move.targetLocation.modal.selectButton=Select
attachment.move.targetLocation.modal.closeButton=Close
        #error($escapetool.xml($services.localization.render('attachment.move.emptyName')))
          #error($escapetool.xml($services.localization.render('attachment.move.targetNotWritable')))
            #displayException($escapetool.xml($services.localization.render('attachment.job.creation.error')), $moveJobException)
  <h2>$escapetool.xml($services.localization.render('attachment.move.status.label'))</h2>
  <p class="text-muted small">
    $escapetool.xml($services.localization.render('attachment.move.status.hint', 
      [$xwiki.getUserName($moveJobRequest.userReference), $xwiki.formatDate($moveJobStatus.startDate)]))
  </p>
    #outputmoveJobStatusJSON($moveJobStatus)
  $escapetool.xml($services.localization.render('attachment.move.status.notFound'))
#set($titleToDisplay = $services.localization.render('attachment.move.title', [
  $escapetool.xml($attachment.name), 
  $escapetool.xml($doc.plainTitle), 
  $escapetool.xml($doc.getURL())
]))
        <dt class="autoRedirect">
            $escapetool.xml($services.localization.render('attachment.move.autoRedirect.label'))
        <dd class="autoRedirect">
          <span class="xHint">
            $escapetool.xml($services.localization.render('attachment.move.autoRedirect.hint'))
          </span>
        <dt class="targetAttachmentName">
          <label for="targetAttachmentNameTitle">
            $escapetool.xml($services.localization.render('attachment.move.newName.label'))
          </label>
          <span class="xHint">
            $escapetool.xml($services.localization.render('attachment.move.newName.hint'))
          </span>
          <input type="text" 
                 id="targetAttachmentNameTitle" 
                 name="targetAttachmentName" 
                 value="$escapetool.xml($attachment.name)"
                 placeholder="$escapetool.xml($services.localization.render('attachment.move.newName.placeholder'))" /> 
          <label for="targetLocation">
              $escapetool.xml($services.localization.render('attachment.move.newLocation.label'))
          </label>
          <span class="xHint">
            $escapetool.xml($services.localization.render('attachment.move.newLocation.hint'))
          </span>
                  <h5 class="modal-title">
                    $escapetool.xml($services.localization.render('attachment.move.targetLocation.modal.title'))
                  </h5>
                  <button type="button" class="btn btn-default" data-dismiss="modal">
                    $escapetool.xml($services.localization.render('attachment.move.targetLocation.modal.closeButton'))
                  </button>
                  <button type="button" class="btn btn-primary">
                    $escapetool.xml($services.localization.render('attachment.move.targetLocation.modal.selectButton'))
                  </button>
      <input type="submit" 
             value="$escapetool.xml($services.localization.render('attachment.move.submit'))" 
             class="button" />
      <a class="secondary button" href="$doc.getURL()">
        $escapetool.xml($services.localization.render('cancel'))
      </a>
  <label>
    $escapetool.xml($services.localization.render('attachment.move.source.label'))
  </label>
  <span class="xHint">
    $escapetool.xml($services.localization.render('attachment.move.source.hint'))
  </span>
    $escapetool.xml($services.localization.render('attachment.move.links.label'))
  $escapetool.xml($services.localization.render('attachment.move.links.hint', [$backLinksCount]))
        assertEquals("error: attachment.move.targetNotWritable",
    @Test
    void submitTargetAttachmentNameEmpty() throws Exception
    {
        this.context.setDoc(this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "Source"), this.context));
        this.request.put("step", "2");
        this.request.put("form_token", "a6DSv7pKWcPargoTvyx2Ww");
        Document render = Jsoup.parse(this.templateManager.render(MOVE_TEMPLATE));
        assertEquals("error: attachment.move.emptyName", render.select(".errormessage").text());
    }

        DocumentReference targetDocumentReference = new DocumentReference("xwiki", "Space", "Target\"'");
        this.request.put("targetLocation", "Space.Target\"'");
        assertEquals("error: attachment.move.alreadyExists "
                + "[attachment.txt, Space.Target\"', /xwiki/bin/view/Space/Target%22%27]",
/*
 * 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.attachment;

import java.util.List;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.xwiki.job.Request;
import org.xwiki.job.event.status.JobProgress;
import org.xwiki.job.event.status.JobStatus;
import org.xwiki.job.script.JobScriptService;
import org.xwiki.script.service.ScriptService;
import org.xwiki.template.TemplateManager;
import org.xwiki.template.script.TemplateScriptService;
import org.xwiki.test.annotation.ComponentList;
import org.xwiki.test.page.HTML50ComponentList;
import org.xwiki.test.page.PageTest;
import org.xwiki.test.page.XWikiSyntax21ComponentList;
import org.xwiki.xml.internal.html.filter.ControlCharactersFilter;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * Test of the macros provided by {@code moveStatus.vm}.
 *
 * @version $Id$
 * @since 14.4RC1
 */
@HTML50ComponentList
@XWikiSyntax21ComponentList
@ComponentList({
    ControlCharactersFilter.class,
    TemplateScriptService.class
})
class MoveStatusPagesTest extends PageTest
{
    private static final String MOVE_STATUS_TEMPLATE = "attachment/moveStatus.vm";

    private TemplateManager templateManager;

    @Mock
    private JobScriptService jobScriptService;

    @BeforeEach
    void setUp() throws Exception
    {
        this.templateManager = this.oldcore.getMocker().getInstance(TemplateManager.class);
        this.componentManager.registerComponent(ScriptService.class, "job", this.jobScriptService);
    }

    @Test
    void renderScriptStatusUnknown() throws Exception
    {
        this.request.put("moveId", "42");
        Document render = Jsoup.parse(this.templateManager.render(MOVE_STATUS_TEMPLATE));
        assertEquals("attachment.move.status.notFound", render.select(".errormessage").text());
        verify(this.jobScriptService).getJobStatus(List.of("refactoring", "moveAttachment", "42"));
    }

    @Test
    void renderScriptActionGet() throws Exception
    {
        JobStatus jobStatus = mock(JobStatus.class);
        Request request = mock(Request.class);
        JobProgress jobProgress = mock(JobProgress.class);

        when(request.getId()).thenReturn(List.of("rq", "id"));
        when(jobProgress.getOffset()).thenReturn(10d);
        when(jobStatus.getRequest()).thenReturn(request);
        when(jobStatus.getState()).thenReturn(JobStatus.State.RUNNING);
        when(jobStatus.getProgress()).thenReturn(jobProgress);
        when(this.jobScriptService.getJobStatus(List.of("refactoring", "moveAttachment", "42")))
            .thenReturn(jobStatus);
        this.request.put("moveId", "42");
        this.context.setAction("get");
        assertEquals(
            "{\"id\":[\"rq\",\"id\"],\"state\":\"RUNNING\",\"progress\":{\"offset\":10.0},"
                + "\"log\":{\"offset\":0,\"items\":[]},\"message\":\"        \",\"questionTimeLeft\":0}",
            this.templateManager.render(MOVE_STATUS_TEMPLATE).trim());
    }
}
/*
 * 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.attachment;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.xwiki.csrf.script.CSRFTokenScriptService;
import org.xwiki.model.reference.AttachmentReference;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.script.service.ScriptService;
import org.xwiki.security.script.SecurityScriptServiceComponentList;
import org.xwiki.template.TemplateManager;
import org.xwiki.test.page.PageTest;
import org.xwiki.velocity.VelocityManager;

import com.xpn.xwiki.doc.XWikiDocument;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.when;

/**
 * Test of {@code moveStep1.vm} template.
 *
 * @version $Id$
 * @since 14.4RC1
 */
@SecurityScriptServiceComponentList
class MoveStep1PageTest extends PageTest
{
    private static final String MOVE_STEP1_TEMPLATE = "attachment/moveStep1.vm";

    private static final String ATTACHMENT_NAME = ">a<b>tt</b>achmen\"'t1.txt";

    private static final DocumentReference DOC_REF = new DocumentReference("xwiki", "Space", "Page");

    private TemplateManager templateManager;

    @Mock
    private CSRFTokenScriptService csrfScriptService;

    private VelocityManager velocityManager;

    @BeforeEach
    void setUp() throws Exception
    {
        this.templateManager = this.oldcore.getMocker().getInstance(TemplateManager.class);
        this.velocityManager = this.oldcore.getMocker().getInstance(VelocityManager.class);
        this.componentManager.registerComponent(ScriptService.class, "csrf", this.csrfScriptService);
        when(this.csrfScriptService.getToken()).thenReturn("csrf_token");
    }

    @Test
    void escapedValues() throws Exception
    {
        XWikiDocument document = this.xwiki.getDocument(DOC_REF, this.context);
        this.context.setDoc(document);

        this.velocityManager.getVelocityContext().put("attachment", new AttachmentReference(ATTACHMENT_NAME,
            document.getDocumentReference()));

        Document render = Jsoup.parse(this.templateManager.render(MOVE_STEP1_TEMPLATE));
        assertNotNull(render);
        assertEquals("cancel", render.select(".buttons .secondary.button").text());
        assertEquals("attachment.move.submit", render.select(".buttons input").attr("value"));
        assertEquals("attachment.move.autoRedirect.label", render.select("dt.autoRedirect label").text());
        assertEquals("attachment.move.autoRedirect.hint", render.select("dd.autoRedirect .xHint").text());
        assertEquals("attachment.move.newName.label", render.select("dt.targetAttachmentName label").text());
        assertEquals("attachment.move.newName.hint", render.select("dt.targetAttachmentName .xHint").text());
        assertEquals(ATTACHMENT_NAME, render.select("[name='sourceAttachmentName']").attr("value"));
        assertEquals(ATTACHMENT_NAME, render.select("#targetAttachmentNameTitle").attr("value"));
    }
}
/*
 * 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.attachment;

import java.util.stream.Stream;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.xwiki.attachment.script.AttachmentScriptService;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.rendering.syntax.Syntax;
import org.xwiki.script.service.ScriptService;
import org.xwiki.test.annotation.ComponentList;
import org.xwiki.test.page.HTML50ComponentList;
import org.xwiki.test.page.PageTest;
import org.xwiki.test.page.XWikiSyntax21ComponentList;
import org.xwiki.xml.internal.html.filter.ControlCharactersFilter;

import com.xpn.xwiki.doc.XWikiDocument;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * Test of the macros provided by {@code refactoring_macros.vm}.
 *
 * @version $Id$
 * @since 14.4RC1
 */
@ComponentList({ ControlCharactersFilter.class })
@HTML50ComponentList
@XWikiSyntax21ComponentList
class RefactoringMacrosPageTest extends PageTest
{
    public static final DocumentReference DOCUMENT_REFERENCE = new DocumentReference("xwiki", "Space", "Page");

    @ParameterizedTest
    @CsvSource({
        "false,false,true",
        "false,true,false",
        "true,false,false",
        "true,true, false"
    })
    void displayAttachmentLinksCheckboxIsHidden(boolean isAdvancedUser, boolean isSuperAdmin, boolean expectHidden)
        throws Exception
    {
        XWikiDocument document = this.xwiki.getDocument(DOCUMENT_REFERENCE, this.context);
        document.setContent(String.format("{{velocity}}\n"
                + "#template('attachment/refactoring_macros.vm')\n"
                + "#set($isAdvancedUser = %s)\n"
                + "#set($isSuperAdmin = %s)\n"
                + "{{html}}#displayAttachmentLinksCheckbox(){{/html}}\n"
                + "{{/velocity}}",
            isAdvancedUser, isSuperAdmin));
        document.setSyntax(Syntax.XWIKI_2_1);
        this.xwiki.saveDocument(document, this.context);
        Document content = Jsoup.parse(document.getRenderedContent(this.context));
        assertEquals(expectHidden, content.select("dt").hasClass("hidden"));
        assertEquals(expectHidden, content.select("dd").hasClass("hidden"));
    }

    @ParameterizedTest
    @MethodSource("displayAttachmentLinksCheckboxSource")
    void displayAttachmentLinksCheckbox(String updateReferences, String expectedChecked)
        throws Exception
    {
        AttachmentScriptService attachmentScriptService = mock(AttachmentScriptService.class);
        this.componentManager.registerComponent(ScriptService.class, "attachment", attachmentScriptService);
        when(attachmentScriptService.backlinksCount(any())).thenReturn(10L);

        XWikiDocument document = this.xwiki.getDocument(DOCUMENT_REFERENCE, this.context);
        document.setContent("{{velocity}}\n"
            + "#template('attachment/refactoring_macros.vm')\n"
            + "{{html}}#displayAttachmentLinksCheckbox(){{/html}}\n"
            + "{{/velocity}}");
        document.setSyntax(Syntax.XWIKI_2_1);
        this.xwiki.saveDocument(document, this.context);

        this.request.put("updateReferences", updateReferences);

        Document parse = Jsoup.parse(document.getRenderedContent(this.context));
        // The checkbox is checked by default, unless updateReferences is defined to value different than 'true'.
        assertEquals(expectedChecked, parse.select("[name='updateReferences']").attr("checked"));
        assertEquals("attachment.move.links.hint [10]", parse.select(".xHint").text());
    }

    /**
     * @return the arguments for {@link #displayAttachmentLinksCheckbox(String, String)}
     */
    private static Stream<Arguments> displayAttachmentLinksCheckboxSource()
    {
        return Stream.of(
            Arguments.of(null, "checked"),
            Arguments.of("", ""),
            Arguments.of("false", ""),
            Arguments.of("true", "checked")
        );
    }
}