Moderate severityNVD Advisory· Published Feb 12, 2026· Updated Feb 12, 2026
XWiki Platform affected by click-jacking through CSS injection in comments
CVE-2026-26000
Description
XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. Prior to 17.9.0, 17.4.6, and 16.10.13, it's possible using comments to inject CSS that would transform the full wiki in a link area leading to a malicious page. This vulnerability is fixed in 17.9.0, 17.4.6, and 16.10.13.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.xwiki.platform:xwiki-platform-webMaven | >= 17.5.0, < 17.9.0 | 17.9.0 |
org.xwiki.platform:xwiki-platform-webMaven | >= 17.0.0-rc-1, < 17.4.6 | 17.4.6 |
org.xwiki.platform:xwiki-platform-webMaven | < 16.10.13 | 16.10.13 |
Affected products
1- Range: >= 17.5.0, < 17.9.0
Patches
27b5a4f8c34d9XWIKI-23433: Provide frontend check of links (#4645)
8 files changed · +312 −5
xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-resources/src/main/resources/flamingo/javascript.vm+9 −0 modified@@ -299,6 +299,15 @@ $xwiki.jsfx.use("flamingo$jsExtension", {'forceSkinAction' : true, 'language' : 'wysiwyg': true })## #end +#if ($services.security.url.isFrontendUrlCheckEnabled()) +<script type="application/json" id="trusted-domains-configuration"> +{ + "trustedDomains": $jsontool.serialize($services.security.url.getTrustedDomains()), + "allowedUrls": $jsontool.serialize($services.security.url.getAllowedFrontendUrls()) +} +</script> + $xwiki.jsfx.use('uicomponents/link/link-protection.js')## +#end ## ## Hooks for inserting JavaScript skin extensions ##
xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-test/xwiki-platform-flamingo-skin-test-docker/src/test/it/org/xwiki/flamingo/test/docker/NavigationIT.java+92 −1 modified@@ -27,6 +27,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.openqa.selenium.Alert; +import org.openqa.selenium.By; +import org.openqa.selenium.NoAlertPresentException; import org.xwiki.administration.test.po.AdministrationPage; import org.xwiki.flamingo.skin.test.po.AttachmentsPane; import org.xwiki.flamingo.skin.test.po.AttachmentsViewPage; @@ -35,6 +38,7 @@ import org.xwiki.test.docker.junit5.TestReference; import org.xwiki.test.docker.junit5.UITest; import org.xwiki.test.ui.TestUtils; +import org.xwiki.test.ui.XWikiWebDriver; import org.xwiki.test.ui.po.CommentsTab; import org.xwiki.test.ui.po.HistoryPane; import org.xwiki.test.ui.po.InformationPane; @@ -43,14 +47,18 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; /** * Tests related to navigation in the wiki. * * @since 11.10 * @version $Id$ */ -@UITest +@UITest(properties = { + "xwikiPropertiesAdditionalProperties=url.trustedDomains=www.xwiki.org,extensions.xwiki.org\n" + + "url.allowedFrontendUrls=https://github.com/xwiki/xwiki-platform,https://github.com/xwiki/" +}) public class NavigationIT { @BeforeAll @@ -206,4 +214,87 @@ public void simpleBinUrlDoesNotThrowException(TestUtils testUtils) ViewPage viewPage = new ViewPage(); assertEquals("XWiki - Main - Main", viewPage.getPageTitle()); } + + @Order(5) + @Test + void navigationToExternalPages(TestUtils testUtils, TestReference testReference) throws Exception + { + String pageContent = """ + [[Internal link>>doc:Navigation.Test]] + [[Google external>>https://www.google.com]] + [[XWiki.org external>>https://www.xwiki.org]] + [[Contrib xwiki>>https://contrib.xwiki.org]] + [[Extensions xwiki>>https://extensions.xwiki.org]] + [[Specific extensions pages>>https://extensions.xwiki.org/bin/view/WebHome]] + [[Github commons>>https://github.com/xwiki/xwiki-commons]] + [[Github XWiki>>https://github.com/xwiki/]] + [[Github platform>>https://github.com/xwiki/xwiki-platform]] + """; + testUtils.rest().savePage(testReference, pageContent, "Test link navigation"); + testUtils.rest().savePage(new DocumentReference("xwiki", "Navigation", "Test"), "Test navigation internal " + + "link", "Navigation test page"); + + XWikiWebDriver driver = testUtils.getDriver(); + testUtils.gotoPage(testReference); + driver.findElementWithoutWaiting(By.linkText("Google external")).click(); + Alert alert = driver.switchTo().alert(); + assertEquals("You are about to leave the domain \"host.testcontainers.internal\" to follow a link " + + "to \"www.google.com\". Are you sure you want to continue?", alert.getText()); + alert.dismiss(); + + driver.findElementWithoutWaiting(By.linkText("Internal link")).click(); + ViewPage viewPage = new ViewPage(); + assertEquals("Test navigation internal link", viewPage.getContent()); + + testUtils.gotoPage(testReference); + driver.findElementWithoutWaiting(By.linkText("XWiki.org external")).click(); + try { + driver.switchTo().alert(); + fail("No alert should be present"); + } catch (NoAlertPresentException e) { + } + + testUtils.gotoPage(testReference); + driver.findElementWithoutWaiting(By.linkText("Contrib xwiki")).click(); + alert = driver.switchTo().alert(); + assertEquals("You are about to leave the domain \"host.testcontainers.internal\" to follow a link " + + "to \"contrib.xwiki.org\". Are you sure you want to continue?", alert.getText()); + alert.dismiss(); + + driver.findElementWithoutWaiting(By.linkText("Extensions xwiki")).click(); + try { + driver.switchTo().alert(); + fail("No alert should be present"); + } catch (NoAlertPresentException e) { + } + + testUtils.gotoPage(testReference); + driver.findElementWithoutWaiting(By.linkText("Specific extensions pages")).click(); + try { + driver.switchTo().alert(); + fail("No alert should be present"); + } catch (NoAlertPresentException e) { + } + + testUtils.gotoPage(testReference); + driver.findElementWithoutWaiting(By.linkText("Github commons")).click(); + assertEquals("You are about to leave the domain \"host.testcontainers.internal\" to follow a link " + + "to \"github.com\". Are you sure you want to continue?", alert.getText()); + alert.dismiss(); + + driver.findElementWithoutWaiting(By.linkText("Github XWiki")).click(); + try { + driver.switchTo().alert(); + fail("No alert should be present"); + } catch (NoAlertPresentException e) { + } + + testUtils.gotoPage(testReference); + driver.findElementWithoutWaiting(By.linkText("Github platform")).click(); + try { + driver.switchTo().alert(); + fail("No alert should be present"); + } catch (NoAlertPresentException e) { + } + } }
xwiki-platform-core/xwiki-platform-url/xwiki-platform-url-api/src/main/java/org/xwiki/url/script/URLSecurityScriptService.java+42 −0 modified@@ -21,6 +21,7 @@ import java.net.URI; import java.net.URISyntaxException; +import java.util.List; import javax.inject.Inject; import javax.inject.Named; @@ -29,6 +30,8 @@ import org.slf4j.Logger; import org.xwiki.component.annotation.Component; import org.xwiki.script.service.ScriptService; +import org.xwiki.stability.Unstable; +import org.xwiki.url.URLConfiguration; import org.xwiki.url.URLSecurityManager; /** @@ -46,6 +49,9 @@ public class URLSecurityScriptService implements ScriptService @Inject private URLSecurityManager urlSecurityManager; + @Inject + private URLConfiguration urlConfiguration; + @Inject private Logger logger; @@ -71,4 +77,40 @@ public URI parseToSafeURI(String uriRepresentation) throws URISyntaxException, S return null; } } + + /** + * @return the list of trusted domains. + * @since 17.9.0RC1 + * @since 17.4.6 + * @since 16.10.13 + */ + @Unstable + public List<String> getTrustedDomains() + { + return this.urlConfiguration.getTrustedDomains(); + } + + /** + * @return {@code true} if the mechanism to enforce URLs check on frontend is enabled. + * @since 17.9.0RC1 + * @since 17.4.6 + * @since 16.10.13 + */ + @Unstable + public boolean isFrontendUrlCheckEnabled() + { + return this.urlConfiguration.isFrontendUrlCheckEnabled(); + } + + /** + * @return the list of URLs that are allowed to avoid asking confirmation to users when accessing them. + * @since 17.9.0RC1 + * @since 17.4.6 + * @since 16.10.13 + */ + @Unstable + public List<String> getAllowedFrontendUrls() + { + return this.urlConfiguration.getAllowedFrontendUrls(); + } }
xwiki-platform-core/xwiki-platform-url/xwiki-platform-url-api/src/main/java/org/xwiki/url/URLConfiguration.java+32 −2 modified@@ -19,10 +19,10 @@ */ package org.xwiki.url; -import java.util.Collections; import java.util.List; import org.xwiki.component.annotation.Role; +import org.xwiki.stability.Unstable; /** * Configuration options for the URL module. @@ -60,7 +60,7 @@ default boolean useResourceLastModificationDate() */ default List<String> getTrustedDomains() { - return Collections.emptyList(); + return List.of(); } /** @@ -88,4 +88,34 @@ default List<String> getTrustedSchemes() { return List.of("http", "https", "ftp"); } + + /** + * @return {@code true} if checks should be done in the frontend when clicking on a link to validate it's driving + * to an authorized domain. This is independent from {@link #isTrustedDomainsEnabled()} which aims at enabling + * checks server side only. + * @since 17.9.0RC1 + * @since 17.4.6 + * @since 16.10.13 + */ + @Unstable + default boolean isFrontendUrlCheckEnabled() + { + return true; + } + + /** + * Define a list of allowed frontend URLs: in case the {@link #isFrontendUrlCheckEnabled()} is enabled, then + * this list can be used to allow specific URLs without asking confirmation from the user, while avoiding to add + * an entire domain in the list of trusted domains. + * + * @return the list of allowed frontend URLs + * @since 17.9.0RC1 + * @since 17.4.6 + * @since 16.10.13 + */ + @Unstable + default List<String> getAllowedFrontendUrls() + { + return List.of(); + } }
xwiki-platform-core/xwiki-platform-url/xwiki-platform-url-api/src/main/resources/ApplicationResources.properties+21 −0 added@@ -0,0 +1,21 @@ +# --------------------------------------------------------------------------- +# 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. +# --------------------------------------------------------------------------- +url.api.followLinkConfirmationText=You are about to leave the domain "{0}" to follow a link to "{1}". Are you sure \ + you want to continue? \ No newline at end of file
xwiki-platform-core/xwiki-platform-url/xwiki-platform-url-default/src/main/java/org/xwiki/url/internal/DefaultURLConfiguration.java+13 −2 modified@@ -19,7 +19,6 @@ */ package org.xwiki.url.internal; -import java.util.Collections; import java.util.List; import javax.inject.Inject; @@ -67,7 +66,7 @@ public boolean useResourceLastModificationDate() @Override public List<String> getTrustedDomains() { - return this.configuration.get().getProperty(PREFIX + "trustedDomains", Collections.emptyList()); + return this.configuration.get().getProperty(PREFIX + "trustedDomains", List.of()); } @Override @@ -81,4 +80,16 @@ public List<String> getTrustedSchemes() { return this.configuration.get().getProperty(PREFIX + "trustedSchemes", List.of("http", "https", "ftp")); } + + @Override + public boolean isFrontendUrlCheckEnabled() + { + return this.configuration.get().getProperty(PREFIX + "frontendUrlCheckEnabled", true); + } + + @Override + public List<String> getAllowedFrontendUrls() + { + return this.configuration.get().getProperty(PREFIX + "allowedFrontendUrls", List.of()); + } }
xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-war/src/main/webapp/resources/uicomponents/link/link-protection.js+85 −0 added@@ -0,0 +1,85 @@ +/* + * 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. + */ +define('link-protection-translations', { + prefix: '', + keys: [ + 'url.api.followLinkConfirmationText' + ] +}); +require(['jquery', 'xwiki-l10n!link-protection-translations', 'xwiki-events-bridge'], function ($, l10n) { + + function protectLinks () { + let configuration = null; + try { + const trustedDomainConfigElement = $('script#trusted-domains-configuration'); + if (trustedDomainConfigElement.length > 0) { + configuration = JSON.parse(trustedDomainConfigElement.text()); + } + } catch (err) { + console.error("Error while parsing the trusted domain configurations, falling back on enforcing checks on all" + + " links going outside current domain.", err); + } + $(document).on('click', 'a[href]', function (event) { + return askIfLinkNotTrusted(event, this, configuration); + }); + } + + function askIfLinkNotTrusted (event, anchor, configuration) { + let currentHostname = window.location.hostname; + let anchorHostname = anchor.hostname; + let customizedMessage = l10n.get('url.api.followLinkConfirmationText', currentHostname, anchorHostname); + if (configuration == null && !isAnchorCurrentDomain(anchor)) { + return confirm(customizedMessage); + } else if (!isAnchorTrustedOomain(anchor, configuration.trustedDomains, configuration.allowedUrls)) { + return confirm(customizedMessage); + } else { + return true; + } + } + + function isAnchorCurrentDomain (anchor) { + let currentHostname = window.location.hostname; + let anchorHostname = anchor.hostname; + return (!anchorHostname || anchorHostname === currentHostname); + } + + function isAnchorTrustedOomain (anchor, trustedDomains, allowedUrls) { + if (isAnchorCurrentDomain(anchor)) { + return true; + } else { + if (allowedUrls.indexOf(anchor.href) > -1) { + return true; + } + let host = anchor.hostname; + do { + if (trustedDomains.indexOf(host) > -1) { + return true; + } else if (host.indexOf(".") > -1) { + host = host.substring(host.indexOf(".") + 1); + } else { + host = ""; + } + } while (host !== ""); + } + return false; + } + + (XWiki.domIsLoaded && protectLinks()) || document.observe('xwiki:dom:loaded', protectLinks); +}); \ No newline at end of file
xwiki-platform-tools/xwiki-platform-tool-configuration-resources/src/main/resources/xwiki.properties.vm+18 −0 modified@@ -1035,6 +1035,24 @@ distribution.automaticStartOnWiki=$xwikiPropertiesAutomaticStartOnWiki #-# The default is: # url.forceAllowAnyCharacter=true +#-# [Since 17.9.0RC1] +#-# [Since 17.4.6] +#-# [Since 16.10.13] +#-# Allow to enable or disable checks performed when clicking links in the UI based on the list of trusted domains. +#-# +#-# By default this property is set to true: +# url.frontendUrlCheckEnabled=true + +#-# [Since 17.9.0RC1] +#-# [Since 17.4.6] +#-# [Since 16.10.13] +#-# Allow to allow specific URLs to be accessible from the frontend without asking confirmation, and without +#-# needing to allow and entire domain. The expected format is absolute URLs separated by commas, e.g.: +#-# https://github.com/xwiki/xwiki-platform,https://www.xwiki.org/xwiki/bin/view/Main/WebHome +#-# +#-# By default this property is empty: +# url.allowedFrontendUrls= + #------------------------------------------------------------------------------------- # Attachment #-------------------------------------------------------------------------------------
29cb81f3a538XWIKI-23433: Provide frontend check of links (#4645)
8 files changed · +312 −5
xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-resources/src/main/resources/flamingo/javascript.vm+9 −0 modified@@ -299,6 +299,15 @@ $xwiki.jsfx.use("flamingo$jsExtension", {'forceSkinAction' : true, 'language' : 'wysiwyg': true })## #end +#if ($services.security.url.isFrontendUrlCheckEnabled()) +<script type="application/json" id="trusted-domains-configuration"> +{ + "trustedDomains": $jsontool.serialize($services.security.url.getTrustedDomains()), + "allowedUrls": $jsontool.serialize($services.security.url.getAllowedFrontendUrls()) +} +</script> + $xwiki.jsfx.use('uicomponents/link/link-protection.js')## +#end ## ## Hooks for inserting JavaScript skin extensions ##
xwiki-platform-core/xwiki-platform-flamingo/xwiki-platform-flamingo-skin/xwiki-platform-flamingo-skin-test/xwiki-platform-flamingo-skin-test-docker/src/test/it/org/xwiki/flamingo/test/docker/NavigationIT.java+92 −1 modified@@ -27,6 +27,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.openqa.selenium.Alert; +import org.openqa.selenium.By; +import org.openqa.selenium.NoAlertPresentException; import org.xwiki.administration.test.po.AdministrationPage; import org.xwiki.flamingo.skin.test.po.AttachmentsPane; import org.xwiki.flamingo.skin.test.po.AttachmentsViewPage; @@ -35,6 +38,7 @@ import org.xwiki.test.docker.junit5.TestReference; import org.xwiki.test.docker.junit5.UITest; import org.xwiki.test.ui.TestUtils; +import org.xwiki.test.ui.XWikiWebDriver; import org.xwiki.test.ui.po.CommentsTab; import org.xwiki.test.ui.po.HistoryPane; import org.xwiki.test.ui.po.InformationPane; @@ -43,14 +47,18 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; /** * Tests related to navigation in the wiki. * * @since 11.10 * @version $Id$ */ -@UITest +@UITest(properties = { + "xwikiPropertiesAdditionalProperties=url.trustedDomains=www.xwiki.org,extensions.xwiki.org\n" + + "url.allowedFrontendUrls=https://github.com/xwiki/xwiki-platform,https://github.com/xwiki/" +}) public class NavigationIT { @BeforeAll @@ -206,4 +214,87 @@ public void simpleBinUrlDoesNotThrowException(TestUtils testUtils) ViewPage viewPage = new ViewPage(); assertEquals("XWiki - Main - Main", viewPage.getPageTitle()); } + + @Order(5) + @Test + void navigationToExternalPages(TestUtils testUtils, TestReference testReference) throws Exception + { + String pageContent = """ + [[Internal link>>doc:Navigation.Test]] + [[Google external>>https://www.google.com]] + [[XWiki.org external>>https://www.xwiki.org]] + [[Contrib xwiki>>https://contrib.xwiki.org]] + [[Extensions xwiki>>https://extensions.xwiki.org]] + [[Specific extensions pages>>https://extensions.xwiki.org/bin/view/WebHome]] + [[Github commons>>https://github.com/xwiki/xwiki-commons]] + [[Github XWiki>>https://github.com/xwiki/]] + [[Github platform>>https://github.com/xwiki/xwiki-platform]] + """; + testUtils.rest().savePage(testReference, pageContent, "Test link navigation"); + testUtils.rest().savePage(new DocumentReference("xwiki", "Navigation", "Test"), "Test navigation internal " + + "link", "Navigation test page"); + + XWikiWebDriver driver = testUtils.getDriver(); + testUtils.gotoPage(testReference); + driver.findElementWithoutWaiting(By.linkText("Google external")).click(); + Alert alert = driver.switchTo().alert(); + assertEquals("You are about to leave the domain \"host.testcontainers.internal\" to follow a link " + + "to \"www.google.com\". Are you sure you want to continue?", alert.getText()); + alert.dismiss(); + + driver.findElementWithoutWaiting(By.linkText("Internal link")).click(); + ViewPage viewPage = new ViewPage(); + assertEquals("Test navigation internal link", viewPage.getContent()); + + testUtils.gotoPage(testReference); + driver.findElementWithoutWaiting(By.linkText("XWiki.org external")).click(); + try { + driver.switchTo().alert(); + fail("No alert should be present"); + } catch (NoAlertPresentException e) { + } + + testUtils.gotoPage(testReference); + driver.findElementWithoutWaiting(By.linkText("Contrib xwiki")).click(); + alert = driver.switchTo().alert(); + assertEquals("You are about to leave the domain \"host.testcontainers.internal\" to follow a link " + + "to \"contrib.xwiki.org\". Are you sure you want to continue?", alert.getText()); + alert.dismiss(); + + driver.findElementWithoutWaiting(By.linkText("Extensions xwiki")).click(); + try { + driver.switchTo().alert(); + fail("No alert should be present"); + } catch (NoAlertPresentException e) { + } + + testUtils.gotoPage(testReference); + driver.findElementWithoutWaiting(By.linkText("Specific extensions pages")).click(); + try { + driver.switchTo().alert(); + fail("No alert should be present"); + } catch (NoAlertPresentException e) { + } + + testUtils.gotoPage(testReference); + driver.findElementWithoutWaiting(By.linkText("Github commons")).click(); + assertEquals("You are about to leave the domain \"host.testcontainers.internal\" to follow a link " + + "to \"github.com\". Are you sure you want to continue?", alert.getText()); + alert.dismiss(); + + driver.findElementWithoutWaiting(By.linkText("Github XWiki")).click(); + try { + driver.switchTo().alert(); + fail("No alert should be present"); + } catch (NoAlertPresentException e) { + } + + testUtils.gotoPage(testReference); + driver.findElementWithoutWaiting(By.linkText("Github platform")).click(); + try { + driver.switchTo().alert(); + fail("No alert should be present"); + } catch (NoAlertPresentException e) { + } + } }
xwiki-platform-core/xwiki-platform-url/xwiki-platform-url-api/src/main/java/org/xwiki/url/script/URLSecurityScriptService.java+42 −0 modified@@ -21,6 +21,7 @@ import java.net.URI; import java.net.URISyntaxException; +import java.util.List; import javax.inject.Inject; import javax.inject.Named; @@ -29,6 +30,8 @@ import org.slf4j.Logger; import org.xwiki.component.annotation.Component; import org.xwiki.script.service.ScriptService; +import org.xwiki.stability.Unstable; +import org.xwiki.url.URLConfiguration; import org.xwiki.url.URLSecurityManager; /** @@ -46,6 +49,9 @@ public class URLSecurityScriptService implements ScriptService @Inject private URLSecurityManager urlSecurityManager; + @Inject + private URLConfiguration urlConfiguration; + @Inject private Logger logger; @@ -71,4 +77,40 @@ public URI parseToSafeURI(String uriRepresentation) throws URISyntaxException, S return null; } } + + /** + * @return the list of trusted domains. + * @since 17.9.0RC1 + * @since 17.4.6 + * @since 16.10.13 + */ + @Unstable + public List<String> getTrustedDomains() + { + return this.urlConfiguration.getTrustedDomains(); + } + + /** + * @return {@code true} if the mechanism to enforce URLs check on frontend is enabled. + * @since 17.9.0RC1 + * @since 17.4.6 + * @since 16.10.13 + */ + @Unstable + public boolean isFrontendUrlCheckEnabled() + { + return this.urlConfiguration.isFrontendUrlCheckEnabled(); + } + + /** + * @return the list of URLs that are allowed to avoid asking confirmation to users when accessing them. + * @since 17.9.0RC1 + * @since 17.4.6 + * @since 16.10.13 + */ + @Unstable + public List<String> getAllowedFrontendUrls() + { + return this.urlConfiguration.getAllowedFrontendUrls(); + } }
xwiki-platform-core/xwiki-platform-url/xwiki-platform-url-api/src/main/java/org/xwiki/url/URLConfiguration.java+32 −2 modified@@ -19,10 +19,10 @@ */ package org.xwiki.url; -import java.util.Collections; import java.util.List; import org.xwiki.component.annotation.Role; +import org.xwiki.stability.Unstable; /** * Configuration options for the URL module. @@ -60,7 +60,7 @@ default boolean useResourceLastModificationDate() */ default List<String> getTrustedDomains() { - return Collections.emptyList(); + return List.of(); } /** @@ -88,4 +88,34 @@ default List<String> getTrustedSchemes() { return List.of("http", "https", "ftp"); } + + /** + * @return {@code true} if checks should be done in the frontend when clicking on a link to validate it's driving + * to an authorized domain. This is independent from {@link #isTrustedDomainsEnabled()} which aims at enabling + * checks server side only. + * @since 17.9.0RC1 + * @since 17.4.6 + * @since 16.10.13 + */ + @Unstable + default boolean isFrontendUrlCheckEnabled() + { + return true; + } + + /** + * Define a list of allowed frontend URLs: in case the {@link #isFrontendUrlCheckEnabled()} is enabled, then + * this list can be used to allow specific URLs without asking confirmation from the user, while avoiding to add + * an entire domain in the list of trusted domains. + * + * @return the list of allowed frontend URLs + * @since 17.9.0RC1 + * @since 17.4.6 + * @since 16.10.13 + */ + @Unstable + default List<String> getAllowedFrontendUrls() + { + return List.of(); + } }
xwiki-platform-core/xwiki-platform-url/xwiki-platform-url-api/src/main/resources/ApplicationResources.properties+21 −0 added@@ -0,0 +1,21 @@ +# --------------------------------------------------------------------------- +# 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. +# --------------------------------------------------------------------------- +url.api.followLinkConfirmationText=You are about to leave the domain "{0}" to follow a link to "{1}". Are you sure \ + you want to continue? \ No newline at end of file
xwiki-platform-core/xwiki-platform-url/xwiki-platform-url-default/src/main/java/org/xwiki/url/internal/DefaultURLConfiguration.java+13 −2 modified@@ -19,7 +19,6 @@ */ package org.xwiki.url.internal; -import java.util.Collections; import java.util.List; import javax.inject.Inject; @@ -67,7 +66,7 @@ public boolean useResourceLastModificationDate() @Override public List<String> getTrustedDomains() { - return this.configuration.get().getProperty(PREFIX + "trustedDomains", Collections.emptyList()); + return this.configuration.get().getProperty(PREFIX + "trustedDomains", List.of()); } @Override @@ -81,4 +80,16 @@ public List<String> getTrustedSchemes() { return this.configuration.get().getProperty(PREFIX + "trustedSchemes", List.of("http", "https", "ftp")); } + + @Override + public boolean isFrontendUrlCheckEnabled() + { + return this.configuration.get().getProperty(PREFIX + "frontendUrlCheckEnabled", true); + } + + @Override + public List<String> getAllowedFrontendUrls() + { + return this.configuration.get().getProperty(PREFIX + "allowedFrontendUrls", List.of()); + } }
xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-war/src/main/webapp/resources/uicomponents/link/link-protection.js+85 −0 added@@ -0,0 +1,85 @@ +/* + * 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. + */ +define('link-protection-translations', { + prefix: '', + keys: [ + 'url.api.followLinkConfirmationText' + ] +}); +require(['jquery', 'xwiki-l10n!link-protection-translations', 'xwiki-events-bridge'], function ($, l10n) { + + function protectLinks () { + let configuration = null; + try { + const trustedDomainConfigElement = $('script#trusted-domains-configuration'); + if (trustedDomainConfigElement.length > 0) { + configuration = JSON.parse(trustedDomainConfigElement.text()); + } + } catch (err) { + console.error("Error while parsing the trusted domain configurations, falling back on enforcing checks on all" + + " links going outside current domain.", err); + } + $(document).on('click', 'a[href]', function (event) { + return askIfLinkNotTrusted(event, this, configuration); + }); + } + + function askIfLinkNotTrusted (event, anchor, configuration) { + let currentHostname = window.location.hostname; + let anchorHostname = anchor.hostname; + let customizedMessage = l10n.get('url.api.followLinkConfirmationText', currentHostname, anchorHostname); + if (configuration == null && !isAnchorCurrentDomain(anchor)) { + return confirm(customizedMessage); + } else if (!isAnchorTrustedOomain(anchor, configuration.trustedDomains, configuration.allowedUrls)) { + return confirm(customizedMessage); + } else { + return true; + } + } + + function isAnchorCurrentDomain (anchor) { + let currentHostname = window.location.hostname; + let anchorHostname = anchor.hostname; + return (!anchorHostname || anchorHostname === currentHostname); + } + + function isAnchorTrustedOomain (anchor, trustedDomains, allowedUrls) { + if (isAnchorCurrentDomain(anchor)) { + return true; + } else { + if (allowedUrls.indexOf(anchor.href) > -1) { + return true; + } + let host = anchor.hostname; + do { + if (trustedDomains.indexOf(host) > -1) { + return true; + } else if (host.indexOf(".") > -1) { + host = host.substring(host.indexOf(".") + 1); + } else { + host = ""; + } + } while (host !== ""); + } + return false; + } + + (XWiki.domIsLoaded && protectLinks()) || document.observe('xwiki:dom:loaded', protectLinks); +}); \ No newline at end of file
xwiki-platform-tools/xwiki-platform-tool-configuration-resources/src/main/resources/xwiki.properties.vm+18 −0 modified@@ -1035,6 +1035,24 @@ distribution.automaticStartOnWiki=$xwikiPropertiesAutomaticStartOnWiki #-# The default is: # url.forceAllowAnyCharacter=true +#-# [Since 17.9.0RC1] +#-# [Since 17.4.6] +#-# [Since 16.10.13] +#-# Allow to enable or disable checks performed when clicking links in the UI based on the list of trusted domains. +#-# +#-# By default this property is set to true: +# url.frontendUrlCheckEnabled=true + +#-# [Since 17.9.0RC1] +#-# [Since 17.4.6] +#-# [Since 16.10.13] +#-# Allow to allow specific URLs to be accessible from the frontend without asking confirmation, and without +#-# needing to allow and entire domain. The expected format is absolute URLs separated by commas, e.g.: +#-# https://github.com/xwiki/xwiki-platform,https://www.xwiki.org/xwiki/bin/view/Main/WebHome +#-# +#-# By default this property is empty: +# url.allowedFrontendUrls= + #------------------------------------------------------------------------------------- # Attachment #-------------------------------------------------------------------------------------
Vulnerability mechanics
Synthesis attempt was rejected by the grounding validator. Re-run pending.
References
9- github.com/advisories/GHSA-74rh-c5rh-88vgghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-26000ghsaADVISORY
- github.com/xwiki/xwiki-platform/commit/29cb81f3a5387cf822d7e7534bdd63903275f86bghsaWEB
- github.com/xwiki/xwiki-platform/commit/7b5a4f8c34d9b1da3d966e17f7dbccabac448e75ghsaWEB
- github.com/xwiki/xwiki-platform/pull/4645ghsaWEB
- github.com/xwiki/xwiki-platform/releases/tag/xwiki-platform-17.4.6ghsax_refsource_MISCWEB
- github.com/xwiki/xwiki-platform/security/advisories/GHSA-74rh-c5rh-88vgghsax_refsource_CONFIRMWEB
- jira.xwiki.org/browse/XWIKI-23433ghsaWEB
- www.xwiki.org/xwiki/bin/view/ReleaseNotes/Data/XWiki/17.9.0RC1/Entry006ghsaWEB
News mentions
0No linked articles in our index yet.