Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,17 @@ $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()),
"whitelistedUrls": $jsontool.serialize($services.security.url.getWhitelistedFrontendUrls()),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment here but I'll also add it to other places: It would be nice to avoid the term "white" and use "allowed" instead as it is much clearer and doesn't have any bad connotations. See also https://en.wikipedia.org/wiki/Whitelist#Controversy_regarding_name

"confirmationText": $jsontool.serialize($services.localization.render('url.api.followLinkConfirmationText',
['__CURRENT_DOMAIN__', '__EXTERNAL_DOMAIN__']))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason for not using the standard translations mechanism in JavaScript? If yes, it might be good to add a comment.

}
</script>
$xwiki.jsfx.use('uicomponents/link/link-protection.js')##
#end
##
## Hooks for inserting JavaScript skin extensions
##
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -60,7 +60,7 @@ default boolean useResourceLastModificationDate()
*/
default List<String> getTrustedDomains()
{
return Collections.emptyList();
return List.of();
}

/**
Expand Down Expand Up @@ -88,4 +88,30 @@ 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
*/
@Unstable
default boolean isFrontendUrlCheckEnabled()
{
return true;
}

/**
* Define a list of whitelisted 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 whitelisted frontend URLs
* @since 17.9.0RC1
*/
@Unstable
default List<String> getWhitelistedFrontendUrls()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as before, I would prefer "allowed" instead of "white".

{
return List.of();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;

import javax.inject.Inject;
import javax.inject.Named;
Expand All @@ -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;

/**
Expand All @@ -46,6 +49,9 @@ public class URLSecurityScriptService implements ScriptService
@Inject
private URLSecurityManager urlSecurityManager;

@Inject
private URLConfiguration urlConfiguration;

@Inject
private Logger logger;

Expand All @@ -71,4 +77,34 @@ public URI parseToSafeURI(String uriRepresentation) throws URISyntaxException, S
return null;
}
}

/**
* @return the list of trusted domains.
* @since 17.9.0RC1
*/
@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
*/
@Unstable
public boolean isFrontendUrlCheckEnabled()
{
return this.urlConfiguration.isFrontendUrlCheckEnabled();
}

/**
* @return the list of URLs that are whitelisted to avoid asking confirmation to users when accessing them.
* @since 17.9.0RC1
*/
@Unstable
public List<String> getWhitelistedFrontendUrls()
{
return this.urlConfiguration.getWhitelistedFrontendUrls();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned before, I would prefer to avoid the term "white" and use "allowed" instead.

}
}
Original file line number Diff line number Diff line change
@@ -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 to \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Are you sure you want to continue?" sounds better as second sentence for me.

continue?
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
*/
package org.xwiki.url.internal;

import java.util.Collections;
import java.util.List;

import javax.inject.Inject;
Expand Down Expand Up @@ -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
Expand All @@ -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> getWhitelistedFrontendUrls()
{
return this.configuration.get().getProperty(PREFIX + "whitelistedFrontendUrls", List.of());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as before, I would prefer "allowed" instead of "white".

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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.
*/
require(['jquery', 'xwiki-events-bridge'], function($) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
require(['jquery', 'xwiki-events-bridge'], function($) {
require(['jquery', 'xwiki-events-bridge'], function ($) {


let protectLinks = function(elements) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let protectLinks = function(elements) {
let protectLinks = function (elements) {

let trustedDomainConfigElement = $('#trusted-domains-configuration');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make this more explicit with

Suggested change
let trustedDomainConfigElement = $('#trusted-domains-configuration');
let trustedDomainConfigElement = $('script#trusted-domains-configuration');

to avoid that somebody creates another element in the document with the same ID - selecting for script elements acts as a protection here as we don't allow creating script elements easily. It's probably unlikely as you would probably still take the first one, and it shouldn't be possible to insert another element before the correct one.

Comment on lines +22 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't those let be const?

if (trustedDomainConfigElement.length > 0) {
let configuration = JSON.parse(trustedDomainConfigElement.text());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you handle parsing issues?
Asking since we encountered some issues where a failed parsing was breaking unrelated pieces of JS.

elements.find('a[href]').on('click', function (event) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this event listener shouldn't be registered on document instead of registering it on every link:

Suggested change
elements.find('a[href]').on('click', function (event) {
$(document).on('click', 'a[href]', function (event) {

return askIfLinkNotTrusted(event, this, configuration);
});
}
};

let askIfLinkNotTrusted = function (event, anchor, configuration) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why assign an anonymous function to a mutable variable?

Suggested change
let askIfLinkNotTrusted = function (event, anchor, configuration) {
function askIfLinkNotTrusted (event, anchor, configuration) {

if (!isAnchorTrustedOomain(anchor, configuration.trustedDomains, configuration.whitelistedUrls)) {
let currentHostname = window.location.hostname;
let anchorHostname = anchor.hostname;
let customizedMessage = configuration.confirmationText
.replace('__CURRENT_DOMAIN__', currentHostname)
.replace('__EXTERNAL_DOMAIN__', anchorHostname);
return confirm(customizedMessage);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if it wouldn't be nicer to use a custom styled <dialog>. But it would probably interfere with the nice property here that the original event continues instead of having a new one where all open in new tab properties etc. would be lost.

} else {
return true;
}
};

let isAnchorTrustedOomain = function (anchor, trustedDomains, whitelistedUrls) {
let currentHostname = window.location.hostname;
let anchorHostname = anchor.hostname;
if (currentHostname === anchorHostname) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't there be a check if anchorHostname is set at all?

return true;
} else {
for (let i = 0; i < whitelistedUrls.length; i++) {
if (anchor.href.startsWith(whitelistedUrls[i])) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the configuration it isn't mentioned that the allowed URLs would be checked as prefix only.

return true;
}
}
for (let i = 0; i < trustedDomains.length; i++) {
if (anchorHostname.indexOf(trustedDomains[i]) !== -1) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not what I would expect how the check should work. This code would trust myxwiki.org when you actually wanted to just trust xwiki.org and it would also trust xwiki.org.attacker.domain.

return true;
}
}
}
return false;
};

$(document).on('xwiki:dom:updated', function(event, data) {
protectLinks($(data.elements));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above, just listening on document is probably easier and would avoid this.

});
protectLinks($('body'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this wait for the document to be ready?

});
Original file line number Diff line number Diff line change
Expand Up @@ -1035,6 +1035,20 @@ distribution.automaticStartOnWiki=$xwikiPropertiesAutomaticStartOnWiki
#-# The default is:
# url.forceAllowAnyCharacter=true

#-# [Since 17.9.0RC1]
#-# 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]
#-# Allow to whitelist specific URLs to be accessible from the frontend without asking confirmation, and without
#-# needing to whitelist 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.whitelistedFrontendUrls=
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above regarding "whitelisted" vs. "allowed".


#-------------------------------------------------------------------------------------
# Attachment
#-------------------------------------------------------------------------------------
Expand Down