CVE-2023-36468 (Java, published 2023-06-29T21:15Z)
Description: value: XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. When an XWiki installation is upgraded and that upgrade contains a fix for a bug in a document, just a new version of that document is added. In some cases, it's still possible to exploit the vulnerability that was fixed in the new version. The severity of this depends on the fixed vulnerability, for the purpose of this advisory take CVE-2022-36100/GHSA-2g5c-228j-p52x as example - it is easily exploitable with just view rights and critical. When XWiki is upgraded from a version before the fix for it (e.g., 14.3) to a version including the fix (e.g., 14.4), the vulnerability can still be reproduced by adding `rev=1.1` to the URL used in the reproduction steps so remote code execution is possible even after upgrading. Therefore, this affects the confidentiality, integrity and availability of the whole XWiki installation. This vulnerability also affects manually added script macros that contained security vulnerabilities that were later fixed by changing the script macro without deleting the versions with the security vulnerability from the history. This vulnerability doesn't affect freshly installed versions of XWiki. Further, this vulnerability doesn't affect content that is only loaded from the current version of a document like the code of wiki macros or UI extensions. This vulnerability has been patched in XWiki 14.10.7 and 15.2RC1 by forcing old revisions to be executed in a restricted mode that disables all script macros. As a workaround, admins can manually delete old revisions of affected documents. A script could be used to identify all installed documents and delete the history for them. However, also manually added and later corrected code may be affected by this vulnerability so it is easy to miss documents.
Severity: nan (CVSS v3 base score: 8.8)
Weakness: CWE-459 \u2014 Incomplete Cleanup
The product does not properly "clean up" and remove temporary or supporting resources after they have been used.
Repository: https://github.com/xwiki/xwiki-platform
Files changed:
xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/DocumentModelBridge.java
xwiki-platform-core/xwiki-platform-display/xwiki-platform-display-api/src/main/java/org/xwiki/display/internal/AbstractDocumentTitleDisplayer.java
xwiki-platform-core/xwiki-platform-display/xwiki-platform-display-api/src/main/java/org/xwiki/display/internal/DocumentContentAsyncRenderer.java
xwiki-platform-core/xwiki-platform-display/xwiki-platform-display-api/src/main/java/org/xwiki/display/internal/DocumentTitleDisplayer.java
xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-resources/src/main/resources/flamingo/contentheader.vm
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/RecycleBinIT.java
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/VersionIT.java
xwiki-platform-core/xwiki-platform-legacy/xwiki-platform-legacy-oldcore/src/main/aspect/com/xpn/xwiki/doc/XWikiDocumentCompatibilityAspect.aj
xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/api/Document.java
xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/XWikiDocument.java
xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/internal/filter/output/XWikiDocumentOutputFilterStream.java
xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/XWikiHibernateStore.java
xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/ApplicationResources.properties
xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/doc/XWikiDocumentMockitoTest.java
xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/doc/XWikiDocumentRenderingTest.java
xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/test/MockitoOldcore.java
xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-macros/xwiki-platform-rendering-macro-context/src/main/java/org/xwiki/rendering/internal/macro/context/ContextMacro.java
xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authorization/xwiki-platform-security-authorization-bridge/src/main/java/org/xwiki/security/authorization/internal/DefaultContextualAuthorizationManager.java
xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authorization/xwiki-platform-security-authorization-bridge/src/test/java/org/xwiki/security/authorization/internal/DefaultContextualAuthorizationManagerTest.java
xwiki-platform-core/xwiki-platform-test/xwiki-platform-test-ui/src/main/java/org/xwiki/test/ui/po/HistoryPane.java
xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/changesdoc.vm
Commit message: XWIKI-20594: Mark old revisions and deleted documents as restricted

* Introduce a new "restricted" attribute on XWikiDocument
* Mark any document that is restored from XML as restricted
* Deny script right when the secure document is restricted
* Make transformations restricted when the document is restricted
* Do not execute Velocity in titles when the document is restricted
* Make sure saved documents aren't restricted.
* Add tests
* Display a warning when a document is rendered in restricted mode and
  the user is advanced or there is actually an error in the output.


Vulnerable code:
                // Evaluate the title only if the document has script rights, otherwise use the raw title.
                if (authorizationManager.hasAccess(Right.SCRIPT, document.getContentAuthorReference(),
                    document.getDocumentReference())) {
                                                  parameters.isTransformationContextRestricted());
                        TransformationContext context = new TransformationContext(headerXDOM, getSyntax());
        when(this.oldcore.getMockContextualAuthorizationManager().hasAccess(any())).thenReturn(true);
                        new TransformationContext(referencedXDOM, referencedDoc.getSyntax());
            if (this.renderingContext.isRestricted()) {
                return false;
            } else if (right == Right.PROGRAM && this.xcontextProvider.get().hasDroppedPermissions()) {
                return false;
            }

