VYPR
Critical severityNVD Advisory· Published Apr 15, 2023· Updated Feb 6, 2025

org.xwiki.platform:xwiki-platform-skin-skinx vulnerable to basic Cross-site Scripting by exploiting JSX or SSX plugins

CVE-2023-29206

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.

PackageAffected versionsPatched versions
org.xwiki.platform:xwiki-platform-skin-skinxMaven
>= 3.0-milestone-1, < 14.9-rc-114.9-rc-1

Affected products

1

Patches

1
fe65bc35d567

XWIKI-9119: Refactoring of SkinExtensionPlugin

https://github.com/xwiki/xwiki-platformSimon UrliOct 10, 2022via ghsa
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

News mentions

0

No linked articles in our index yet.