CVE-2023-36468 (published 2023-06-29T21:15Z)

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.

CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-459
cwe_name: Incomplete Cleanup
description: The product does not properly "clean up" and remove temporary or supporting resources after they have been used.
extended_description: 
            
            
         
         

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-10 17:19:00+01:00
msg: 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.
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: DocumentModelBridge.java
change_type: ModificationType.MODIFY
diff: @@ -179,4 +179,15 @@ default Date getDate()
     {
         return null;
     }
+
+    /**
+     * @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;
+    }
 }

programming_language: Java
filename: AbstractDocumentTitleDisplayer.java
change_type: ModificationType.MODIFY
diff: @@ -173,9 +173,11 @@ private XDOM displayTitle(DocumentModelBridge document, DocumentDisplayerParamet
         if (!StringUtils.isEmpty(rawTitle)) {
             try {
                 String title = rawTitle;
-                // Evaluate the title only if the document has script rights, otherwise use the raw title.
-                if (authorizationManager.hasAccess(Right.SCRIPT, document.getContentAuthorReference(),
-                    document.getDocumentReference())) {
+                // 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()))
+                {
                     title = evaluateTitle(rawTitle, document, parameters);
                 }
                 return parseTitle(title);

programming_language: Java
filename: DocumentContentAsyncRenderer.java
change_type: ModificationType.MODIFY
diff: @@ -77,6 +77,11 @@ public Set<String> initialize(DocumentModelBridge document, DocumentDisplayerPar
     {
         this.parameters = parameters;
 
+        // Make sure the restricted property of the document is properly taken into account.
+        if (document.isRestricted()) {
+            parameters.setTransformationContextRestricted(true);
+        }
+
         this.asyncProperties = this.asyncParser.getAsyncProperties(document);
 
         String transformationId = this.defaultEntityReferenceSerializer

programming_language: Java
filename: DocumentTitleDisplayer.java
change_type: ModificationType.MODIFY
diff: @@ -77,7 +77,7 @@ protected XDOM extractTitleFromContent(DocumentModelBridge document, DocumentDis
                 try {
                     TransformationContext txContext =
                         new TransformationContext(headingXDOM, document.getSyntax(),
-                                                  parameters.isTransformationContextRestricted());
+                            parameters.isTransformationContextRestricted() || document.isRestricted());
                     txContext.setTargetSyntax(parameters.getTargetSyntax());
                     transformationManager.performTransformations(headingXDOM, txContext);
 

programming_language: Java
filename: contentheader.vm
change_type: ModificationType.MODIFY
diff: @@ -51,4 +51,11 @@
     </div>
   #end
 </div>
+## 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
 <hr/>

programming_language: Perl
filename: RecycleBinIT.java
change_type: ModificationType.MODIFY
diff: @@ -39,6 +39,8 @@
 import org.xwiki.test.ui.po.HistoryPane;
 import org.xwiki.test.ui.po.ViewPage;
 
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.startsWith;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -284,4 +286,34 @@ void viewDeletedPage(TestUtils testUtils, TestReference parentReference)
         assertTrue(deletedPagesEntries.get(0).canBeRestored());
         assertTrue(deletedTerminalPagesEntries.get(0).canBeRestored());
     }
+
+    @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());
+    }
 }

programming_language: Java
filename: VersionIT.java
change_type: ModificationType.MODIFY
diff: @@ -33,6 +33,8 @@
 import org.xwiki.test.ui.po.ViewPage;
 import org.xwiki.test.ui.po.editor.WikiEditPage;
 
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.startsWith;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -238,4 +240,50 @@ void rollbackAttachmentWithSameNameAndVersion(TestUtils utils, TestReference tes
         attachmentsPane.getAttachmentLink("file.txt").click();
         assertEquals("1", utils.getDriver().findElement(By.tagName("html")).getText());
     }
+
+    @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());
+    }
+
 }

programming_language: Java
filename: XWikiDocumentCompatibilityAspect.aj
change_type: ModificationType.MODIFY
diff: @@ -282,7 +282,8 @@ privileged public aspect XWikiDocumentCompatibilityAspect
                         XDOM headerXDOM = new XDOM(Collections.<Block> singletonList(header));
 
                         // transform
-                        TransformationContext context = new TransformationContext(headerXDOM, getSyntax());
+                        TransformationContext context =
+                            new TransformationContext(headerXDOM, getSyntax(), isRestricted());
                         Utils.getComponent(TransformationManager.class).performTransformations(headerXDOM, context);
 
                         // render

programming_language: Java
filename: Document.java
change_type: ModificationType.MODIFY
diff: @@ -3200,6 +3200,17 @@ public boolean isMostRecent()
         return this.doc.isMostRecent();
     }
 
+    /**
+     * @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();
+    }
+
     @Override
     public String toString()
     {

programming_language: Java
filename: XWikiDocument.java
change_type: ModificationType.MODIFY
diff: @@ -624,6 +624,11 @@ public Set<Entry<DocumentReference, List<BaseObject>>> entrySet()
      */
     private XWikiDocument originalDocument;
 
