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 @@ -14,7 +14,8 @@ adminFeature.themes_management.description=Theme Management
################################################################################
# Messages
message.stylesheetAlreadyExists=A file with the same mode associated with this style already exists! <br> You must change the mode or create a new style.
message.stylesheetNotValid=The selected XSL stylesheet is not valid! <br /> The retrieved error is: <br /> {0}.
message.stylesheetNotValid=The selected XSL stylesheet is not valid.
message.stylesheetSecurityViolation=The XSL file contains unsupported constructs.
message.stylesheetConfirmDelete=Deleting the style will also delete the associated stylesheet ''{0}''.<br />Do you confirm the deletion of this stylesheet?
message.cannotDeleteStylePorlets=Unable to delete this style. There are still sections using this style! <br />You must modify the style of these sections before deleting.
message.confirmDeleteStyle=Are you sure you want to delete this style?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ adminFeature.themes_management.description=Gestion des th\u00e8mes
################################################################################
# Messages
message.stylesheetAlreadyExists=Il existe d\u00e9j\u00e0 un fichier xsl du m\u00eame mode associ\u00e9 \u00e0 ce style \!<br> Vous devez changer de mode ou cr\u00e9er un nouveau style.
message.stylesheetNotValid=La feuille de style XSL s\u00e9lectionn\u00e9e n''est pas valide \! <br /> L''erreur r\u00e9cup\u00e9r\u00e9e est \:<br /> {0}.
message.stylesheetNotValid=La feuille de style XSL s\u00e9lectionn\u00e9e n'est pas valide.
message.stylesheetSecurityViolation=Le fichier XSL contient des constructions non support\u00e9es.
message.stylesheetConfirmDelete=La suppression du style va entra\u00eener la suppression de la feuille de style ''{0}'' associ\u00e9e.<br />Confirmez-vous la suppression de cette feuille style ?
message.cannotDeleteStylePorlets=Impossible de supprimer ce style. Il y a encore des rubriques utilisant ce style \! <br />Vous devez modifier le style de ces rubriques avant d'effectuer la suppression.
message.confirmDeleteStyle=Etes-vous s\u00fbr de vouloir supprimer ce style ?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ permission.label.modify_xsl_export=Modify a transformation stylesheet
# Messages
message.confirm_remove_xsl_export=Are you sure you want to delete this style sheet?
message.permission_denied=You are not authorized to access this feature.
message.xml_not_valid=The file is not a valid XML file
message.xml_not_valid=Invalid XML file.
message.xsl_security_violation=The XSL file contains unsupported constructs.
message.errorNumberColumns=This line has {0} columns when it should have {1}
message.errorUnknown=An error occurred while processing the line

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ permission.label.modify_xsl_export=Modifier une feuille de transformation
# Messages
message.confirm_remove_xsl_export=\u00cates-vous s\u00fbr de vouloir supprimer cette feuille de style ?
message.permission_denied=Vous n'\u00eates pas autoris\u00e9 \u00e0 acc\u00e9der \u00e0 cette fonctionnalit\u00e9.
message.xml_not_valid=Le fichier n'est pas un fichier XML valide
message.xml_not_valid=Fichier XML invalide.
message.xsl_security_violation=Le fichier XSL contient des constructions non support\u00e9es.
message.errorNumberColumns=Cette ligne a {0} colonnes alors qu''elle devrait en avoir {1}
message.errorUnknown=Une erreur est survenue lors du traitement de la ligne

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public final class XmlTransformerService
{
private static final String XSLSOURCE_STYLE_PREFIX_ID = UniqueIDGenerator.getNewId( );
private static final String LOGGER_XML_CONTENT = "lutece.debug.xmlContent";
private static final String ERROR_MESSAGE_XSL_TRANSFORMATION = "An error occurred during XSL transformation. The requested operation was blocked for security reasons.";
private static final Logger _log = LogManager.getLogger( LOGGER_XML_CONTENT );

/**
Expand Down Expand Up @@ -163,8 +164,8 @@ public String transformBySourceWithXslCache( String strXml, Source sourceStyleSh
}
catch( Exception e )
{
strContent = e.getMessage( );
AppLogService.error( e.getMessage( ), e );
strContent = ERROR_MESSAGE_XSL_TRANSFORMATION;
AppLogService.error( "XSL transformation error for stylesheet {}", strStyleSheetId, e );
}

return strContent;
Expand Down Expand Up @@ -197,8 +198,8 @@ public String transformBySourceWithXslCache( Source sourceXml, Source sourceStyl
}
catch( Exception e )
{
strContent = e.getMessage( );
AppLogService.error( e.getMessage( ), e );
strContent = ERROR_MESSAGE_XSL_TRANSFORMATION;
AppLogService.error( "XSL transformation error for stylesheet {}", strStyleSheetId, e );
}

return strContent;
Expand Down
224 changes: 224 additions & 0 deletions src/java/fr/paris/lutece/portal/service/xsl/XslSecurityService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/*
* Copyright (c) 2002-2025, City of Paris
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright notice
* and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice
* and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* License 1.0
*/
package fr.paris.lutece.portal.service.xsl;

import fr.paris.lutece.portal.service.util.AppLogService;

import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
* Service for validating XSL files against security threats.
* Detects dangerous constructs such as extension namespaces, script elements,
* document() calls, and xsl:result-document before the stylesheet reaches
* the XSLT processor.
*/
public final class XslSecurityService
{
private static final String XSL_NAMESPACE = "http://www.w3.org/1999/XSL/Transform";

private static final List<String> BLACKLISTED_NAMESPACE_PREFIXES = List.of(
"http://xml.apache.org/xalan",
"http://xml.apache.org/xslt",
"http://xml.apache.org/xalan/java",
"http://saxon.sf.net/",
"http://icl.com/saxon",
"http://exslt.org/common",
"http://exslt.org/functions",
"http://exslt.org/dynamic",
"urn:schemas-microsoft-com:xslt",
"http://www.w3.org/TR/xslt-30"
);

private static final String DOCUMENT_FUNCTION_PATTERN = "document(";

/**
* Checks whether a namespace URI matches any blacklisted prefix.
* Uses startsWith to catch URIs like {@code http://xml.apache.org/xalan/java/java.lang.Runtime}.
*
* @param uri
* the namespace URI to check
* @return true if the URI starts with any blacklisted prefix
*/
private static boolean isBlacklistedNamespace( String uri )
{
return BLACKLISTED_NAMESPACE_PREFIXES.stream( ).anyMatch( uri::startsWith );
}

/**
* Private constructor.
*/
private XslSecurityService( )
{
}

/**
* Validates an XSL stylesheet for security threats.
*
* @param baXslSource
* the XSL source as a byte array
* @return a list of security violations found, empty if the stylesheet is safe
*/
public static List<String> validateXslSecurity( byte [ ] baXslSource )
{
List<String> listViolations = new ArrayList<>( );

try
{
SAXParserFactory factory = SAXParserFactory.newInstance( );
factory.setNamespaceAware( true );
factory.setFeature( "http://apache.org/xml/features/disallow-doctype-decl", true );
factory.setFeature( "http://xml.org/sax/features/external-general-entities", false );
factory.setFeature( "http://xml.org/sax/features/external-parameter-entities", false );

SAXParser parser = factory.newSAXParser( );
XslSecurityHandler handler = new XslSecurityHandler( listViolations );
InputSource is = new InputSource( new ByteArrayInputStream( baXslSource ) );
parser.parse( is, handler );
}
catch( SAXException e )
{
if ( e.getMessage( ) != null && e.getMessage( ).contains( "DOCTYPE" ) )
{
listViolations.add( "DOCTYPE declaration detected" );
}
else
{
listViolations.add( "XML parsing error: " + e.getClass( ).getSimpleName( ) );
}
AppLogService.error( "XSL security validation error: {}", e.getMessage( ), e );
}
catch( Exception e )
{
listViolations.add( "XML parsing error: " + e.getClass( ).getSimpleName( ) );
AppLogService.error( "XSL security validation error: {}", e.getMessage( ), e );
}

return listViolations;
}

/**
* SAX handler that detects dangerous XSL constructs during parsing.
*/
private static class XslSecurityHandler extends DefaultHandler
{
private final List<String> _listViolations;

/**
* Constructor.
*
* @param listViolations
* the list to collect violations into
*/
XslSecurityHandler( List<String> listViolations )
{
_listViolations = listViolations;
}

/**
* {@inheritDoc}
*/
@Override
public void startPrefixMapping( String prefix, String uri ) throws SAXException
{
if ( isBlacklistedNamespace( uri ) )
{
_listViolations.add( "Forbidden extension namespace: " + uri + " (prefix: " + prefix + ")" );
AppLogService.error( "XSL security: blocked extension namespace {} (prefix: {})", uri, prefix );
}
}

/**
* {@inheritDoc}
*/
@Override
public void startElement( String uri, String localName, String qName, Attributes attributes ) throws SAXException
{
checkForbiddenElement( uri, localName );
checkDocumentFunction( attributes );
}

/**
* Checks if the element is a forbidden script or result-document element.
*
* @param uri
* the namespace URI
* @param localName
* the local name of the element
*/
private void checkForbiddenElement( String uri, String localName )
{
if ( "script".equals( localName ) && isBlacklistedNamespace( uri ) )
{
_listViolations.add( "Forbidden script element: {" + uri + "}" + localName );
AppLogService.error( "XSL security: blocked script element {{}}{}", uri, localName );
}

if ( XSL_NAMESPACE.equals( uri ) && "result-document".equals( localName ) )
{
_listViolations.add( "Forbidden xsl:result-document element" );
AppLogService.error( "XSL security: blocked xsl:result-document element" );
}
}

/**
* Checks all attributes for the presence of the document() function.
*
* @param attributes
* the element attributes
*/
private void checkDocumentFunction( Attributes attributes )
{
for ( int i = 0; i < attributes.getLength( ); i++ )
{
String strValue = attributes.getValue( i );

if ( strValue != null && strValue.contains( DOCUMENT_FUNCTION_PATTERN ) )
{
_listViolations.add( "Forbidden document() function call in attribute " + attributes.getQName( i ) );
AppLogService.error( "XSL security: blocked document() call in attribute {}", attributes.getQName( i ) );
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
import fr.paris.lutece.portal.service.security.SecurityTokenService;
import fr.paris.lutece.portal.service.template.AppTemplateService;
import fr.paris.lutece.portal.service.util.AppLogService;
import fr.paris.lutece.portal.service.xsl.XslSecurityService;
import fr.paris.lutece.portal.web.admin.AdminFeaturesPageJspBean;
import fr.paris.lutece.portal.web.cdi.mvc.Models;
import fr.paris.lutece.portal.web.constants.Messages;
Expand Down Expand Up @@ -117,6 +118,7 @@ public class StyleSheetJspBean extends AdminFeaturesPageJspBean
private static final String PROPERTY_STYLESHEETS_PER_PAGE = "paginator.stylesheet.itemsPerPage";
private static final String MESSAGE_STYLESHEET_ALREADY_EXISTS = "portal.style.message.stylesheetAlreadyExists";
private static final String MESSAGE_STYLESHEET_NOT_VALID = "portal.style.message.stylesheetNotValid";
private static final String MESSAGE_STYLESHEET_SECURITY_VIOLATION = "portal.style.message.stylesheetSecurityViolation";
private static final String MESSAGE_CONFIRM_DELETE_STYLESHEET = "portal.style.message.stylesheetConfirmDelete";
private static final String LABEL_ALL = "portal.util.labelAll";
private static final String JSP_DO_REMOVE_STYLESHEET = "jsp/admin/style/DoRemoveStyleSheet.jsp";
Expand Down Expand Up @@ -276,11 +278,15 @@ private String getData( MultipartHttpServletRequest multipartRequest, StyleSheet
// Check the XML validity of the XSL stylesheet
if ( isValid( baXslSource ) != null )
{
Object [ ] args = {
isValid( baXslSource )
};
return AdminMessageService.getMessageUrl( multipartRequest, MESSAGE_STYLESHEET_NOT_VALID, AdminMessage.TYPE_STOP );
}

// Check the XSL stylesheet for security threats
List<String> listSecurityViolations = XslSecurityService.validateXslSecurity( baXslSource );

return AdminMessageService.getMessageUrl( multipartRequest, MESSAGE_STYLESHEET_NOT_VALID, args, AdminMessage.TYPE_STOP );
if ( !listSecurityViolations.isEmpty( ) )
{
return AdminMessageService.getMessageUrl( multipartRequest, MESSAGE_STYLESHEET_SECURITY_VIOLATION, AdminMessage.TYPE_STOP );
}

stylesheet.setDescription( strDescription );
Expand Down Expand Up @@ -436,16 +442,17 @@ private String isValid( byte [ ] baXslSource )
try
{
SAXParserFactory factory = SAXParserFactory.newInstance( );
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature( "http://apache.org/xml/features/disallow-doctype-decl", true );
factory.setFeature( "http://xml.org/sax/features/external-general-entities", false );
factory.setFeature( "http://xml.org/sax/features/external-parameter-entities", false );
SAXParser analyzer = factory.newSAXParser( );
InputSource is = new InputSource( new ByteArrayInputStream( baXslSource ) );
analyzer.getXMLReader( ).parse( is );
}
catch( Exception e )
{
strError = e.getMessage( );
AppLogService.debug( e.getMessage( ), e );
strError = "invalid XSL stylesheet";
AppLogService.error( "XSL validation error: {}", e.getMessage( ), e );
}

return strError;
Expand Down
Loading