Fixed code:

    /**
     * @return {@code true} if the document is restricted, i.e., transformations should be executed in restricted mode
     * @since 15.2RC1
     * @since 14.10.7
     */
    @Unstable
    default boolean isRestricted()
    {
        return false;
    }
                // Evaluate the title only if the document is not restricted and its content's author has script
                // right, otherwise use the raw title.
                if (!document.isRestricted() && this.authorizationManager.hasAccess(Right.SCRIPT,
                    document.getContentAuthorReference(), document.getDocumentReference()))
                {
        // Make sure the restricted property of the document is properly taken into account.
        if (document.isRestricted()) {
            parameters.setTransformationContextRestricted(true);
        }

                            parameters.isTransformationContextRestricted() || document.isRestricted());
## Display a warning regarding restricted mode for advanced users or when there is an actual error to not to confuse
## simple users. Also, only display the warning for the content viewer as other viewers shouldn't display
## rendered content of old revisions and this would thus only confuse users.
#if ($viewer == 'content' && $doc.isRestricted()
  && ($isAdvancedUser || $stringtool.contains($renderedContent, 'xwikirenderingerror')))
  #warning($escapetool.xml($services.localization.render('core.document.restrictedInfo')))
#end
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.startsWith;

    @Test
    @Order(3)
    void deletedDocumentIsRestricted(TestUtils testUtils, TestReference testReference)
    {
        testUtils.loginAsSuperAdmin();

        ViewPage viewPage = testUtils.createPage(testReference, "{{velocity}}Velocity content{{/velocity}}", "Title");
        // Make sure the Velocity macro is executed normally.
        assertEquals("Velocity content", viewPage.getContent());

        viewPage.deletePage().confirmDeletePage().waitUntilFinished();

        testUtils.gotoPage(testReference);
        DeletePageOutcomePage recycleBinPage = new DeletePageOutcomePage();

        List<DeletedPageEntry> deletedPageEntries = recycleBinPage.getDeletedPagesEntries();
        assertFalse(deletedPageEntries.isEmpty());
        viewPage = deletedPageEntries.get(0).clickView();

        assertEquals("Title", viewPage.getDocumentTitle());
        // In the preview the Velocity macro should be forbidden.
        assertThat(viewPage.getContent(), startsWith("Failed to execute the [velocity] macro."));

        testUtils.gotoPage(testReference);
        recycleBinPage = new DeletePageOutcomePage();
        viewPage = recycleBinPage.getDeletedPagesEntries().get(0).clickRestore();
        // Assert that in the restored document, scripts are allowed again.
        assertEquals("Velocity content", viewPage.getContent());
    }
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.startsWith;

    @Test
    @Order(5)
    void oldRevisionsAreRestricted(TestUtils utils, TestReference testReference) throws Exception
    {
        utils.loginAsSuperAdmin();

        utils.rest().delete(testReference);

        // Create first version of the page
        ViewPage vp = utils.createPage(testReference, "{{velocity}}" + CONTENT1 + "{{/velocity}}", TITLE);
        assertEquals(CONTENT1, vp.getContent());

        // Adds second version
        WikiEditPage wikiEditPage = vp.editWiki();
        wikiEditPage.setContent("{{velocity}}" + CONTENT2 + "{{velocity}}");
        vp = wikiEditPage.clickSaveAndView();

        assertEquals(CONTENT2, vp.getContent());

        // TODO: Remove when XWIKI-6688 (Possible race condition when clicking on a tab at the bottom of a page in
        // view mode) is fixed.
        vp.waitForDocExtraPaneActive("comments");

        HistoryPane historyTab = vp.openHistoryDocExtraPane();
        vp = historyTab.viewVersion("1.1");

        // In the preview the Velocity macro should be forbidden.
        assertThat(vp.getContent(), startsWith("Failed to execute the [velocity] macro."));

        // TODO: Remove when XWIKI-6688 (Possible race condition when clicking on a tab at the bottom of a page in
        // view mode) is fixed.
        vp.waitForDocExtraPaneActive("comments");

        historyTab = vp.openHistoryDocExtraPane();
        vp = historyTab.rollbackToVersion("1.1");

        // Rollback doesn't wait...
        // Wait for the comment tab to be selected since we're currently on the history tab and rolling
        // back is going to load a new page and make the focus active on the comments tab.
        vp.waitForDocExtraPaneActive("comments");

        // Assert that scripts are executed again after restoring the version.
        assertEquals(CONTENT1, vp.getContent());
    }

                        TransformationContext context =
                            new TransformationContext(headerXDOM, getSyntax(), isRestricted());
    /**
     * @return if rendering transformations shall be executed in restricted mode and the title not be executed
     * @since 14.10.7
     * @since 15.2RC1
     */
    @Unstable
    public boolean isRestricted()
    {
        return this.doc.isRestricted();
    }

    /**
     * If the document should always be rendered in restricted mode.
     */
    private boolean restricted;

                // Don't consider isRestricted() here as this could invoke a sheet.
            fakeDocument.setRestricted(sDocument != null && sDocument.isRestricted());
            doc.setRestricted(isRestricted());
    /**
     * Indicates whether some other document is "equal to" this one.
     * <p>
     * This ignores the {@link #isRestricted()} property as it is not considered to be part of the data.
     *
     * @param object the document to compare to
     * @return {@code true} if this document is the same as the object argument; {@code false} otherwise
     */

    /**
     * @return if rendering transformations shall be executed in restricted mode and the title not be executed
     * @since 14.10.7
     * @since 15.2RC1
     */
    @Override
    @Unstable
    public boolean isRestricted()
    {
        return this.restricted;
    }

    /**
     * Set the restricted property that disables scripts and other dangerous content.
     * <p>
     * This property is not stored in the database as it is only supposed to be {@code true} on documents that do not
     * correspond to the current version of the document.
     *
     * @param restricted if rendering transformations shall be executed in restricted mode and the title not be executed
     * @since 14.10.7
     * @since 15.2RC1
     */
    @Unstable
    public void setRestricted(boolean restricted)
    {
        this.restricted = restricted;
    }
        // Mark the document as restricted to avoid that any scripts are executed as scripts should only be executed
        // on the current, saved version, see https://jira.xwiki.org/browse/XWIKI-20594
        this.entity.setRestricted(true);

                    // Make sure that properly saved documents aren't restricted.
                    doc.setRestricted(false);