+    /**
+     * If the document should always be rendered in restricted mode.
+     */
+    private boolean restricted;
+
     /**
      * Used to display the title and the content of this document. Do not inject the component here to avoid any simple
      * new XWikiDocument to cause many useless initialization, in particular, during initialization of the stub context
@@ -1337,6 +1342,7 @@ private String display(Syntax targetSyntax, boolean executionContextIsolated, bo
                 DocumentDisplayerParameters parameters = new DocumentDisplayerParameters();
                 parameters.setExecutionContextIsolated(executionContextIsolated);
                 parameters.setTransformationContextIsolated(transformationContextIsolated);
+                // Don't consider isRestricted() here as this could invoke a sheet.
                 parameters.setTransformationContextRestricted(transformationContextRestricted);
                 // Render the translated content (matching the current language) using this document's syntax.
                 parameters.setContentTranslated(tdoc != this);
@@ -1664,6 +1670,7 @@ public String getRenderedContent(String text, Syntax sourceSyntaxId, Syntax targ
             XWikiDocument fakeDocument = new XWikiDocument(getDocumentReference());
             fakeDocument.setSyntax(sourceSyntaxId);
             fakeDocument.setContent(text);
+            fakeDocument.setRestricted(sDocument != null && sDocument.isRestricted());
 
             // We don't let displayer take care of the context isolation because we don't want the fake document to be
             // context document
@@ -4583,6 +4590,7 @@ private XWikiDocument cloneInternal(DocumentReference newDocumentReference,
             doc.setMinorEdit(isMinorEdit());
             doc.setSyntax(getSyntax());
             doc.setHidden(isHidden());
+            doc.setRestricted(isRestricted());
 
             if (this.xClass != null) {
                 doc.setXClass(this.xClass.clone());
@@ -4744,6 +4752,14 @@ public void loadAttachments(XWikiContext context) throws XWikiException
         }
     }
 
+    /**
+     * 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
+     */
     @Override
     public boolean equals(Object object)
     {
@@ -9516,4 +9532,32 @@ public void initialize()
         // There is no syntax by default in a new document and the default one is retrieved from the configuration
         setSyntax(getSyntax());
     }
