VYPR
Medium severity5.9GHSA Advisory· Published May 26, 2026

XWiki Platform vulnerable to potential arbitrary file writing using path traversal from (subwiki) admin

CVE-2026-48047

Description

Impact

A potential path traversal vulnerability allow an attacker who manages to get a malicious WebJar extension installed on the wiki to write arbitrary files. While the consequences could be severe like overriding configuration files and setting the superadmin password, the attack first requires that the attacker already has admin access to at least a subwiki to be able to install a malicious extension. Further, the attacker needs to publish a malicious extension in an extension repository that is configured in the instance.

Patches

This vulnerability has been patched in XWiki 16.10.17, 17.4.9, 17.10.3, and 18.0.0RC1.

Workarounds

XWiki is not aware of any workarounds except for being careful whom developers grant script and admin rights to.

### Resources * https://jira.xwiki.org/browse/XWIKI-23902 * https://github.com/xwiki/xwiki-platform/commit/9f747fcd3200259a1de51957d3f5f6acc8e3816c

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Path traversal in XWiki WebJar API allows arbitrary file write, requiring admin access to install malicious extension.

Vulnerability

A path traversal vulnerability exists in the FilesystemResourceReferenceCopier class of XWiki Platform's WebJar API, affecting versions from 9.6-rc-1 up to (but not including) 16.10.17, 17.4.9, 17.10.3, and 18.0.0RC1 [2]. The bug allows an attacker who can install a malicious WebJar extension to write arbitrary files outside the intended export directory by using ../ sequences in resource paths [1][3]. The vulnerable code path is triggered during HTML export when a page references a WebJar resource with a crafted path [3].

Exploitation

An attacker must have admin access to at least a subwiki to install a malicious extension, and must publish that extension in a configured extension repository [2][4]. The attacker then creates a page that uses a script like $xwiki.linkx.use($services.webjars.url('my.webjar:malicious', '../../../malicious.txt')) to reference a path with ../ sequences [3]. When an HTML export of that page is performed, the vulnerable code copies the resource to a location outside the export directory, controlled by the attacker [1][3].

Impact

Successful exploitation allows arbitrary file write on the server, potentially overriding configuration files such as xwiki.cfg or setting the superadmin password, leading to full compromise of the XWiki instance [2][4]. The attacker gains the ability to write files with the privileges of the XWiki process.

Mitigation

The vulnerability is patched in XWiki versions 16.10.17, 17.4.9, 17.10.3, and 18.0.0RC1 [2][4]. No workarounds are available; administrators should carefully control who has script and admin rights [2][4]. The fix adds a canonical path check to ensure the target file remains within the export directory [1].

AI Insight generated on May 26, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

1
9f747fcd3200

XWIKI-23902: Protect against path traversal from WebJar