core.document.restrictedInfo=For security reasons, the document is displayed in restricted mode as it is not the \
  current version. There may be differences and errors due to this.
web.history.changes.restrictedInfo=For security reasons, at least one of the compared documents is displayed in \
  restricted mode as it is not the current version. There may be differences and errors due to this.
        // Assert that the document restored from XML is restricted in contrast to the original document.
        assertFalse(this.document.isRestricted());
        assertTrue(newDocument.isRestricted());
import java.util.List;
import org.xwiki.rendering.transformation.RenderingContext;
import org.xwiki.security.authorization.Right;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.startsWith;
        when(this.oldcore.getMockContextualAuthorizationManager().hasAccess(any())).thenAnswer(invocationOnMock -> {
            if (List.of(Right.SCRIPT, Right.PROGRAM).contains(invocationOnMock.getArgument(0))) {
                RenderingContext renderingContext = this.oldcore.getMocker().getInstance(RenderingContext.class);
                return !renderingContext.isRestricted();
            } else {
                return true;
            }
        });
    @Test
    void getRenderedTitleRestricted()
    {
        this.document.setRestricted(true);
        // Title with velocity that shouldn't be evaluated
        String title = "#set($key = \"title\")$key";
        this.document.setTitle(title);
        assertEquals(title, this.document.getRenderedTitle(Syntax.XHTML_1_0, this.oldcore.getXWikiContext()));
    }

    @Test
    void getRenderedTitleWhenRestricted()
    {
        // Configure XWiki to extract title from content
        this.oldcore.getConfigurationSource().setProperty("xwiki.title.compatibility", "1");
        this.document.setRestricted(true);

        this.document.setContent("content not in section\n"
            + "= {{groovy}}print \"value\"{{/groovy}}=\nheader 1 content\n" + "== header 2==\nheader 2 content");
        assertThat(this.document.getRenderedTitle(Syntax.XHTML_1_0, this.oldcore.getXWikiContext()),
            startsWith("<span class=\"xwikirenderingerror\">Failed to execute the [groovy] macro."));
    }

    @Test
    void getRenderedContentSetsRestrictedRendering() throws Exception
    {
        XWikiDocument otherDocument = new XWikiDocument(new DocumentReference("otherwiki", "otherspace", "otherpage"));
        otherDocument.setContentAuthorReference(new DocumentReference("otherwiki", "XWiki", "othercontentauthor"));
        XWikiDocument sdoc = new XWikiDocument(new DocumentReference("callerwiki", "callerspace", "callerpage"));
        Document apiDocument = this.document.newDocument(this.oldcore.getXWikiContext());

        String content = "{{velocity}}test{{/velocity}}";

        this.document.setRestricted(true);
        this.document.setContent(content);
        this.oldcore.getXWikiContext().setDoc(null);

        // Verify that the Velocity macro is not executed.
        assertThat(this.document.getRenderedContent(this.oldcore.getXWikiContext()),
            startsWith("<div class=\"xwikirenderingerror\">Failed to execute the [velocity] macro."));

        this.document.setRestricted(false);

        assertEquals("<p>test</p>", this.document.getRenderedContent(this.oldcore.getXWikiContext()));

        this.oldcore.getXWikiContext().setDoc(otherDocument);

        assertEquals("<p>test</p>", apiDocument.getRenderedContent(content, Syntax.XWIKI_2_1.toIdString()));

        otherDocument.setRestricted(true);

        assertThat(apiDocument.getRenderedContent(content, Syntax.XWIKI_2_1.toIdString()),
            startsWith("<div class=\"xwikirenderingerror\">Failed to execute the [velocity] macro."));

        this.oldcore.getXWikiContext().put("sdoc", sdoc);
        assertEquals("<p>test</p>", apiDocument.getRenderedContent(content, Syntax.XWIKI_2_1.toIdString()));

        sdoc.setRestricted(true);

        assertThat(apiDocument.getRenderedContent(content, Syntax.XWIKI_2_1.toIdString()),
            startsWith("<div class=\"xwikirenderingerror\">Failed to execute the [velocity] macro."));
    }

                // Make sure the saved version is not restricted.
                savedDocument.setRestricted(false);
                    // Make sure the saved version is not restricted.
                    savedDocument.setRestricted(false);

                        new TransformationContext(referencedXDOM, referencedDoc.getSyntax(),
                            referencedDoc.isRestricted());
            XWikiDocument doc = getProgrammingDocument();
            boolean restricted = this.renderingContext.isRestricted() || (doc != null && doc.isRestricted());
            return !(restricted || (right == Right.PROGRAM && this.xcontextProvider.get().hasDroppedPermissions()));