+
+    /**
+     * @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;
+    }
 }

programming_language: Java
filename: XWikiDocumentOutputFilterStream.java
change_type: ModificationType.MODIFY
diff: @@ -233,6 +233,10 @@ private void begin(FilterEventParameters parameters) throws FilterException
             this.entity.setLocale(this.currentLocale);
         }
 
+        // 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);
+
         // Find default author
         DocumentReference defaultAuthorDocumentReference;
         // TODO: move to UserReference based APIs in DocumentInstanceOutputProperties

programming_language: Java
filename: XWikiHibernateStore.java
change_type: ModificationType.MODIFY
diff: @@ -714,6 +714,9 @@ public void saveXWikiDoc(XWikiDocument doc, XWikiContext inputxcontext, boolean
 
                     doc.setNew(false);
 
+                    // Make sure that properly saved documents aren't restricted.
+                    doc.setRestricted(false);
+
                     // We need to ensure that the saved document becomes the original document
                     doc.setOriginalDocument(doc.clone());
                 } finally {

programming_language: Java
filename: ApplicationResources.properties
change_type: ModificationType.MODIFY
diff: @@ -740,6 +740,8 @@ core.footer.creation=Created by {0} on {1}
 core.footer.translationCreation=Translated into {0} by {1} on {2}
 core.footer.modification=Last modified by {0} on {1}
 core.document.modificationWithVersion=Version {0} by {1} on {2}
+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.
 
 core.document.error.failedParse=Failed to parse document content
 
@@ -1685,6 +1687,8 @@ web.history.changes.privateInformation=Private information
 web.history.changes.attachment.notAvailable=The content diff is not available. One attachment might have been deleted from the recycle bin.
 web.history.changes.showContext=Show context
 web.history.changes.hideContext=Hide context
+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.
 
 core.viewers.diff.title=Changes for page <a href="{1}">{0}</a>
 core.viewers.diff.from=From version {0}

programming_language: Shell
filename: XWikiDocumentMockitoTest.java
change_type: ModificationType.MODIFY
diff: @@ -1583,6 +1583,9 @@ void tofromXMLDocument() throws XWikiException
         newDocument.fromXML(document, false);
 
         assertEquals(this.document, newDocument);
+        // Assert that the document restored from XML is restricted in contrast to the original document.
+        assertFalse(this.document.isRestricted());
+        assertTrue(newDocument.isRestricted());
     }
 
     @Test

programming_language: Java
filename: XWikiDocumentRenderingTest.java
change_type: ModificationType.MODIFY
diff: @@ -21,6 +21,7 @@
 
 import java.io.Reader;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Locale;
 import java.util.Properties;
 
@@ -31,6 +32,8 @@
 import org.xwiki.display.internal.DisplayConfiguration;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.rendering.syntax.Syntax;
+import org.xwiki.rendering.transformation.RenderingContext;
+import org.xwiki.security.authorization.Right;
 import org.xwiki.test.annotation.AllComponents;
 import org.xwiki.test.junit5.mockito.InjectComponentManager;
 import org.xwiki.test.mockito.MockitoComponentManager;
@@ -47,6 +50,8 @@
 import com.xpn.xwiki.test.junit5.mockito.InjectMockitoOldcore;
 import com.xpn.xwiki.test.junit5.mockito.OldcoreTest;
 
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.startsWith;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
@@ -134,7 +139,14 @@ public void setup() throws Exception
         this.oldcore.getXWikiContext().put("isInRenderingEngine", true);
 
         when(this.oldcore.getMockAuthorizationManager().hasAccess(any(), any(), any())).thenReturn(true);
-        when(this.oldcore.getMockContextualAuthorizationManager().hasAccess(any())).thenReturn(true);
+        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;
+            }
+        });
         when(this.xwiki.getRightService().hasProgrammingRights(any())).thenReturn(true);
 
         this.componentManager
@@ -174,6 +186,16 @@ public void getRenderedTitleWhenTitleIsSet()
         assertEquals("title", this.document.getRenderedTitle(Syntax.XHTML_1_0, this.oldcore.getXWikiContext()));
     }
 
+    @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
     public void getRenderedTitleInHTMLWhenExtractedFromContent()
     {
@@ -208,6 +230,19 @@ public void getRenderedTitleInHTMLWhenExtractedFromContent()
         assertEquals("Page", 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
     public void getRenderedTitleInPlainWhenExtractedFromContent()
     {
@@ -347,6 +382,46 @@ public void getRenderedContentIsForcingCurrentDocumentAsTheSecurityDocument() th
         assertEquals("<p>Space.Page</p>", this.document.getRenderedContent(this.oldcore.getXWikiContext()));
     }
 
+    @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."));
+    }
+
     @Test
     public void getRenderedContentTextWithSourceSyntaxSpecified()
     {

programming_language: Java
filename: MockitoOldcore.java
change_type: ModificationType.MODIFY
diff: @@ -629,6 +629,8 @@ public Void answer(InvocationOnMock invocation) throws Throwable
                 document.setStore(getMockStore());
 
                 XWikiDocument savedDocument = document.clone();
+                // Make sure the saved version is not restricted.
+                savedDocument.setRestricted(false);
 
                 documents.put(document.getDocumentReferenceWithLocale(), savedDocument);
 
@@ -832,6 +834,9 @@ public Void answer(InvocationOnMock invocation) throws Throwable
                     }
 
                     XWikiDocument savedDocument = document.clone();
+                    // Make sure the saved version is not restricted.
+                    savedDocument.setRestricted(false);
+
                     documents.put(document.getDocumentReferenceWithLocale(), savedDocument);
 
                     if (isNew) {

programming_language: Java
filename: ContextMacro.java
change_type: ModificationType.MODIFY
diff: @@ -199,7 +199,8 @@ private List<Block> executeContext(XDOM xdom, ContextMacroParameters parameters,
                     // IMPORTANT: This can be dangerous since it means executing macros, and thus also script macros
                     // defined in the referenced document. To be used with caution.
                     TransformationContext referencedTxContext =
-                        new TransformationContext(referencedXDOM, referencedDoc.getSyntax());
+                        new TransformationContext(referencedXDOM, referencedDoc.getSyntax(),
+                            referencedDoc.isRestricted());
                     this.transformationManager.performTransformations(referencedXDOM, referencedTxContext);
                 }
 

programming_language: Java
filename: DefaultContextualAuthorizationManager.java
change_type: ModificationType.MODIFY
diff: @@ -136,11 +136,9 @@ private EntityReference getFullReference(EntityReference reference)
     private boolean checkPreAccess(Right right)
     {
         if (CONTENT_AUTHOR_RIGHTS.contains(right)) {
-            if (this.renderingContext.isRestricted()) {
-                return false;
-            } else if (right == Right.PROGRAM && this.xcontextProvider.get().hasDroppedPermissions()) {
-                return false;
-            }
+            XWikiDocument doc = getProgrammingDocument();
+            boolean restricted = this.renderingContext.isRestricted() || (doc != null && doc.isRestricted());
+            return !(restricted || (right == Right.PROGRAM && this.xcontextProvider.get().hasDroppedPermissions()));
         }
 
         return true;

programming_language: Java
filename: DefaultContextualAuthorizationManagerTest.java
change_type: ModificationType.MODIFY
diff: @@ -19,25 +19,36 @@
  */
 package org.xwiki.security.authorization.internal;
 
