CVE-2023-36468: Incomplete Cleanup (CWE-459)
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.
CWE description: The product does not properly "clean up" and remove temporary or supporting resources after they have been used.
CVSS v3 base score: 8.8
Published: 2023-06-29T21:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: Java
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 date: 2023-03-15 18:04:46 +0100
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.

Diff: diff --git a/xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/DocumentModelBridge.java b/xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/DocumentModelBridge.java
index a97e911772ca2..3efdce445606f 100644
--- a/xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/DocumentModelBridge.java
+++ b/xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/DocumentModelBridge.java
@@ -177,6 +177,17 @@ default Boolean isHidden()
     @Unstable
     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;
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-display/xwiki-platform-display-api/src/main/java/org/xwiki/display/internal/AbstractDocumentTitleDisplayer.java b/xwiki-platform-core/xwiki-platform-display/xwiki-platform-display-api/src/main/java/org/xwiki/display/internal/AbstractDocumentTitleDisplayer.java
index 46a508a01c9e9..0d2d26ba76cb8 100644
--- a/xwiki-platform-core/xwiki-platform-display/xwiki-platform-display-api/src/main/java/org/xwiki/display/internal/AbstractDocumentTitleDisplayer.java
+++ b/xwiki-platform-core/xwiki-platform-display/xwiki-platform-display-api/src/main/java/org/xwiki/display/internal/AbstractDocumentTitleDisplayer.java
@@ -171,13 +171,15 @@ private XDOM displayTitle(DocumentModelBridge document, DocumentDisplayerParamet
         // 1. Try to use the title provided by the user.
         String rawTitle = document.getTitle();
         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);
             } catch (Exception e) {
                 logger.warn("Failed to interpret title of document [{}].", document.getDocumentReference(), e);
diff --git a/xwiki-platform-core/xwiki-platform-display/xwiki-platform-display-api/src/main/java/org/xwiki/display/internal/DocumentContentAsyncRenderer.java b/xwiki-platform-core/xwiki-platform-display/xwiki-platform-display-api/src/main/java/org/xwiki/display/internal/DocumentContentAsyncRenderer.java
index 0a725fdf541ef..a555bc4f475d4 100644
--- a/xwiki-platform-core/xwiki-platform-display/xwiki-platform-display-api/src/main/java/org/xwiki/display/internal/DocumentContentAsyncRenderer.java
+++ b/xwiki-platform-core/xwiki-platform-display/xwiki-platform-display-api/src/main/java/org/xwiki/display/internal/DocumentContentAsyncRenderer.java
@@ -75,10 +75,15 @@ public class DocumentContentAsyncRenderer extends AbstractBlockAsyncRenderer
      */
     public Set<String> initialize(DocumentModelBridge document, DocumentDisplayerParameters parameters)
     {
         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
             .serialize(parameters.isContentTransformed() && parameters.isTransformationContextIsolated()
                 ? document.getDocumentReference() : this.documentAccessBridge.getCurrentDocumentReference());
diff --git a/xwiki-platform-core/xwiki-platform-display/xwiki-platform-display-api/src/main/java/org/xwiki/display/internal/DocumentTitleDisplayer.java b/xwiki-platform-core/xwiki-platform-display/xwiki-platform-display-api/src/main/java/org/xwiki/display/internal/DocumentTitleDisplayer.java
index dc11f1da4a9a8..a8ed848cc1848 100644
--- a/xwiki-platform-core/xwiki-platform-display/xwiki-platform-display-api/src/main/java/org/xwiki/display/internal/DocumentTitleDisplayer.java
+++ b/xwiki-platform-core/xwiki-platform-display/xwiki-platform-display-api/src/main/java/org/xwiki/display/internal/DocumentTitleDisplayer.java
@@ -75,11 +75,11 @@ protected XDOM extractTitleFromContent(DocumentModelBridge document, DocumentDis
             if (heading.getLevel().getAsInt() <= displayConfiguration.getTitleHeadingDepth()) {
                 XDOM headingXDOM = new XDOM(Collections.<Block> singletonList(heading));
                 try {
                     TransformationContext txContext =
                         new TransformationContext(headingXDOM, document.getSyntax(),
-                                                  parameters.isTransformationContextRestricted());
+                            parameters.isTransformationContextRestricted() || document.isRestricted());
                     txContext.setTargetSyntax(parameters.getTargetSyntax());
                     transformationManager.performTransformations(headingXDOM, txContext);
 
                     Block headingBlock = headingXDOM.getChildren().size() > 0 ? headingXDOM.getChildren().get(0) : null;
                     if (headingBlock instanceof HeaderBlock) {
diff --git a/xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-resources/src/main/resources/flamingo/contentheader.vm b/xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-resources/src/main/resources/flamingo/contentheader.vm
index 1435f9910e523..7cbf038086ab3 100644
--- a/xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-resources/src/main/resources/flamingo/contentheader.vm
+++ b/xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-resources/src/main/resources/flamingo/contentheader.vm
@@ -49,6 +49,13 @@
     <div class="document-menu col-xs-12 col-md-5">
       #template("menus_content.vm")
     </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/>
diff --git a/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 b/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
index c0b54b5fc2d5d..99da14c8569aa 100644
--- a/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
+++ b/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
@@ -37,10 +37,12 @@
 import org.xwiki.test.ui.po.DeletedPageEntry;
 import org.xwiki.test.ui.po.DeletingPage;
 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;
 
 /**
@@ -282,6 +284,36 @@ void viewDeletedPage(TestUtils testUtils, TestReference parentReference)
         assertTrue(deletedTerminalPagesEntries.get(0).canBeDeleted());
 
         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());
+    }
 }
diff --git a/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 b/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
index 28e0eb626f35d..7458112a406c9 100644
--- a/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
+++ b/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
@@ -31,10 +31,12 @@
 import org.xwiki.test.ui.TestUtils;
 import org.xwiki.test.ui.po.HistoryPane;
 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;
 
 /**
  * Verify versioning features of documents and attachments.
@@ -236,6 +238,52 @@ void rollbackAttachmentWithSameNameAndVersion(TestUtils utils, TestReference tes
         assertTrue(attachmentsPane.attachmentExistsByFileName("file.txt"));
         assertEquals("1.1", attachmentsPane.getLatestVersionOfAttachment("file.txt"));
         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());
+    }
+
 }
diff --git a/xwiki-platform-core/xwiki-platform-legacy/xwiki-platform-legacy-oldcore/src/main/aspect/com/xpn/xwiki/doc/XWikiDocumentCompatibilityAspect.aj b/xwiki-platform-core/xwiki-platform-legacy/xwiki-platform-legacy-oldcore/src/main/aspect/com/xpn/xwiki/doc/XWikiDocumentCompatibilityAspect.aj
index 311bdc1f36118..65e795ffa17df 100644
--- a/xwiki-platform-core/xwiki-platform-legacy/xwiki-platform-legacy-oldcore/src/main/aspect/com/xpn/xwiki/doc/XWikiDocumentCompatibilityAspect.aj
+++ b/xwiki-platform-core/xwiki-platform-legacy/xwiki-platform-legacy-oldcore/src/main/aspect/com/xpn/xwiki/doc/XWikiDocumentCompatibilityAspect.aj
@@ -280,11 +280,12 @@ privileged public aspect XWikiDocumentCompatibilityAspect
                     HeaderBlock header = blocks.get(0);
                     if (header.getLevel().compareTo(HeaderLevel.LEVEL2) <= 0) {
                         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
                         Block headerBlock = headerXDOM.getChildren().get(0);
                         if (headerBlock instanceof HeaderBlock) {
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/api/Document.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/api/Document.java
index a2379ecffcb3d..c7626f8607dd2 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/api/Document.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/api/Document.java
@@ -3198,10 +3198,21 @@ public Document getPreviousDocument() throws XWikiException
     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()
     {
         return this.doc.toString();
     }
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/XWikiDocument.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/XWikiDocument.java
index 6b914380d1af5..a0f588b639c26 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/XWikiDocument.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/XWikiDocument.java
@@ -622,10 +622,15 @@ public Set<Entry<DocumentReference, List<BaseObject>>> entrySet()
     /**
      * @see #getOriginalDocument()
      */
     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
      * and other fake documents in tests.
      */
@@ -1335,10 +1340,11 @@ private String display(Syntax targetSyntax, boolean executionContextIsolated, bo
 
                 // Configure display
                 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);
                 parameters.setTargetSyntax(targetSyntax);
 
@@ -1662,10 +1668,11 @@ public String getRenderedContent(String text, Syntax sourceSyntaxId, Syntax targ
 
             // Reuse this document's reference so that the Velocity macro name-space is computed based on it.
             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
             return fakeDocument.display(targetSyntaxId, false, isolated, restrictedTransformationContext,
                 false);
@@ -4581,10 +4588,11 @@ private XWikiDocument cloneInternal(DocumentReference newDocumentReference,
             doc.setValidationScript(getValidationScript());
             doc.setComment(getComment());
             doc.setMinorEdit(isMinorEdit());
             doc.setSyntax(getSyntax());
             doc.setHidden(isHidden());
+            doc.setRestricted(isRestricted());
 
             if (this.xClass != null) {
                 doc.setXClass(this.xClass.clone());
             }
 
@@ -4742,10 +4750,18 @@ public void loadAttachments(XWikiContext context) throws XWikiException
             attachment.loadAttachmentContent(context);
             attachment.loadArchive(context);
         }
     }
 
+    /**
+     * 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)
     {
         // Same Java object, they sure are equal
         if (this == object) {
@@ -9514,6 +9530,34 @@ private void setOriginalMetadataAuthorReference(String serializedUserReference)
     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;
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/internal/filter/output/XWikiDocumentOutputFilterStream.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/internal/filter/output/XWikiDocumentOutputFilterStream.java
index 51b2ed2104617..2b5ad6cf041fc 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/internal/filter/output/XWikiDocumentOutputFilterStream.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/internal/filter/output/XWikiDocumentOutputFilterStream.java
@@ -231,10 +231,14 @@ private void begin(FilterEventParameters parameters) throws FilterException
         } else {
             this.entity.setDocumentReference(documentReference);
             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
         if (this.properties.isAuthorSet()) {
             defaultAuthorDocumentReference = this.properties.getAuthor();
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/XWikiHibernateStore.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/XWikiHibernateStore.java
index 7429b19d16c7a..a1a9500f1728b 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/XWikiHibernateStore.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/XWikiHibernateStore.java
@@ -712,10 +712,13 @@ public void saveXWikiDoc(XWikiDocument doc, XWikiContext inputxcontext, boolean
                         endTransaction(context, true);
                     }
 
                     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 {
                     if (bTransaction) {
                         try {
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/ApplicationResources.properties b/xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/ApplicationResources.properties
index 3fdc7d4328e22..ff9731cc28529 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/ApplicationResources.properties
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/ApplicationResources.properties
@@ -738,10 +738,12 @@ platform.core.profile.watchlist.unwatch=Remove from my watch list
 
 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
 
 core.footnotes.gotofootnote=Go to footnote {0}
 core.footnotes.backtoref=Back to footnote reference
@@ -1683,10 +1685,12 @@ web.history.changes.attachment.content=Content
 web.history.changes.attachment.noContentChanges=Either this is not a text file or the file is too large
 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}
 core.viewers.diff.fromNew=From empty
 core.viewers.diff.to=To version {0}
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/doc/XWikiDocumentMockitoTest.java b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/doc/XWikiDocumentMockitoTest.java
index 97bf9530512a5..d794a195e53b0 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/doc/XWikiDocumentMockitoTest.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/doc/XWikiDocumentMockitoTest.java
@@ -1581,10 +1581,13 @@ void tofromXMLDocument() throws XWikiException
 
         XWikiDocument newDocument = new XWikiDocument(this.document.getDocumentReference());
         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
     void getAttachmentWithNullFilename() throws XWikiException
     {
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/doc/XWikiDocumentRenderingTest.java b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/doc/XWikiDocumentRenderingTest.java
index 6cc9698c4a55a..8b74ca50fcebc 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/doc/XWikiDocumentRenderingTest.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/doc/XWikiDocumentRenderingTest.java
@@ -19,20 +19,23 @@
  */
 package com.xpn.xwiki.doc;
 
 import java.io.Reader;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Locale;
 import java.util.Properties;
 
 import org.apache.velocity.VelocityContext;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.xwiki.configuration.ConfigurationSource;
 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;
 import org.xwiki.velocity.VelocityEngine;
 import org.xwiki.velocity.VelocityFactory;
@@ -45,10 +48,12 @@
 import com.xpn.xwiki.objects.classes.TextAreaClass;
 import com.xpn.xwiki.test.MockitoOldcore;
 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;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.when;
@@ -132,11 +137,18 @@ public void setup() throws Exception
         this.baseObject.setStringListValue("stringlist", Arrays.asList("VALUE1", "VALUE2"));
 
         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
             .registerComponent(ConfigurationSource.class, "xwikicfg", this.oldcore.getConfigurationSource());
     }
@@ -172,10 +184,20 @@ public void getRenderedTitleWhenTitleIsSet()
         // Title with velocity that should be evaluated
         this.document.setTitle("#set($key = \"title\")$key");
         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()
     {
         // Configure XWiki to extract title from content
         this.oldcore.getConfigurationSource().setProperty("xwiki.title.compatibility", "1");
@@ -206,10 +228,23 @@ public void getRenderedTitleInHTMLWhenExtractedFromContent()
 
         this.document.setContent("content not in section\n=== header 3===");
         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()
     {
         this.document.setContent(
             "content not in section\n" + "= **header 1**=\nheader 1 content\n" + "== header 2==\nheader 2 content");
@@ -345,10 +380,50 @@ public void getRenderedContentIsForcingCurrentDocumentAsTheSecurityDocument() th
         // Verifies that a security document is always set,  independently of what was set before the execution of
         // getRenderedContent().
         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()
     {
         this.document.setSyntax(Syntax.XWIKI_1_0);
 
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/test/MockitoOldcore.java b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/test/MockitoOldcore.java
index 739f34bd4e5a3..cdd7f65c3c573 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/test/MockitoOldcore.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/test/MockitoOldcore.java
@@ -627,10 +627,12 @@ public Void answer(InvocationOnMock invocation) throws Throwable
                 }
                 document.setNew(false);
                 document.setStore(getMockStore());
 
                 XWikiDocument savedDocument = document.clone();
+                // Make sure the saved version is not restricted.
+                savedDocument.setRestricted(false);
 
                 documents.put(document.getDocumentReferenceWithLocale(), savedDocument);
 
 
                 // Set the document as it's original document
@@ -830,10 +832,13 @@ public Void answer(InvocationOnMock invocation) throws Throwable
                             spyXWiki.getDocument(document.getDocumentReferenceWithLocale(), getXWikiContext());
                         document.setOriginalDocument(originalDocument);
                     }
 
                     XWikiDocument savedDocument = document.clone();
+                    // Make sure the saved version is not restricted.
+                    savedDocument.setRestricted(false);
+
                     documents.put(document.getDocumentReferenceWithLocale(), savedDocument);
 
                     if (isNew) {
                         if (notifyDocumentCreatedEvent) {
                             getObservationManager().notify(new DocumentCreatedEvent(document.getDocumentReference()),
diff --git a/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 b/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
index 6224278072801..dce64daff0c1c 100644
--- a/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
+++ b/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
@@ -197,11 +197,12 @@ private List<Block> executeContext(XDOM xdom, ContextMacroParameters parameters,
                     // Get the XDOM from the referenced doc but with Transformations applied so that all macro are
                     // executed and contribute XDOM elements.
                     // 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);
                 }
 
                 // Configure the Transformation Context XDOM depending on the mode asked.
                 configuration.setXDOM(referencedXDOM);
diff --git a/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 b/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
index 28227ba5bf72a..0c3ecd9824b48 100644
--- a/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
+++ b/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
@@ -134,15 +134,13 @@ private EntityReference getFullReference(EntityReference reference)
      * @return true if pre-condition are fulfilled.
      */
     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;
     }
 
diff --git a/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 b/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
index b73feabeacadb..4bedd1b299bee 100644
--- a/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
+++ b/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
@@ -17,29 +17,40 @@
  * 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.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}.
  * 
  * @version $Id$
@@ -49,10 +60,13 @@
 class DefaultContextualAuthorizationManagerTest
 {
     @MockComponent
     private AuthorizationManager authorizationManager;
 
+    @MockComponent
+    private RenderingContext renderingContext;
+
     @InjectMockComponents
     private DefaultContextualAuthorizationManager contextualAuthorizationManager;
 
     @InjectMockitoOldcore
     private MockitoOldcore oldcore;
@@ -87,6 +101,40 @@ void hasAccess()
         this.contextualAuthorizationManager.hasAccess(Right.VIEW, localReference);
 
         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);
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-test/xwiki-platform-test-ui/src/main/java/org/xwiki/test/ui/po/HistoryPane.java b/xwiki-platform-core/xwiki-platform-test/xwiki-platform-test-ui/src/main/java/org/xwiki/test/ui/po/HistoryPane.java
index cf3a17e167eb1..62e438527f715 100644
--- a/xwiki-platform-core/xwiki-platform-test/xwiki-platform-test-ui/src/main/java/org/xwiki/test/ui/po/HistoryPane.java
+++ b/xwiki-platform-core/xwiki-platform-test/xwiki-platform-test-ui/src/main/java/org/xwiki/test/ui/po/HistoryPane.java
@@ -114,10 +114,25 @@ public ViewPage rollbackToVersion(String version)
         // own wait.
 
         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);
 
         pane.findElement(
diff --git a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/changesdoc.vm b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/changesdoc.vm
index 8ec4904edeb5d..837f5e5c2ebbb 100644
--- a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/changesdoc.vm
+++ b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/changesdoc.vm
@@ -134,10 +134,14 @@
         #elseif ("$!htmlDiff" == '')
           <div class="errormessage">
             $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
     </div>
   </div>

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