org.xwiki.platform:xwiki-platform-skin-skinx vulnerable to basic Cross-site Scripting by exploiting JSX or SSX plugins
Description
XWiki Commons are technical libraries common to several other top level XWiki projects. There was no check in the author of a JavaScript xobject or StyleSheet xobject added in a XWiki document, so until now it was possible for a user having only Edit Right to create such object and to craft a script allowing to perform some operations when executing by a user with appropriate rights. This has been patched in XWiki 14.9-rc-1 by only executing the script if the author of it has Script rights.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.xwiki.platform:xwiki-platform-skin-skinxMaven | >= 3.0-milestone-1, < 14.9-rc-1 | 14.9-rc-1 |
Affected products
1- Range: >= 3.0-milestone-1, < 14.9-rc-1
Patches
1fe65bc35d567XWIKI-9119: Refactoring of SkinExtensionPlugin
5 files changed · +553 −37
xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/pom.xml+1 −1 modified@@ -31,7 +31,7 @@ <name>XWiki Platform - Skin - Skin Extensions</name> <description>XWiki Platform - Skin - Skin Extensions</description> <properties> - <xwiki.jacoco.instructionRatio>0.18</xwiki.jacoco.instructionRatio> + <xwiki.jacoco.instructionRatio>0.25</xwiki.jacoco.instructionRatio> </properties> <dependencies> <!-- Build tools -->
xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/main/java/com/xpn/xwiki/plugin/skinx/AbstractDocumentSkinExtensionPlugin.java+82 −23 modified@@ -29,6 +29,7 @@ import java.util.Set; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xwiki.bridge.event.DocumentCreatedEvent; @@ -37,6 +38,7 @@ import org.xwiki.bridge.event.WikiDeletedEvent; import org.xwiki.model.EntityType; import org.xwiki.model.reference.DocumentReference; +import org.xwiki.model.reference.DocumentReferenceResolver; import org.xwiki.model.reference.EntityReferenceResolver; import org.xwiki.model.reference.EntityReferenceSerializer; import org.xwiki.observation.EventListener; @@ -85,6 +87,10 @@ public abstract class AbstractDocumentSkinExtensionPlugin extends AbstractSkinEx */ private final List<Event> events = new ArrayList<>(3); + private AuthorizationManager authorizationManager; + private DocumentReferenceResolver<String> stringDocumentReferenceResolver; + private EntityReferenceResolver<String> currentEntityReferenceResolver; + /** * XWiki plugin constructor. * @@ -174,7 +180,7 @@ public void virtualInit(XWikiContext context) @Override public Set<String> getAlwaysUsedExtensions(XWikiContext context) { - EntityReferenceSerializer<String> serializer = Utils.getComponent(EntityReferenceSerializer.TYPE_STRING); + EntityReferenceSerializer<String> serializer = getDefaultEntityReferenceSerializer(); Set<DocumentReference> references = getAlwaysUsedExtensions(); Set<String> names = new HashSet<>(references.size()); for (DocumentReference reference : references) { @@ -214,7 +220,7 @@ public Set<DocumentReference> getAlwaysUsedExtensions() XWikiDocument doc = context.getWiki().getDocument(extension, context); // Only add the extension as being "always used" if the page holding it has been saved with // programming rights. - if (Utils.getComponent(AuthorizationManager.class).hasAccess(Right.PROGRAM, + if (getAuthorizationManager().hasAccess(Right.PROGRAM, doc.getAuthorReference(), doc.getDocumentReference())) { extensions.add(extension); } @@ -236,36 +242,83 @@ public Set<DocumentReference> getAlwaysUsedExtensions() public boolean hasPageExtensions(XWikiContext context) { XWikiDocument doc = context.getDoc(); - if (doc != null) { - List<BaseObject> objects = doc.getObjects(getExtensionClassName()); - if (objects != null) { - for (BaseObject obj : objects) { - if (obj == null) { - continue; - } - if (obj.getStringValue(USE_FIELDNAME).equals("currentPage")) { - return true; - } + boolean result = false; + if (doc != null && this.hasCurrentPageExtensionObjects(doc)) { + if (getAuthorizationManager().hasAccess(Right.SCRIPT, doc.getAuthorReference(), + doc.getDocumentReference())) { + result = true; + } else { + displayScriptRightLog(doc.getDocumentReference()); + } + } + return result; + } + + private void displayScriptRightLog(Object documentReference) + { + LOGGER.warn("Extensions present in [{}] ignored because of lack of script right from the author.", + documentReference); + } + + private boolean hasCurrentPageExtensionObjects(XWikiDocument doc) + { + List<BaseObject> objects = doc.getObjects(getExtensionClassName()); + if (objects != null) { + for (BaseObject obj : objects) { + if (obj == null) { + continue; + } + if (StringUtils.equals(obj.getStringValue(USE_FIELDNAME), "currentPage")) { + return true; } } } return false; } @Override - public void use(String resource, XWikiContext context) + public void use(String resource, Map<String, Object> parameters, XWikiContext context) { String canonicalResource = getCanonicalDocumentName(resource); - super.use(canonicalResource, context); + if (this.canResourceBeUsed(canonicalResource, context)) { + super.use(canonicalResource, parameters, context); + } else { + displayScriptRightLog(canonicalResource); + } } - @Override - public void use(String resource, Map<String, Object> parameters, XWikiContext context) + private DocumentReferenceResolver<String> getDocumentReferenceResolver() { - String canonicalResource = getCanonicalDocumentName(resource); + if (this.stringDocumentReferenceResolver == null) { + this.stringDocumentReferenceResolver = Utils.getComponent(DocumentReferenceResolver.TYPE_STRING); + } + return this.stringDocumentReferenceResolver; + } - super.use(canonicalResource, parameters, context); + private AuthorizationManager getAuthorizationManager() + { + if (this.authorizationManager == null) { + this.authorizationManager = Utils.getComponent(AuthorizationManager.class); + } + return this.authorizationManager; + } + + private boolean canResourceBeUsed(String resource, XWikiContext context) + { + DocumentReferenceResolver<String> documentReferenceResolver = getDocumentReferenceResolver(); + DocumentReference documentReference = documentReferenceResolver.resolve(resource); + + try { + XWikiDocument document = context.getWiki().getDocument(documentReference, context); + DocumentReference authorReference = document.getAuthorReference(); + return getAuthorizationManager().hasAccess(Right.SCRIPT, authorReference, documentReference); + } catch (XWikiException e) { + LOGGER.error("Error while loading [{}] for checking script right: [{}]", documentReference, + ExceptionUtils.getRootCauseMessage(e)); + LOGGER.debug("Original error stack trace: ", e); + return false; + } } /** @@ -332,7 +385,7 @@ private void onDocumentEvent(XWikiDocument document) if (document.getObject(getExtensionClassName()) != null) { // new or already existing object if (document.getObject(getExtensionClassName(), USE_FIELDNAME, "always", false) != null) { - if (Utils.getComponent(AuthorizationManager.class).hasAccess(Right.PROGRAM, + if (getAuthorizationManager().hasAccess(Right.PROGRAM, document.getAuthorReference(), document.getDocumentReference())) { getAlwaysUsedExtensions().add(document.getDocumentReference()); @@ -355,6 +408,14 @@ private void onDocumentEvent(XWikiDocument document) } } + private EntityReferenceResolver<String> getCurrentEntityReferenceResolver() + { + if (this.currentEntityReferenceResolver == null) { + this.currentEntityReferenceResolver = Utils.getComponent(EntityReferenceResolver.TYPE_STRING, "current"); + } + return this.currentEntityReferenceResolver; + } + /** * Get the canonical serialization of a document name, in the {@code wiki:Space.Document} format. * @@ -363,10 +424,8 @@ private void onDocumentEvent(XWikiDocument document) */ private String getCanonicalDocumentName(String documentName) { - @SuppressWarnings("unchecked") - EntityReferenceResolver<String> resolver = Utils.getComponent(EntityReferenceResolver.TYPE_STRING, "current"); - @SuppressWarnings("unchecked") - EntityReferenceSerializer<String> serializer = Utils.getComponent(EntityReferenceSerializer.TYPE_STRING); + EntityReferenceResolver<String> resolver = getCurrentEntityReferenceResolver(); + EntityReferenceSerializer<String> serializer = getDefaultEntityReferenceSerializer(); return serializer.serialize(resolver.resolve(documentName, EntityType.DOCUMENT)); }
xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/main/java/com/xpn/xwiki/plugin/skinx/AbstractSkinExtensionPlugin.java+9 −10 modified@@ -229,14 +229,7 @@ private void useResource(String resource, XWikiContext context) */ public void use(String resource, XWikiContext context) { - useResource(resource, context); - - // In case a previous call added some parameters, remove them, since the last call for a resource always - // discards previous ones. - getParametersMap(context).remove(resource); - - // Register the use of the resource in case the current execution is an asynchronous renderer - getSkinExtensionAsync().use(getName(), resource, null); + use(resource, null, context); } /** @@ -256,8 +249,14 @@ public void use(String resource, Map<String, Object> parameters, XWikiContext co { useResource(resource, context); - // Associate parameters to the resource - getParametersMap(context).put(resource, parameters); + // In case a previous call added some parameters, remove them, since the last call for a resource always + // discards previous ones. + if (parameters == null) { + getParametersMap(context).remove(resource); + } else { + // Associate parameters to the resource + getParametersMap(context).put(resource, parameters); + } getSkinExtensionAsync().use(getName(), resource, parameters); }
xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/test/java/com/xpn/xwiki/plugin/skinx/CssSkinExtensionPluginTest.java+181 −3 modified@@ -22,28 +22,41 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.Vector; import javax.inject.Named; import javax.inject.Provider; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.xwiki.model.EntityType; import org.xwiki.model.reference.DocumentReference; import org.xwiki.model.reference.DocumentReferenceResolver; +import org.xwiki.model.reference.EntityReferenceResolver; import org.xwiki.model.reference.EntityReferenceSerializer; import org.xwiki.model.reference.SpaceReference; import org.xwiki.model.reference.WikiReference; import org.xwiki.observation.ObservationManager; import org.xwiki.security.authorization.AuthorizationManager; import org.xwiki.security.authorization.ContextualAuthorizationManager; import org.xwiki.security.authorization.Right; +import org.xwiki.skinx.internal.async.SkinExtensionAsync; +import org.xwiki.test.LogLevel; +import org.xwiki.test.junit5.LogCaptureExtension; import org.xwiki.test.junit5.mockito.MockComponent; import com.xpn.xwiki.XWiki; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.XWikiException; import com.xpn.xwiki.doc.XWikiDocument; +import com.xpn.xwiki.objects.BaseObject; import com.xpn.xwiki.objects.classes.BaseClass; import com.xpn.xwiki.store.XWikiStoreInterface; import com.xpn.xwiki.test.MockitoOldcore; @@ -53,7 +66,13 @@ import com.xpn.xwiki.web.XWikiURLFactory; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; /** @@ -63,7 +82,7 @@ * @since 13.10RC1 */ @OldcoreTest -public class CssSkinExtensionPluginTest +class CssSkinExtensionPluginTest { @InjectMockitoOldcore private MockitoOldcore mockitoOldcore; @@ -83,18 +102,31 @@ public class CssSkinExtensionPluginTest @MockComponent @Named("currentmixed") - private DocumentReferenceResolver<String> documentReferenceResolver; + private DocumentReferenceResolver<String> currentMixedDocumentReferenceResolver; @MockComponent @Named("current") private DocumentReferenceResolver<String> currentDocumentReferenceResolver; + @MockComponent + private DocumentReferenceResolver<String> documentReferenceResolver; + + @MockComponent + @Named("current") + private EntityReferenceResolver<String> currentEntityReferenceResolver; + @MockComponent private AuthorizationManager authorizationManager; @MockComponent private ContextualAuthorizationManager contextualAuthorizationManager; + @MockComponent + private SkinExtensionAsync skinExtensionAsync; + + @RegisterExtension + LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN); + private XWikiContext context; private BaseClass pluginClass; private CssSkinExtensionPlugin skinExtensionPlugin; @@ -112,7 +144,7 @@ void setup() throws XWikiException XWikiRequest xWikiRequest = mock(XWikiRequest.class); context.setRequest(xWikiRequest); XWiki wiki = mockitoOldcore.getSpyXWiki(); - when(this.documentReferenceResolver.resolve(CssSkinExtensionPlugin.SSX_CLASS_NAME)).thenReturn( + when(this.currentMixedDocumentReferenceResolver.resolve(CssSkinExtensionPlugin.SSX_CLASS_NAME)).thenReturn( new DocumentReference(CssSkinExtensionPlugin.SSX_CLASS_REFERENCE, new WikiReference("xwiki"))); when(wiki.getDocument(CssSkinExtensionPlugin.SSX_CLASS_NAME, context)).thenReturn(classDoc); when(classDoc.getXClass()).thenReturn(this.pluginClass); @@ -206,4 +238,150 @@ void endParsingAlwaysUsedExtensions() throws XWikiException, MalformedURLExcepti String obtainedContent = this.skinExtensionPlugin.endParsing(content, this.context); assertEquals(expectedContent, obtainedContent); } + + @Test + void use() throws XWikiException + { + String resource = "MySpace.MySSXPage"; + Map<String, Object> parameters = Collections.singletonMap("fooParam", "barValue"); + + DocumentReference documentReference = new DocumentReference("xwiki", "MySpace", "MySSXPage"); + when(this.currentEntityReferenceResolver.resolve(resource, EntityType.DOCUMENT)).thenReturn(documentReference); + when(this.entityReferenceSerializer.serialize(documentReference)).thenReturn(resource); + + when(this.documentReferenceResolver.resolve(resource)).thenReturn(documentReference); + XWikiDocument document = mock(XWikiDocument.class); + when(this.mockitoOldcore.getSpyXWiki().getDocument(documentReference, context)).thenReturn(document); + + DocumentReference userReference = new DocumentReference("xwiki", "XWiki", "Foo"); + when(document.getAuthorReference()).thenReturn(userReference); + when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference, documentReference)).thenReturn(true); + + this.skinExtensionPlugin.use(resource, parameters, context); + String className = CssSkinExtensionPlugin.class.getCanonicalName(); + Set<String> resources = (Set<String>) context.get(className); + + assertEquals(Collections.singleton(resource), resources); + + Map<String, Map<String, Object>> parametersMap = + (Map<String, Map<String, Object>>) context.get(className + "_parameters"); + Map<String, Map<String, Object>> expectedParameters = new HashMap<>(); + expectedParameters.put(resource, parameters); + + assertEquals(expectedParameters, parametersMap); + verify(this.authorizationManager).hasAccess(Right.SCRIPT, userReference, documentReference); + verify(this.skinExtensionAsync).use("ssx", resource, parameters); + + String resource2 = "MySpace.MyOtherSSXPage"; + Map<String, Object> parameters2 = null; + DocumentReference documentReference2 = new DocumentReference("xwiki", "MySpace", "MyOtherSSXPage"); + + when(this.currentEntityReferenceResolver.resolve(resource2, EntityType.DOCUMENT)) + .thenReturn(documentReference2); + when(this.entityReferenceSerializer.serialize(documentReference2)).thenReturn(resource2); + + when(this.documentReferenceResolver.resolve(resource2)).thenReturn(documentReference2); + XWikiDocument document2 = mock(XWikiDocument.class); + when(this.mockitoOldcore.getSpyXWiki().getDocument(documentReference2, context)).thenReturn(document2); + + DocumentReference userReference2 = new DocumentReference("xwiki", "XWiki", "Bar"); + when(document2.getAuthorReference()).thenReturn(userReference2); + when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference2, documentReference2)).thenReturn(false); + + this.skinExtensionPlugin.use(resource2, parameters2, context); + resources = (Set<String>) context.get(className); + assertEquals(Collections.singleton(resource), resources); + parametersMap = + (Map<String, Map<String, Object>>) context.get(className + "_parameters"); + assertEquals(expectedParameters, parametersMap); + verify(this.authorizationManager).hasAccess(Right.SCRIPT, userReference2, documentReference2); + verify(this.skinExtensionAsync, never()).use("ssx", resource2, parameters2); + + assertEquals(1, this.logCapture.size()); + assertEquals("Extensions present in [MySpace.MyOtherSSXPage] ignored because of lack of script right " + + "from the author.", this.logCapture.getMessage(0)); + + when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference2, documentReference2)).thenReturn(true); + this.skinExtensionPlugin.use(resource2, parameters2, context); + + Set<String> expectedSet = new HashSet<>(); + expectedSet.add(resource); + expectedSet.add(resource2); + + resources = (Set<String>) context.get(className); + assertEquals(expectedSet, resources); + + parametersMap = + (Map<String, Map<String, Object>>) context.get(className + "_parameters"); + assertEquals(expectedParameters, parametersMap); + verify(this.authorizationManager, times(2)).hasAccess(Right.SCRIPT, userReference2, documentReference2); + verify(this.skinExtensionAsync).use("ssx", resource2, null); + + parameters2 = Collections.singletonMap("buzValue", 42); + this.skinExtensionPlugin.use(resource2, parameters2, context); + + resources = (Set<String>) context.get(className); + assertEquals(expectedSet, resources); + parametersMap = + (Map<String, Map<String, Object>>) context.get(className + "_parameters"); + expectedParameters.put(resource2, parameters2); + assertEquals(expectedParameters, parametersMap); + verify(this.skinExtensionAsync).use("ssx", resource2, parameters2); + + this.skinExtensionPlugin.use(resource, null, context); + expectedParameters.remove(resource); + resources = (Set<String>) context.get(className); + assertEquals(expectedSet, resources); + parametersMap = + (Map<String, Map<String, Object>>) context.get(className + "_parameters"); + expectedParameters.put(resource2, parameters2); + assertEquals(expectedParameters, parametersMap); + verify(this.skinExtensionAsync).use("ssx", resource, null); + } + + @Test + void hasPageExtensions() + { + this.context.setDoc(null); + assertFalse(this.skinExtensionPlugin.hasPageExtensions(context)); + + XWikiDocument currentDoc = mock(XWikiDocument.class, "currentDoc"); + this.context.setDoc(currentDoc); + + String className = CssSkinExtensionPlugin.SSX_CLASS_NAME; + when(currentDoc.getObjects(className)).thenReturn(null); + assertFalse(this.skinExtensionPlugin.hasPageExtensions(context)); + verify(currentDoc).getObjects(className); + + BaseObject baseObject = mock(BaseObject.class, "specificObj"); + when(baseObject.getStringValue("use")).thenReturn("wiki"); + Vector<BaseObject> objectList = new Vector<>(); + objectList.add(mock(BaseObject.class)); + objectList.add(null); + objectList.add(mock(BaseObject.class)); + objectList.add(baseObject); + objectList.add(null); + objectList.add(mock(BaseObject.class)); + + when(currentDoc.getObjects(className)).thenReturn(objectList); + assertFalse(this.skinExtensionPlugin.hasPageExtensions(context)); + verifyNoInteractions(this.authorizationManager); + + when(baseObject.getStringValue("use")).thenReturn("currentPage"); + DocumentReference documentReference = new DocumentReference("xwiki", "MySpace", "SomePage"); + DocumentReference userReference = new DocumentReference("xwiki", "XWiki", "Foo"); + when(currentDoc.getDocumentReference()).thenReturn(documentReference); + when(currentDoc.getAuthorReference()).thenReturn(userReference); + + when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference, documentReference)).thenReturn(false); + assertFalse(this.skinExtensionPlugin.hasPageExtensions(context)); + verify(this.authorizationManager).hasAccess(Right.SCRIPT, userReference, documentReference); + + assertEquals(1, this.logCapture.size()); + assertEquals("Extensions present in [xwiki:MySpace.SomePage] ignored because of lack of script right " + + "from the author.", this.logCapture.getMessage(0)); + + when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference, documentReference)).thenReturn(true); + assertTrue(this.skinExtensionPlugin.hasPageExtensions(context)); + } }
xwiki-platform-core/xwiki-platform-skin/xwiki-platform-skin-skinx/src/test/java/com/xpn/xwiki/plugin/skinx/JsSkinExtensionPluginTest.java+280 −0 added@@ -0,0 +1,280 @@ +/* + * 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.plugin.skinx; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.Vector; + +import javax.inject.Named; +import javax.inject.Provider; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.xwiki.model.EntityType; +import org.xwiki.model.reference.DocumentReference; +import org.xwiki.model.reference.DocumentReferenceResolver; +import org.xwiki.model.reference.EntityReferenceResolver; +import org.xwiki.model.reference.EntityReferenceSerializer; +import org.xwiki.model.reference.WikiReference; +import org.xwiki.observation.ObservationManager; +import org.xwiki.security.authorization.AuthorizationManager; +import org.xwiki.security.authorization.Right; +import org.xwiki.skinx.internal.async.SkinExtensionAsync; +import org.xwiki.test.LogLevel; +import org.xwiki.test.junit5.LogCaptureExtension; +import org.xwiki.test.junit5.mockito.MockComponent; + +import com.xpn.xwiki.XWiki; +import com.xpn.xwiki.XWikiContext; +import com.xpn.xwiki.XWikiException; +import com.xpn.xwiki.doc.XWikiDocument; +import com.xpn.xwiki.objects.BaseObject; +import com.xpn.xwiki.objects.classes.BaseClass; +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.web.XWikiRequest; +import com.xpn.xwiki.web.XWikiURLFactory; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link JsSkinExtensionPlugin}. + * + * @version $Id$ + * @since 14.9RC1 + */ +@OldcoreTest +class JsSkinExtensionPluginTest +{ + @InjectMockitoOldcore + private MockitoOldcore mockitoOldcore; + + @MockComponent + private Provider<DocumentReference> documentReferenceProvider; + + @MockComponent + private ObservationManager observationManager; + + @MockComponent + private EntityReferenceSerializer<String> entityReferenceSerializer; + + @MockComponent + @Named("currentmixed") + private DocumentReferenceResolver<String> currentMixedDocumentReferenceResolver; + + @MockComponent + private DocumentReferenceResolver<String> documentReferenceResolver; + + @MockComponent + @Named("current") + private EntityReferenceResolver<String> currentEntityReferenceResolver; + + @MockComponent + private AuthorizationManager authorizationManager; + + @MockComponent + private SkinExtensionAsync skinExtensionAsync; + + @RegisterExtension + LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN); + + private XWikiContext context; + private BaseClass pluginClass; + private JsSkinExtensionPlugin skinExtensionPlugin; + private XWikiURLFactory urlFactory; + + @BeforeEach + void setup() throws XWikiException + { + XWikiDocument classDoc = mock(XWikiDocument.class); + this.pluginClass = mock(BaseClass.class); + + this.context = mockitoOldcore.getXWikiContext(); + this.urlFactory = mock(XWikiURLFactory.class); + context.setURLFactory(urlFactory); + XWikiRequest xWikiRequest = mock(XWikiRequest.class); + context.setRequest(xWikiRequest); + XWiki wiki = mockitoOldcore.getSpyXWiki(); + when(this.currentMixedDocumentReferenceResolver.resolve(JsSkinExtensionPlugin.JSX_CLASS_NAME)).thenReturn( + new DocumentReference(JsSkinExtensionPlugin.JSX_CLASS_REFERENCE, new WikiReference("xwiki"))); + when(wiki.getDocument(JsSkinExtensionPlugin.JSX_CLASS_NAME, context)).thenReturn(classDoc); + when(classDoc.getXClass()).thenReturn(this.pluginClass); + this.skinExtensionPlugin = new JsSkinExtensionPlugin("plugin1", "unused", context); + skinExtensionPlugin.init(context); + } + + @Test + void use() throws XWikiException + { + String resource = "MySpace.MyJSXPage"; + Map<String, Object> parameters = Collections.singletonMap("fooParam", "barValue"); + + DocumentReference documentReference = new DocumentReference("xwiki", "MySpace", "MyJSXPage"); + when(this.currentEntityReferenceResolver.resolve(resource, EntityType.DOCUMENT)).thenReturn(documentReference); + when(this.entityReferenceSerializer.serialize(documentReference)).thenReturn(resource); + + when(this.documentReferenceResolver.resolve(resource)).thenReturn(documentReference); + XWikiDocument document = mock(XWikiDocument.class); + when(this.mockitoOldcore.getSpyXWiki().getDocument(documentReference, context)).thenReturn(document); + + DocumentReference userReference = new DocumentReference("xwiki", "XWiki", "Foo"); + when(document.getAuthorReference()).thenReturn(userReference); + when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference, documentReference)).thenReturn(true); + + this.skinExtensionPlugin.use(resource, parameters, context); + String className = JsSkinExtensionPlugin.class.getCanonicalName(); + Set<String> resources = (Set<String>) context.get(className); + + assertEquals(Collections.singleton(resource), resources); + + Map<String, Map<String, Object>> parametersMap = + (Map<String, Map<String, Object>>) context.get(className + "_parameters"); + Map<String, Map<String, Object>> expectedParameters = new HashMap<>(); + expectedParameters.put(resource, parameters); + + assertEquals(expectedParameters, parametersMap); + verify(this.authorizationManager).hasAccess(Right.SCRIPT, userReference, documentReference); + verify(this.skinExtensionAsync).use("jsx", resource, parameters); + + String resource2 = "MySpace.MyOtherJSXPage"; + Map<String, Object> parameters2 = null; + DocumentReference documentReference2 = new DocumentReference("xwiki", "MySpace", "MyOtherJSXPage"); + + when(this.currentEntityReferenceResolver.resolve(resource2, EntityType.DOCUMENT)) + .thenReturn(documentReference2); + when(this.entityReferenceSerializer.serialize(documentReference2)).thenReturn(resource2); + + when(this.documentReferenceResolver.resolve(resource2)).thenReturn(documentReference2); + XWikiDocument document2 = mock(XWikiDocument.class); + when(this.mockitoOldcore.getSpyXWiki().getDocument(documentReference2, context)).thenReturn(document2); + + DocumentReference userReference2 = new DocumentReference("xwiki", "XWiki", "Bar"); + when(document2.getAuthorReference()).thenReturn(userReference2); + when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference2, documentReference2)).thenReturn(false); + + this.skinExtensionPlugin.use(resource2, parameters2, context); + resources = (Set<String>) context.get(className); + assertEquals(Collections.singleton(resource), resources); + parametersMap = + (Map<String, Map<String, Object>>) context.get(className + "_parameters"); + assertEquals(expectedParameters, parametersMap); + verify(this.authorizationManager).hasAccess(Right.SCRIPT, userReference2, documentReference2); + verify(this.skinExtensionAsync, never()).use("jsx", resource2, parameters2); + + assertEquals(1, this.logCapture.size()); + assertEquals("Extensions present in [MySpace.MyOtherJSXPage] ignored because of lack of script right " + + "from the author.", this.logCapture.getMessage(0)); + + when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference2, documentReference2)).thenReturn(true); + this.skinExtensionPlugin.use(resource2, parameters2, context); + + Set<String> expectedSet = new HashSet<>(); + expectedSet.add(resource); + expectedSet.add(resource2); + + resources = (Set<String>) context.get(className); + assertEquals(expectedSet, resources); + + parametersMap = + (Map<String, Map<String, Object>>) context.get(className + "_parameters"); + assertEquals(expectedParameters, parametersMap); + verify(this.authorizationManager, times(2)).hasAccess(Right.SCRIPT, userReference2, documentReference2); + verify(this.skinExtensionAsync).use("jsx", resource2, null); + + parameters2 = Collections.singletonMap("buzValue", 42); + this.skinExtensionPlugin.use(resource2, parameters2, context); + + resources = (Set<String>) context.get(className); + assertEquals(expectedSet, resources); + parametersMap = + (Map<String, Map<String, Object>>) context.get(className + "_parameters"); + expectedParameters.put(resource2, parameters2); + assertEquals(expectedParameters, parametersMap); + verify(this.skinExtensionAsync).use("jsx", resource2, parameters2); + + this.skinExtensionPlugin.use(resource, null, context); + expectedParameters.remove(resource); + resources = (Set<String>) context.get(className); + assertEquals(expectedSet, resources); + parametersMap = + (Map<String, Map<String, Object>>) context.get(className + "_parameters"); + expectedParameters.put(resource2, parameters2); + assertEquals(expectedParameters, parametersMap); + verify(this.skinExtensionAsync).use("jsx", resource, null); + } + + @Test + void hasPageExtensions() + { + this.context.setDoc(null); + assertFalse(this.skinExtensionPlugin.hasPageExtensions(context)); + + XWikiDocument currentDoc = mock(XWikiDocument.class, "currentDoc"); + this.context.setDoc(currentDoc); + + String className = JsSkinExtensionPlugin.JSX_CLASS_NAME; + when(currentDoc.getObjects(className)).thenReturn(null); + assertFalse(this.skinExtensionPlugin.hasPageExtensions(context)); + verify(currentDoc).getObjects(className); + + BaseObject baseObject = mock(BaseObject.class, "specificObj"); + when(baseObject.getStringValue("use")).thenReturn("wiki"); + Vector<BaseObject> objectList = new Vector<>(); + objectList.add(mock(BaseObject.class)); + objectList.add(null); + objectList.add(mock(BaseObject.class)); + objectList.add(baseObject); + objectList.add(null); + objectList.add(mock(BaseObject.class)); + + when(currentDoc.getObjects(className)).thenReturn(objectList); + assertFalse(this.skinExtensionPlugin.hasPageExtensions(context)); + verifyNoInteractions(this.authorizationManager); + + when(baseObject.getStringValue("use")).thenReturn("currentPage"); + DocumentReference documentReference = new DocumentReference("xwiki", "MySpace", "SomePage"); + DocumentReference userReference = new DocumentReference("xwiki", "XWiki", "Foo"); + when(currentDoc.getDocumentReference()).thenReturn(documentReference); + when(currentDoc.getAuthorReference()).thenReturn(userReference); + + when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference, documentReference)).thenReturn(false); + assertFalse(this.skinExtensionPlugin.hasPageExtensions(context)); + verify(this.authorizationManager).hasAccess(Right.SCRIPT, userReference, documentReference); + + assertEquals(1, this.logCapture.size()); + assertEquals("Extensions present in [xwiki:MySpace.SomePage] ignored because of lack of script right " + + "from the author.", this.logCapture.getMessage(0)); + + when(this.authorizationManager.hasAccess(Right.SCRIPT, userReference, documentReference)).thenReturn(true); + assertTrue(this.skinExtensionPlugin.hasPageExtensions(context)); + } +} \ No newline at end of file
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
7- github.com/advisories/GHSA-cmvg-w72j-7phxghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-29206ghsaADVISORY
- github.com/xwiki/xwiki-platform/commit/fe65bc35d5672dd2505b7ac4ec42aec57d500fbbghsax_refsource_MISCWEB
- github.com/xwiki/xwiki-platform/security/advisories/GHSA-cmvg-w72j-7phxghsax_refsource_CONFIRMWEB
- jira.xwiki.org/browse/XWIKI-19514ghsax_refsource_MISCWEB
- jira.xwiki.org/browse/XWIKI-19583ghsax_refsource_MISCWEB
- jira.xwiki.org/browse/XWIKI-9119ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.