import java.util.stream.Stream;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.xwiki.rendering.transformation.RenderingContext;
import com.xpn.xwiki.doc.XWikiDocument;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
    @MockComponent
    private RenderingContext renderingContext;


    @ParameterizedTest
    @MethodSource("contentRightsSource")
    void contentAuthorRightPreAccess(Right right)
    {
        when(this.authorizationManager.hasAccess(eq(right), any(), any())).thenReturn(true);

        assertTrue(this.contextualAuthorizationManager.hasAccess(right));

        // Check restricted rendering context (once).
        when(this.renderingContext.isRestricted()).thenReturn(true).thenReturn(false);
        assertFalse(this.contextualAuthorizationManager.hasAccess(right));

        XWikiDocument contextDocument = mock(XWikiDocument.class);
        this.oldcore.getXWikiContext().setDoc(contextDocument);
        assertTrue(this.contextualAuthorizationManager.hasAccess(right));
        verify(contextDocument).isRestricted();
        // Check restricted document denies script right.
        when(contextDocument.isRestricted()).thenReturn(true).thenReturn(false);
        assertFalse(this.contextualAuthorizationManager.hasAccess(right));

        // Check dropping permissions keeps script but not programming right
        this.oldcore.getXWikiContext().dropPermissions();
        if (right == Right.PROGRAM) {
            assertFalse(this.contextualAuthorizationManager.hasAccess(right));
        } else {
            assertTrue(this.contextualAuthorizationManager.hasAccess(right));
        }
    }

    static Stream<Right> contentRightsSource()
    {
        return Stream.of(Right.SCRIPT, Right.PROGRAM);
    }
    /**
     * View the document at the given version.
     *
     * @param version the version to view
     * @return the viewpage
     * @since 14.10.7
     * @since 15.2RC1
     */
    public ViewPage viewVersion(String version)
    {
        this.pane.findElement(By.xpath(".//table//tr//td[position()=3]/a[contains(., '" + version + "')]")).click();

        return new ViewPage();
    }

          #if (($origdoc.isRestricted() || $newdoc.isRestricted()) && ($services.user.allProperties.type == 'ADVANCED'
            || $stringtool.contains($htmlDiff, 'xwikirenderingerror')))
            #warning($escapetool.xml($services.localization.render('web.history.changes.restrictedInfo')))
          #end