+import java.util.stream.Stream;
+
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.model.reference.LocalDocumentReference;
 import org.xwiki.model.reference.WikiReference;
+import org.xwiki.rendering.transformation.RenderingContext;
 import org.xwiki.security.authorization.AuthorizationManager;
 import org.xwiki.security.authorization.Right;
 import org.xwiki.test.junit5.mockito.InjectMockComponents;
 import org.xwiki.test.junit5.mockito.MockComponent;
 
+import com.xpn.xwiki.doc.XWikiDocument;
 import com.xpn.xwiki.test.MockitoOldcore;
 import com.xpn.xwiki.test.junit5.mockito.InjectMockitoOldcore;
 import com.xpn.xwiki.test.junit5.mockito.OldcoreTest;
 import com.xpn.xwiki.test.reference.ReferenceComponentList;
 
+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.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 /**
  * Validate {@link DefaultContextualAuthorizationManager}.
@@ -51,6 +62,9 @@
     @MockComponent
     private AuthorizationManager authorizationManager;
 
+    @MockComponent
+    private RenderingContext renderingContext;
+
     @InjectMockComponents
     private DefaultContextualAuthorizationManager contextualAuthorizationManager;
 
@@ -89,4 +103,38 @@ void hasAccess()
         verify(this.authorizationManager).hasAccess(same(Right.VIEW), isNull(),
             eq(new DocumentReference(localReference, this.currentWikiReference)));
     }
+
+    @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);
+    }
 }

programming_language: Java
filename: HistoryPane.java
change_type: ModificationType.MODIFY
diff: @@ -116,6 +116,21 @@ public ViewPage rollbackToVersion(String version)
         return new ViewPage();
     }
 
+    /**
+     * 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();
+    }
+
     public HistoryPane deleteVersion(String version)
     {
         getDriver().makeConfirmDialogSilent(true);

programming_language: Java
filename: changesdoc.vm
change_type: ModificationType.MODIFY
diff: @@ -136,6 +136,10 @@
             $escapetool.xml($services.localization.render('web.history.changes.failedToCompute'))
           </div>
         #else
+          #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
           $htmlDiff
         #end
       #end

programming_language: CoffeeScript