Velocity execution without script rights in Xwiki platform
Description
XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. It is possible in XWiki to execute Velocity code without having script right by creating an XClass with a property of type "TextArea" and content type "VelocityCode" or "VelocityWiki". For the former, the syntax of the document needs to be set the xwiki/1.0 (this syntax doesn't need to be installed). In both cases, when adding the property to an object, the Velocity code is executed regardless of the rights of the author of the property (edit right is still required, though). In both cases, the code is executed with the correct context author so no privileged APIs can be accessed. However, Velocity still grants access to otherwise inaccessible data and APIs that could allow further privilege escalation. At least for "VelocityCode", this behavior is most likely very old but only since XWiki 7.2, script right is a separate right, before that version all users were allowed to execute Velocity and thus this was expected and not a security issue. This has been patched in XWiki 14.10.10 and 15.4 RC1. Users are advised to upgrade. There are no known workarounds.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.xwiki.platform:xwiki-platform-oldcoreMaven | >= 7.2, < 14.10.10 | 14.10.10 |
org.xwiki.platform:xwiki-platform-oldcoreMaven | >= 15.0-rc-1, < 15.4-rc-1 | 15.4-rc-1 |
Affected products
1- Range: >= 7.2, < 14.10.10
Patches
1edc52579eeaaXWIKI-20847/XWIKI-20848: Improve Velocity execution in TextAreaClass
2 files changed · +345 −27
xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/objects/classes/TextAreaClass.java+102 −27 modified@@ -34,7 +34,11 @@ import org.xwiki.edit.EditorManager; import org.xwiki.rendering.syntax.Syntax; import org.xwiki.rendering.syntax.SyntaxContent; +import org.xwiki.security.authorization.AuthorExecutor; +import org.xwiki.security.authorization.ContextualAuthorizationManager; +import org.xwiki.security.authorization.Right; import org.xwiki.stability.Unstable; +import org.xwiki.xml.XMLUtils; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.doc.XWikiDocument; @@ -47,6 +51,9 @@ public class TextAreaClass extends StringClass { + private static final String FAILED_VELOCITY_EXECUTION_WARNING = + "Failed to execute velocity code in text area property [{}]: [{}]"; + /** * Possible values for the editor meta property. * <p> @@ -437,47 +444,115 @@ public void displayView(StringBuffer buffer, String name, String prefix, BaseCol if (contentType == ContentType.PURE_TEXT) { super.displayView(buffer, name, prefix, object, context); } else if (contentType == ContentType.VELOCITY_CODE) { - StringBuffer result = new StringBuffer(); - super.displayView(result, name, prefix, object, context); - if (getObjectDocumentSyntax(object, context).equals(Syntax.XWIKI_1_0)) { - buffer.append(context.getWiki().parseContent(result.toString(), context)); - } else { - // Don't do anything since this mode is deprecated and not supported in the new rendering. - buffer.append(result); - } + displayVelocityCode(buffer, name, prefix, object, context); } else { - BaseProperty property = (BaseProperty) object.safeget(name); + BaseProperty<?> property = (BaseProperty<?>) object.safeget(name); if (property != null) { String content = property.toText(); XWikiDocument sdoc = getObjectDocument(object, context); + if (contentType == ContentType.VELOCITYWIKI) { + content = maybeEvaluateContent(name, isolated, content, sdoc); + } + if (sdoc != null) { - if (contentType == ContentType.VELOCITYWIKI) { - // Start with a pass of Velocity - // TODO: maybe make velocity+wiki a syntax so that getRenderedContent can directly take care - // of that - VelocityEvaluator velocityEvaluator = Utils.getComponent(VelocityEvaluator.class); - content = velocityEvaluator.evaluateVelocityNoException(content, + sdoc = ensureContentAuthorIsMetadataAuthor(sdoc); + + buffer.append( + context.getDoc().getRenderedContent(content, sdoc.getSyntax(), isRestricted(), sdoc, + isolated, context)); + } else { + buffer.append(XMLUtils.escapeElementText(content)); + } + } + } + } + + private static XWikiDocument ensureContentAuthorIsMetadataAuthor(XWikiDocument sdoc) + { + XWikiDocument result; + + // Make sure the right author is used to execute the textarea + // Clone the document to avoid changing the cached document instance + if (!Objects.equals(sdoc.getAuthors().getEffectiveMetadataAuthor(), sdoc.getAuthors().getContentAuthor())) { + result = sdoc.clone(); + result.getAuthors().setContentAuthor(sdoc.getAuthors().getEffectiveMetadataAuthor()); + } else { + result = sdoc; + } + + return result; + } + + private String maybeEvaluateContent(String name, boolean isolated, String content, XWikiDocument sdoc) + { + if (sdoc != null) { + // Start with a pass of Velocity + // TODO: maybe make velocity+wiki a syntax so that getRenderedContent can directly take care + // of that + AuthorExecutor authorExecutor = Utils.getComponent(AuthorExecutor.class); + VelocityEvaluator velocityEvaluator = Utils.getComponent(VelocityEvaluator.class); + try { + return authorExecutor.call(() -> { + String result; + // Check script right inside the author executor as otherwise the context author might not be + // correct. + if (isDocumentAuthorAllowedToEvaluateScript(sdoc)) { + result = velocityEvaluator.evaluateVelocityNoException(content, isolated ? sdoc.getDocumentReference() : null); + } else { + result = content; } + return result; + }, sdoc.getAuthorReference(), sdoc.getDocumentReference()); + } catch (Exception e) { + LOGGER.warn(FAILED_VELOCITY_EXECUTION_WARNING, name, ExceptionUtils.getRootCauseMessage(e)); + } + } - // Make sure the right author is used to execute the textarea - // Clone the document to void messaging with the cache - if (!Objects.equals(sdoc.getAuthors().getEffectiveMetadataAuthor(), - sdoc.getAuthors().getContentAuthor())) { - sdoc = sdoc.clone(); - sdoc.getAuthors().setContentAuthor(sdoc.getAuthors().getEffectiveMetadataAuthor()); - } + return content; + } - buffer.append(context.getDoc().getRenderedContent(content, sdoc.getSyntax(), isRestricted(), sdoc, - isolated, context)); - } else { - buffer.append(content); - } + private void displayVelocityCode(StringBuffer buffer, String name, String prefix, BaseCollection object, + XWikiContext context) + { + StringBuffer result = new StringBuffer(); + super.displayView(result, name, prefix, object, context); + XWikiDocument sdoc = getObjectDocument(object, context); + if (getObjectDocumentSyntax(object, context).equals(Syntax.XWIKI_1_0) && sdoc != null) { + try { + Utils.getComponent(AuthorExecutor.class).call(() -> { + // Check script right inside the author executor as otherwise the context author might not be + // correct. + if (isDocumentAuthorAllowedToEvaluateScript(sdoc)) { + buffer.append(context.getWiki().parseContent(result.toString(), context)); + } else { + buffer.append(result); + } + return null; + }, sdoc.getAuthorReference(), sdoc.getDocumentReference()); + } catch (Exception e) { + LOGGER.warn(FAILED_VELOCITY_EXECUTION_WARNING, name, ExceptionUtils.getRootCauseMessage(e)); + buffer.append(result); } + } else { + // Don't do anything since this mode is deprecated and not supported in the new rendering. + buffer.append(result); } } + private boolean isDocumentAuthorAllowedToEvaluateScript(XWikiDocument document) + { + boolean isAllowed = !isRestricted() && !document.isRestricted(); + + if (isAllowed) { + ContextualAuthorizationManager authorization = Utils.getComponent(ContextualAuthorizationManager.class); + isAllowed = authorization.hasAccess(Right.SCRIPT); + } + + return isAllowed; + } + private XWikiDocument getObjectDocument(BaseCollection object, XWikiContext context) { try {
xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/objects/classes/TextAreaClassTest.java+243 −0 added@@ -0,0 +1,243 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package com.xpn.xwiki.objects.classes; + +import javax.inject.Provider; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.xwiki.model.document.DocumentAuthors; +import org.xwiki.model.reference.DocumentReference; +import org.xwiki.rendering.syntax.Syntax; +import org.xwiki.security.authorization.Right; +import org.xwiki.test.annotation.ComponentList; +import org.xwiki.test.junit5.LogCaptureExtension; +import org.xwiki.test.junit5.mockito.MockComponent; +import org.xwiki.user.GuestUserReference; +import org.xwiki.user.SuperAdminUserReference; + +import com.xpn.xwiki.XWikiContext; +import com.xpn.xwiki.doc.XWikiDocument; +import com.xpn.xwiki.internal.render.OldRendering; +import com.xpn.xwiki.internal.security.authorization.DefaultAuthorExecutor; +import com.xpn.xwiki.internal.velocity.VelocityEvaluator; +import com.xpn.xwiki.objects.BaseObject; +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.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for the {@link TextAreaClass} class. + * + * @version $Id$ + */ +@OldcoreTest +@ComponentList({ DefaultAuthorExecutor.class }) +class TextAreaClassTest +{ + private static final String PROPERTY_NAME = "test"; + + @InjectMockitoOldcore + private MockitoOldcore oldcore; + + @MockComponent + private Provider<OldRendering> oldRenderingProvider; + + @MockComponent + private VelocityEvaluator velocityEvaluator; + + @RegisterExtension + private final LogCaptureExtension logCaptureExtension = new LogCaptureExtension(); + + @Test + void viewWikiText() + { + // Use a spy, so we don't need to mess around with clone-support in a mock. + XWikiDocument spyDocument = getSpyDocument(); + + TextAreaClass textAreaClass = new TextAreaClass(); + textAreaClass.setContentType(TextAreaClass.ContentType.WIKI_TEXT); + BaseObject object = new BaseObject(); + object.setOwnerDocument(spyDocument); + object.setLargeStringValue(PROPERTY_NAME, "**Test bold**"); + StringBuffer buffer = new StringBuffer(); + String renderingResult = "<p><strong>Test bold</strong></p>"; + doAnswer(invocationOnMock -> { + XWikiDocument sDoc = invocationOnMock.getArgument(3); + // Verify that the content author is set to the metadata author. + assertEquals(GuestUserReference.INSTANCE, sDoc.getAuthors().getContentAuthor()); + return renderingResult; + }).when(spyDocument).getRenderedContent(anyString(), any(Syntax.class), anyBoolean(), any(XWikiDocument.class), + anyBoolean(), any(XWikiContext.class)); + textAreaClass.displayView(buffer, PROPERTY_NAME, "", object, true, this.oldcore.getXWikiContext()); + + verify(spyDocument).getRenderedContent(anyString(), any(Syntax.class), anyBoolean(), + any(XWikiDocument.class), anyBoolean(), any(XWikiContext.class)); + + assertEquals(renderingResult, buffer.toString()); + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void viewVelocityCode(boolean allowExecution) + { + XWikiDocument spyDocument = getSpyDocument(); + // Velocity code is only supported in XWiki 1.0. + spyDocument.setSyntax(Syntax.XWIKI_1_0); + + TextAreaClass textAreaClass = new TextAreaClass(); + textAreaClass.setContentType(TextAreaClass.ContentType.VELOCITY_CODE); + + BaseObject object = new BaseObject(); + object.setOwnerDocument(spyDocument); + String velocityCode = "#set($x = 1) $1 & 1"; + object.setLargeStringValue(PROPERTY_NAME, velocityCode); + StringBuffer buffer = new StringBuffer(); + + when(this.oldcore.getMockContextualAuthorizationManager().hasAccess(Right.SCRIPT)).then(invocationOnMock -> { + // Verify that the content author is set to the metadata author. + XWikiDocument sDoc = (XWikiDocument) this.oldcore.getXWikiContext().get(XWikiDocument.CKEY_SDOC); + assertEquals(GuestUserReference.INSTANCE, sDoc.getAuthors().getContentAuthor()); + return allowExecution; + }); + + String renderingResult = "1 & 1"; + OldRendering oldRendering = mock(); + String renderingInput = velocityCode.replace("&", "&"); + when(oldRendering.parseContent(renderingInput, this.oldcore.getXWikiContext())) + .thenReturn(renderingResult); + when(this.oldRenderingProvider.get()).thenReturn(oldRendering); + + textAreaClass.displayView(buffer, PROPERTY_NAME, "", object, true, this.oldcore.getXWikiContext()); + + if (allowExecution) { + assertEquals(renderingResult, buffer.toString()); + } else { + assertEquals(renderingInput, buffer.toString()); + verify(oldRendering, never()).parseContent(anyString(), any()); + } + + verify(this.oldcore.getMockContextualAuthorizationManager()).hasAccess(Right.SCRIPT); + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void viewVelocityWiki(boolean allowExecution) + { + XWikiDocument spyDocument = getSpyDocument(); + + TextAreaClass textAreaClass = new TextAreaClass(); + textAreaClass.setContentType(TextAreaClass.ContentType.VELOCITYWIKI); + + BaseObject object = new BaseObject(); + object.setOwnerDocument(spyDocument); + String velocityCode = "#set($x = \"bold\") **$1**"; + object.setLargeStringValue(PROPERTY_NAME, velocityCode); + StringBuffer buffer = new StringBuffer(); + + when(this.oldcore.getMockContextualAuthorizationManager().hasAccess(Right.SCRIPT)).then(invocationOnMock -> { + // Verify that the content author is set to the metadata author. + XWikiDocument sDoc = (XWikiDocument) this.oldcore.getXWikiContext().get(XWikiDocument.CKEY_SDOC); + assertEquals(GuestUserReference.INSTANCE, sDoc.getAuthors().getContentAuthor()); + return allowExecution; + }); + + String velocityResult = "**bold**"; + when(this.velocityEvaluator.evaluateVelocityNoException(velocityCode, spyDocument.getDocumentReference())) + .thenReturn(velocityResult); + + String renderingResult = "<p><strong>bold</strong></p>"; + + // Mock the rendering of the result of the Velocity code. Check that the content author is set to the + // metadata author. + doAnswer(invocationOnMock -> { + // Verify that the input is as expected. + if (allowExecution) { + assertEquals(velocityResult, invocationOnMock.getArgument(0)); + } else { + assertEquals(velocityCode, invocationOnMock.getArgument(0)); + } + + XWikiDocument sDoc = invocationOnMock.getArgument(3); + assertEquals(GuestUserReference.INSTANCE, sDoc.getAuthors().getContentAuthor()); + + return renderingResult; + }).when(spyDocument).getRenderedContent(anyString(), same(Syntax.XWIKI_2_1), anyBoolean(), + any(XWikiDocument.class), anyBoolean(), any(XWikiContext.class)); + + textAreaClass.displayView(buffer, PROPERTY_NAME, "", object, true, this.oldcore.getXWikiContext()); + + assertEquals(renderingResult, buffer.toString()); + + // Verify that script right was actually checked. + verify(this.oldcore.getMockContextualAuthorizationManager()).hasAccess(Right.SCRIPT); + } + + @Test + void viewVelocityWikiWithoutOwnerDocument() + { + TextAreaClass textAreaClass = new TextAreaClass(); + textAreaClass.setContentType(TextAreaClass.ContentType.VELOCITYWIKI); + + BaseObject object = new BaseObject(); + String velocityCode = "1 & 2"; + object.setLargeStringValue(PROPERTY_NAME, velocityCode); + StringBuffer buffer = new StringBuffer(); + + textAreaClass.displayView(buffer, PROPERTY_NAME, "", object, true, this.oldcore.getXWikiContext()); + + assertEquals("1 & 2", buffer.toString()); + + verify(this.oldcore.getMockContextualAuthorizationManager(), never()).hasAccess(Right.SCRIPT); + + assertEquals(1, this.logCaptureExtension.size()); + assertEquals("Error while getting the syntax corresponding to object [null]." + + " Defaulting to using XWiki 1.0 syntax. Internal error [NullPointerException: ]", + this.logCaptureExtension.getMessage(0)); + } + + private XWikiDocument getSpyDocument() + { + // Use a spy, so we don't need to mess around with clone-support in a mock. + XWikiDocument spyDocument = spy(new XWikiDocument(new DocumentReference("wiki", "space", "page"))); + spyDocument.setSyntax(Syntax.XWIKI_2_1); + this.oldcore.getXWikiContext().setDoc(spyDocument); + // Add some authors to the document to verify that TextAreaClass is correctly setting the content author to + // the metadata author. + DocumentAuthors authors = spyDocument.getAuthors(); + authors.setEffectiveMetadataAuthor(GuestUserReference.INSTANCE); + authors.setContentAuthor(SuperAdminUserReference.INSTANCE); + return spyDocument; + } +}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-m5m2-h6h9-p2c8ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-41046ghsaADVISORY
- github.com/xwiki/xwiki-platform/commit/edc52579eeaab1b4514785c134044671a1ecd839ghsax_refsource_MISCWEB
- github.com/xwiki/xwiki-platform/security/advisories/GHSA-m5m2-h6h9-p2c8ghsax_refsource_CONFIRMWEB
- jira.xwiki.org/browse/XWIKI-20847ghsax_refsource_MISCWEB
- jira.xwiki.org/browse/XWIKI-20848ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.