Skip to content
Open
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 @@ -120,6 +120,7 @@ modify_properties.boxTitle=Site properties modification
modify_properties.keyLabel=Key
modify_properties.keyValue=Value
modify_properties.buttonLabel=Edit
message.invalidColorFormat=The color value is not valid. Accepted formats: #RGB, #RRGGBB, #RRGGBBAA, or rgb()/rgba().

# Template manage_plugins
manage_plugins.titleCorePlugin=Lutece Core
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ modify_properties.boxTitle=Modification des propri\u00e9t\u00e9s du site
modify_properties.keyLabel=Cl\u00e9
modify_properties.keyValue=Valeur
modify_properties.buttonLabel=Modifier
message.invalidColorFormat=La valeur de couleur n''est pas valide. Formats accept\u00e9s : #RGB, #RRGGBB, #RRGGBBAA, ou rgb()/rgba().

# Template manage_plugins
manage_plugins.titleCorePlugin=C\u0153ur Lutece
Expand Down
110 changes: 107 additions & 3 deletions src/java/fr/paris/lutece/portal/web/system/SystemJspBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
Expand All @@ -60,6 +63,8 @@
import fr.paris.lutece.portal.service.file.FileService;
import fr.paris.lutece.portal.service.file.IFileStoreServiceProvider;
import fr.paris.lutece.portal.service.i18n.I18nService;
import fr.paris.lutece.portal.service.message.AdminMessage;
import fr.paris.lutece.portal.service.message.AdminMessageService;
import fr.paris.lutece.portal.service.security.SecurityTokenService;
import fr.paris.lutece.portal.service.site.properties.SitePropertiesService;
import fr.paris.lutece.portal.service.template.AppTemplateService;
Expand All @@ -70,6 +75,7 @@
import fr.paris.lutece.portal.web.admin.AdminFeaturesPageJspBean;
import fr.paris.lutece.util.html.HtmlTemplate;
import fr.paris.lutece.util.http.SecurityUtil;
import fr.paris.lutece.util.string.StringUtil;