https://github.com/xwiki/xwiki-platformMichael HamannJan 15, 2026via ghsa-ref
2 files changed · +79 20
  • xwiki-platform-core/xwiki-platform-webjars/xwiki-platform-webjars-api/src/main/java/org/xwiki/webjars/internal/FilesystemResourceReferenceCopier.java+44 20 modified
    @@ -29,6 +29,7 @@
     import java.net.URI;
     import java.net.URISyntaxException;
     import java.net.URL;
    +import java.nio.charset.StandardCharsets;
     import java.util.Enumeration;
     import java.util.jar.JarEntry;
     import java.util.jar.JarFile;
    @@ -62,6 +63,8 @@ public class FilesystemResourceReferenceCopier
     
         private static final String CONCAT_PATH_FORMAT = "%s/%s";
     
    +    private static final String PATH_SEPARATOR = "/";
    +
         private File getJARFile(String resourceName) throws IOException
         {
             // Get the JAR URL by looking up the passed resource name to extract the location of the JAR
    @@ -108,13 +111,23 @@ void copyResourceFromJAR(String resourcePrefix, String resourceName, String targ
                         // TODO: Won't this cause collisions if the same resource is available on several subwikis
                         // for example?
                         String targetPath = targetPrefix + entry.getName().substring(resourcePrefix.length());
    -                    File targetLocation = new File(exportContext.getExportDir(), targetPath);
    -                    if (!targetLocation.exists()) {
    -                        targetLocation.getParentFile().mkdirs();
    -                        try (InputStream is = jar.getInputStream(entry);
    -                             FileOutputStream fos = new FileOutputStream(targetLocation)) {
    -                            IOUtils.copy(is, fos);
    +                    File exportDirectory = exportContext.getExportDir();
    +                    File targetLocation = new File(exportDirectory, targetPath);
    +                    // Check if the canonical file is within the export directory to avoid path traversal issues
    +                    String canonicalTargetPath = targetLocation.getCanonicalPath();
    +                    String canonicalExportPath = exportDirectory.getCanonicalPath();
    +                    if (canonicalTargetPath.startsWith(canonicalExportPath)) {
    +                        if (!targetLocation.exists()) {
    +                            targetLocation.getParentFile().mkdirs();
    +                            try (InputStream is = jar.getInputStream(entry);
    +                                 FileOutputStream fos = new FileOutputStream(targetLocation)) {
    +                                IOUtils.copy(is, fos);
    +                            }
                             }
    +                    } else {
    +                        LOGGER.warn("Skipping copying of resource [{}] to target location [{}] since it is outside "
    +                            + "of the export directory [{}]. Possible path traversal attempt.", entry.getName(),
    +                            canonicalTargetPath, canonicalExportPath);
                         }
                     }
                 }
    @@ -146,38 +159,49 @@ private void processCSSfile(String resourcePrefix, String targetPrefix, JarEntry
             FilesystemExportContext exportContext) throws Exception
         {
             // Limitation: we only support url() constructs located on a single line
    -        try (BufferedReader br = new BufferedReader(new InputStreamReader(jar.getInputStream(entry), "UTF-8"))) {
    +        try (BufferedReader br =
    +                 new BufferedReader(new InputStreamReader(jar.getInputStream(entry), StandardCharsets.UTF_8))) {
                 String line;
                 while ((line = br.readLine()) != null) {
                     Matcher matcher = URL_PATTERN.matcher(line);
                     while (matcher.find()) {
    -                    // Find the first non-null group
    -                    String url = null;
    -                    for (int i : new int[] { 1, 2, 3 }) {
    -                        String group = matcher.group(i);
    -                        if (group != null) {
    -                            url = group.trim();
    -                            break;
    -                        }
    -                    }
    +                    String url = resolveUrlFromMatcher(matcher);
                         // Determine if URL is relative
                         if (isRelativeURL(url)) {
                             // Remove any query string part and any fragment part too
                             url = StringUtils.substringBefore(url, "?");
                             url = StringUtils.substringBefore(url, "#");
                             // Normalize paths
                             String resourceName = String.format(CONCAT_PATH_FORMAT,
    -                            StringUtils.substringBeforeLast(entry.getName(), "/"), url);
    +                            StringUtils.substringBeforeLast(entry.getName(), PATH_SEPARATOR), url);
                             resourceName = new URI(resourceName).normalize().getPath();
    -                        resourceName = resourceName.substring(resourcePrefix.length() + 1);
    -                        // Copy to filesystem
    -                        copyResourceFromJAR(resourcePrefix, resourceName, targetPrefix, exportContext);
    +                        if (resourceName.startsWith(resourcePrefix + PATH_SEPARATOR)) {
    +                            resourceName = resourceName.substring(resourcePrefix.length() + 1);
    +                            // Copy to filesystem
    +                            copyResourceFromJAR(resourcePrefix, resourceName, targetPrefix, exportContext);
    +                        } else {
    +                            LOGGER.warn("Skipping copying of resource [{}] since it is outside of the expected "
    +                                + "prefix [{}]. Possible path traversal attempt.", resourceName, resourcePrefix);
    +                        }
                         }
                     }
                 }
             }
         }
     
    +    private static String resolveUrlFromMatcher(Matcher matcher)
    +    {
    +        // Find the first non-null group
    +        for (int i : new int[] { 1, 2, 3 }) {
    +            String group = matcher.group(i);
    +            if (group != null) {
    +                return group.trim();
    +            }
    +        }
    +
    +        return null;
    +    }
    +
         private boolean isRelativeURL(String url)
         {
             try {
    
  • xwiki-platform-core/xwiki-platform-webjars/xwiki-platform-webjars-api/src/test/java/org/xwiki/webjars/internal/FilesystemResourceReferenceCopierTest.java+35 0 modified
    @@ -32,13 +32,18 @@
     import org.junit.jupiter.api.BeforeEach;
     import org.junit.jupiter.api.Test;
     import org.junit.jupiter.api.extension.ExtendWith;
    +import org.junit.jupiter.api.extension.RegisterExtension;
     import org.junit.jupiter.params.ParameterizedTest;
     import org.junit.jupiter.params.provider.CsvSource;
     import org.junit.jupiter.params.provider.ValueSource;
    +import org.xwiki.test.LogLevel;
    +import org.xwiki.test.junit5.LogCaptureExtension;
     import org.xwiki.test.junit5.XWikiTempDir;
     import org.xwiki.test.junit5.XWikiTempDirExtension;
     import org.xwiki.url.filesystem.FilesystemExportContext;
     
    +import static org.hamcrest.MatcherAssert.assertThat;
    +import static org.hamcrest.Matchers.containsString;
     import static org.junit.jupiter.api.Assertions.assertFalse;
     import static org.junit.jupiter.api.Assertions.assertTrue;
     
    @@ -64,6 +69,9 @@ class FilesystemResourceReferenceCopierTest
     
         private ClassLoader originalClassLoader;
     
    +    @RegisterExtension
    +    private LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);
    +
         @BeforeEach
         void setUp()
         {
    @@ -112,6 +120,20 @@ void copyResourceFromJARDirectory() throws Exception
             assertTrue(new File(this.exportDir, "webjars/testlib/1.0.0/assets/file1.txt.gz").exists());
         }
     
    +    @Test
    +    void copyResourceFromJARWithPathTraversalAttack() throws Exception
    +    {
    +        // Create a JAR with a malicious resource
    +        String resourcePath = "../../../../file.txt";
    +        createTestJarWithResources(resourcePath);
    +
    +        FilesystemResourceReferenceCopier copier = new FilesystemResourceReferenceCopier();
    +        copier.copyResourceFromJAR(RESOURCE_PREFIX, "../../../../file.txt", "webjars/testlib/1.0.0",
    +            this.exportContext);
    +
    +        assertThat(this.logCapture.getMessage(0), containsString("Skipping copying of resource"));
    +    }
    +
         @ParameterizedTest
         @CsvSource({
             ".icon { background: url(\"../images/icon.png\"); }, css/style.css, images/icon.png",
    @@ -174,6 +196,19 @@ void processCSSWithIgnoredUrl(String cssContent) throws Exception
             assertFalse(webjarDir.exists());
         }
     
    +    @Test
    +    void processCSSWithPathTraversalAttack() throws Exception
    +    {
    +        // Create a JAR with a malicious CSS file
    +        String cssContent = ".icon { background: url(\"../../../../file.png\"); }";
    +        createTestJarWithCSS(cssContent, "css/style.css", "../../../../file.png");
    +
    +        FilesystemResourceReferenceCopier copier = new FilesystemResourceReferenceCopier();
    +        copier.processCSS(RESOURCE_PREFIX, "css/style.css", "webjars/testlib/1.0.0", this.exportContext);
    +
    +        assertThat(this.logCapture.getMessage(0), containsString("Skipping copying of resource"));
    +    }
    +
         /**
          * Creates a test JAR file with a CSS file and optional resource files, then sets up the class loader to find it.
          */
    

Vulnerability mechanics

Root cause

"Missing canonical-path validation when copying resources from a WebJar JAR allows path traversal via `../` sequences in resource names and CSS `url()` references."

Attack vector

An attacker with admin access to a subwiki creates a malicious WebJar extension containing resource names with `../` path traversal sequences (e.g., `../../../../file.txt`). The extension is published to a configured extension repository and installed on the wiki. When a page references the malicious resource (e.g., via `$services.webjars.url(...)`) and an HTML export is triggered, the `copyResourceFromJAR` method writes the file to a path outside the intended export directory. This could allow overwriting configuration files or other sensitive data on the server.

Affected code

The vulnerability resides in `FilesystemResourceReferenceCopier.java` in the `copyResourceFromJAR` and `processCSSfile` methods. When copying resources from a WebJar JAR to the filesystem, the code constructs a target file path by concatenating user-controlled resource names without validating that the resulting path stays within the intended export directory. The same issue exists when resolving relative `url()` references inside CSS files.

What the fix does

The patch adds two canonical-path checks. In `copyResourceFromJAR`, after constructing the target `File`, the code resolves both the target and the export directory to their canonical paths and verifies the target starts with the export directory — if not, the copy is skipped with a warning. In `processCSSfile`, after normalizing a relative URL from a CSS `url()` reference, the code checks that the resulting resource name still starts with the expected resource prefix before proceeding. Both changes prevent `../` sequences from escaping the intended directory boundary.

Preconditions

  • authAttacker must have admin access to at least a subwiki to install a malicious extension
  • configAttacker must publish a malicious WebJar extension in a repository configured in the XWiki instance
  • inputA page referencing the malicious resource must be exported (e.g., HTML export)

Reproduction

Create a WebJar extension containing a malicious path with `../` sequences (e.g., `../../../../malicious.txt`). Publish the extension in a configured extension repository and install it without programming rights. Create a page referencing the malicious path via a script such as `$xwiki.linkx.use($services.webjars.url('my.webjar:malicious', '../../../malicious.txt'))`. Perform an HTML export of that page — the malicious file is written outside the intended export directory [ref_id=2].

Generated on May 26, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.