/**
* This class provides the user interface to manage system features ( manage logs, view system files, ... ).
Expand All @@ -87,6 +93,7 @@ public class SystemJspBean extends AdminFeaturesPageJspBean

// Markers
private static final String MARK_PROPERTIES_GROUPS_LIST = "groups_list";
private static final String MARK_BYPASS_XSS_KEYS = "bypass_xss_keys";

// Template
private static final String TEMPLATE_MODIFY_PROPERTIES = "admin/system/modify_properties.html";
Expand All @@ -95,6 +102,26 @@ public class SystemJspBean extends AdminFeaturesPageJspBean
private static final String MARK_WEBAPP_URL = "webapp_url";
private static final String MARK_LOCALE = "locale";

// Site properties keys requiring XSS bypass decoding
private static final Set<String> BYPASS_XSS_KEYS = Set.of(
"portal.site.site_property.home_url",
"portal.site.site_property.admin_home_url"
);

// Property for extending the bypass whitelist
private static final String PROPERTY_XSS_BYPASS_ADDITIONAL_KEYS = "portal.site.site_property.xss.bypass.keys";
private static final String SITE_PROPERTY_PREFIX = "portal.site.site_property.";

// Color validation
private static final String MESSAGE_INVALID_COLOR_FORMAT = "portal.system.message.invalidColorFormat";
private static final Pattern COLOR_PATTERN = Pattern.compile( "^(#[0-9a-fA-F]{3,8}|rgba?\\([^)]+\\))?$" );
private static final Set<String> COLOR_KEYS = Set.of(
"portal.site.site_property.banner.title.color",
"portal.site.site_property.banner.title.bgcolor",
"portal.theme.site_property.banner.title.color",
"portal.theme.site_property.banner.title.bgcolor"
);


/**
* Returns the form to update site properties in DataStore
Expand All @@ -105,8 +132,10 @@ public class SystemJspBean extends AdminFeaturesPageJspBean
*/
public String getManageProperties( HttpServletRequest request )
{
List<LocalizedDataGroup> groups = SitePropertiesService.getGroups( getLocale( ) );
Map<String, Object> model = new HashMap<>( );
model.put( MARK_PROPERTIES_GROUPS_LIST, SitePropertiesService.getGroups( getLocale( ) ) );
model.put( MARK_PROPERTIES_GROUPS_LIST, groups );
model.put( MARK_BYPASS_XSS_KEYS, getEffectiveBypassKeys( groups ) );
model.put( MARK_WEBAPP_URL, AppPathService.getBaseUrl( request ) );
model.put( MARK_LOCALE, getLocale( ).getLanguage( ) );
model.put( SecurityTokenService.MARK_TOKEN, SecurityTokenService.getInstance( ).getToken( request, TEMPLATE_MODIFY_PROPERTIES ) );
Expand Down Expand Up @@ -134,6 +163,7 @@ public static String doModifyProperties( HttpServletRequest request, ServletCont
throw new AccessDeniedException( ERROR_INVALID_TOKEN );
}
List<LocalizedDataGroup> groups = SitePropertiesService.getGroups( AdminUserService.getAdminUser( request ).getLocale( ) );
Set<String> effectiveBypassKeys = getEffectiveBypassKeys( groups );

for ( LocalizedDataGroup group : groups )
{
Expand All @@ -143,14 +173,88 @@ public static String doModifyProperties( HttpServletRequest request, ServletCont
{
String strValue = request.getParameter( data.getKey( ) );

if ( ( strValue != null ) && !data.getValue( ).equals( strValue ) )
if ( strValue != null )
{
DatastoreService.setDataValue( data.getKey( ), strValue );
String strKey = data.getKey( );

if ( effectiveBypassKeys.contains( strKey ) )
{
strValue = StringUtil.decodeXssBypass( strValue );
}

if ( COLOR_KEYS.contains( strKey ) && strValue != null && !strValue.isEmpty( )
&& !COLOR_PATTERN.matcher( strValue ).matches( ) )
{
return AdminMessageService.getMessageUrl( request, MESSAGE_INVALID_COLOR_FORMAT, AdminMessage.TYPE_STOP );
}

if ( strValue != null && !strValue.equals( data.getValue( ) ) )
{
DatastoreService.setDataValue( strKey, strValue );
}
}
}
}

// if the operation occurred well, redirects towards the view of the Properties
return JSP_MANAGE_PROPERTIES;
}

/**
* Builds the effective set of site property keys that require XSS bypass decoding.
* Merges the hardcoded keys with additional keys declared via the
* {@value #PROPERTY_XSS_BYPASS_ADDITIONAL_KEYS} property. Additional keys are validated
* against the declared site properties: keys with an invalid prefix or that do not match
* any known site property are logged as errors and ignored.
*
* @param groups
* the list of site property groups used to validate additional keys
* @return the merged set of bypass keys
*/
private static Set<String> getEffectiveBypassKeys( List<LocalizedDataGroup> groups )
{
String strAdditionalKeys = AppPropertiesService.getProperty( PROPERTY_XSS_BYPASS_ADDITIONAL_KEYS, "" );

if ( strAdditionalKeys.isEmpty( ) )
{
return BYPASS_XSS_KEYS;
}

Set<String> validSiteKeys = new HashSet<>( );
for ( LocalizedDataGroup group : groups )
{
for ( LocalizedData data : group.getLocalizedDataList( ) )
{
validSiteKeys.add( data.getKey( ) );
}
}

Set<String> effectiveKeys = new HashSet<>( BYPASS_XSS_KEYS );

for ( String strKey : strAdditionalKeys.split( "," ) )
{
String strTrimmedKey = strKey.trim( );

if ( strTrimmedKey.isEmpty( ) )
{
continue;
}

if ( !strTrimmedKey.startsWith( SITE_PROPERTY_PREFIX ) )
{
AppLogService.error( "XSS bypass key '{}' does not start with required prefix '{}' — ignoring", strTrimmedKey, SITE_PROPERTY_PREFIX );
continue;
}

if ( !validSiteKeys.contains( strTrimmedKey ) )
{
AppLogService.error( "XSS bypass key '{}' does not match any declared site property — ignoring", strTrimmedKey );
continue;
}

effectiveKeys.add( strTrimmedKey );
}

return effectiveKeys;
}
}
16 changes: 8 additions & 8 deletions src/sql/init_db_lutece_core.sql
Original file line number Diff line number Diff line change
Expand Up @@ -163,17 +163,17 @@ INSERT INTO core_datastore VALUES ('core.frontOffice.defaultEditor', 'sceeditor'
INSERT INTO core_datastore VALUES ('core_banned_domain_names', 'yopmail.com');

INSERT INTO core_datastore VALUES ('portal.site.site_property.name', 'LUTECE');
INSERT INTO core_datastore VALUES ('portal.site.site_property.meta.author', '<author>');
INSERT INTO core_datastore VALUES ('portal.site.site_property.meta.copyright', '<copyright>');
INSERT INTO core_datastore VALUES ('portal.site.site_property.meta.description', '<description>');
INSERT INTO core_datastore VALUES ('portal.site.site_property.meta.keywords', '<keywords>');
INSERT INTO core_datastore VALUES ('portal.site.site_property.email', '<webmaster email>');
INSERT INTO core_datastore VALUES ('portal.site.site_property.meta.author', 'author');
INSERT INTO core_datastore VALUES ('portal.site.site_property.meta.copyright', 'copyright');
INSERT INTO core_datastore VALUES ('portal.site.site_property.meta.description', 'description');
INSERT INTO core_datastore VALUES ('portal.site.site_property.meta.keywords', 'keywords');
INSERT INTO core_datastore VALUES ('portal.site.site_property.email', 'webmaster@mydomain.com');
INSERT INTO core_datastore VALUES ('portal.site.site_property.noreply_email', 'no-reply@mydomain.com');
INSERT INTO core_datastore VALUES ('portal.site.site_property.home_url', 'jsp/site/Portal.jsp');
INSERT INTO core_datastore VALUES ('portal.site.site_property.admin_home_url', 'jsp/admin/AdminMenu.jsp');
INSERT INTO core_datastore VALUES ('portal.site.site_property.popup_credits.textblock', '&lt;credits text&gt;');
INSERT INTO core_datastore VALUES ('portal.site.site_property.popup_legal_info.copyright.textblock', '&lt;copyright text&gt;');
INSERT INTO core_datastore VALUES ('portal.site.site_property.popup_legal_info.privacy.textblock', '&lt;privacy text&gt;');
INSERT INTO core_datastore VALUES ('portal.site.site_property.popup_credits.textblock', 'credits text');
INSERT INTO core_datastore VALUES ('portal.site.site_property.popup_legal_info.copyright.textblock', 'copyright text');
INSERT INTO core_datastore VALUES ('portal.site.site_property.popup_legal_info.privacy.textblock', 'privacy text');
INSERT INTO core_datastore VALUES ('portal.site.site_property.logo_url', 'themes/admin/shared/images/logo-header-icon.svg');
INSERT INTO core_datastore VALUES ('portal.site.site_property.locale.default', 'fr');
INSERT INTO core_datastore VALUES ('portal.site.site_property.avatar_default', 'themes/admin/shared/images/unknown.svg');
Expand Down
8 changes: 7 additions & 1 deletion webapp/WEB-INF/conf/lutece.properties
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,10 @@ lutece.safe.request.admin.xssCharacters=<>#"&

lutece.safe.request.site.activateXssFilter=true
lutece.safe.request.site.sanitizeFilterMode=false
lutece.safe.request.site.xssCharacters=<>#"&
lutece.safe.request.site.xssCharacters=<>#"&

# Additional site property keys to bypass the XSS filter on the Site Properties form.
# Comma-separated list of full datastore keys (must start with portal.site.site_property.).
# Keys that do not match a declared site property are ignored.
# Example: portal.site.site_property.xss.bypass.keys=portal.site.site_property.custom_url,portal.site.site_property.redirect_url
portal.site.site_property.xss.bypass.keys=
11 changes: 6 additions & 5 deletions webapp/WEB-INF/templates/admin/system/modify_properties.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<@pageContainer>
<@pageColumn>
<@pageColumn>
<@pageHeader title='#i18n{portal.system.modify_properties.boxTitle}'>
<@tform>
<@formGroup labelFor='plugin_type' labelKey='#i18n{portal.system.manage_plugins.buttonFilter}' hideLabel=['all'] formStyle='inline'>
Expand Down Expand Up @@ -49,10 +49,11 @@ <h3>${groupName} </h3>
<@box>
<@boxBody class='searchable'>
<@formGroup class='property' labelKey=labelText labelFor=groupItem.key helpKey=helpText params=' data-property="${groupItem.key}"' rows=2>
<#assign needsBypass = bypass_xss_keys?seq_contains( groupItem.key ) />
<#if groupItem.key?ends_with( ".textblock" )>
<@input type='textarea' name=groupItem.key id=groupItem.key>${groupItem.value?html}</@input>
<@input type='textarea' name=groupItem.key id=groupItem.key bypassXssFilter=needsBypass>${groupItem.value?html}</@input>
<#elseif groupItem.key?ends_with( ".htmlblock" )>
<@input type='textarea' name=groupItem.key id=groupItem.key class='richtext'>${groupItem.value?html}</@input>
<@input type='textarea' name=groupItem.key id=groupItem.key class='richtext' bypassXssFilter=needsBypass>${groupItem.value?html}</@input>
<#elseif groupItem.key?ends_with( ".checkbox" )>
<#if groupItem.value == "1">
<@checkBox orientation='switch' name=groupItem.key id=groupItem.key labelKey=groupItem.label value='1' checked=true />
Expand All @@ -62,14 +63,14 @@ <h3>${groupName} </h3>
<@input type='hidden' name='${groupItem.key}' value='0' />
<#elseif groupItem.key?contains( "color" )>
<@inputGroup class='color-wrapper-input'>
<@input type='text' class='color-input' name=groupItem.key id=groupItem.key value=groupItem.value?html />
<@input type='text' class='color-input' name=groupItem.key id=groupItem.key value=groupItem.value?html bypassXssFilter=needsBypass />
<@inputGroupItem>
<!-- <@input type='color' class='color-input' name='color-input' value=groupItem.value?html /> -->
<input type="color" class="color-input" name="color-input" value="${groupItem.value?html}" colorspace="display-p3" alpha>
</@inputGroupItem>
</@inputGroup>
<#else>
<@input type='text' name=groupItem.key id=groupItem.key value=groupItem.value?html />
<@input type='text' name=groupItem.key id=groupItem.key value=groupItem.value?html bypassXssFilter=needsBypass />
</#if>
</@formGroup>
</@boxBody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Parameters:
<@div class="card-body p-5 fs-6">
<@div class="text-center mb-4">
<@link href='/' target='_blank'>
<img src="${dskey('portal.site.site_property.logo_url')}" height="40" alt="Logo" aria-hidden="true" >
<img src="${logoUrl}" height="40" alt="Logo" aria-hidden="true" >
<span class="visually-hidden">${site_name!'Lutece'}</span>
</@link>
</@div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ const password = new LutecePassword();

document.addEventListener( "DOMContentLoaded", function(){
/* backGround image random */
const aImages = '#dskey{portal.site.site_property.layout.login.image}'.split(',');
const backImages = '#dskey{portal.site.site_property.back_images}'.split(',');
const aImages = '${dskey('portal.site.site_property.layout.login.image')?js_string}'.split(',');
const backImages = '${dskey('portal.site.site_property.back_images')?js_string}'.split(',');
login.randomImages = aImages;
login.randomBgImages = backImages;
<#if loginLayoutImg != '' >
Expand Down