From 2a673db7b0494b23ecca1f7fc02b2f6797517d28 Mon Sep 17 00:00:00 2001 From: evdngsl <158455076+evdngsl@users.noreply.github.com> Date: Tue, 29 Oct 2024 16:59:13 +0100 Subject: [PATCH] LUT-28902 : Security token filter --- .../AdminTokenAccessDeniedException.java | 56 ++++ .../service/content/XPageAppService.java | 11 +- .../service/message/SiteMessageHandler.java | 7 + .../security/SecurityTokenFilterAdmin.java | 132 ++++++++ .../security/SecurityTokenFilterSite.java | 80 +++++ .../security/SecurityTokenHandler.java | 282 ++++++++++++++++++ .../security/SecurityTokenService.java | 2 +- .../service/template/AppTemplateService.java | 16 + .../util/mvc/admin/MVCAdminJspBean.java | 60 +++- .../util/mvc/commons/annotations/Action.java | 4 + .../portal/util/mvc/xpage/MVCApplication.java | 46 ++- .../portal/web/admin/AdminMessageJspBean.java | 10 +- webapp/WEB-INF/conf/lutece.properties | 5 + webapp/WEB-INF/web.xml | 40 ++- 14 files changed, 733 insertions(+), 18 deletions(-) create mode 100644 src/java/fr/paris/lutece/portal/service/admin/AdminTokenAccessDeniedException.java create mode 100644 src/java/fr/paris/lutece/portal/service/security/SecurityTokenFilterAdmin.java create mode 100644 src/java/fr/paris/lutece/portal/service/security/SecurityTokenFilterSite.java create mode 100644 src/java/fr/paris/lutece/portal/service/security/SecurityTokenHandler.java diff --git a/src/java/fr/paris/lutece/portal/service/admin/AdminTokenAccessDeniedException.java b/src/java/fr/paris/lutece/portal/service/admin/AdminTokenAccessDeniedException.java new file mode 100644 index 0000000000..7df28f5549 --- /dev/null +++ b/src/java/fr/paris/lutece/portal/service/admin/AdminTokenAccessDeniedException.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2002-2022, 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.admin; + +/** + * Exception thrown when the user can't access a feature. + */ +public class AdminTokenAccessDeniedException extends RuntimeException +{ + /** + * + */ + private static final long serialVersionUID = 323303358249337792L; + + /** + * Builds a AccessDeniedException with the message + * + * @param strMessage + * the message + */ + public AdminTokenAccessDeniedException( String strMessage ) + { + super( strMessage ); + } +} diff --git a/src/java/fr/paris/lutece/portal/service/content/XPageAppService.java b/src/java/fr/paris/lutece/portal/service/content/XPageAppService.java index 5d418d3eef..a4888e63a0 100644 --- a/src/java/fr/paris/lutece/portal/service/content/XPageAppService.java +++ b/src/java/fr/paris/lutece/portal/service/content/XPageAppService.java @@ -40,10 +40,12 @@ import fr.paris.lutece.portal.service.portal.PortalService; import fr.paris.lutece.portal.service.security.LuteceUser; import fr.paris.lutece.portal.service.security.SecurityService; +import fr.paris.lutece.portal.service.security.SecurityTokenHandler; import fr.paris.lutece.portal.service.security.UserNotSignedException; import fr.paris.lutece.portal.service.template.AppTemplateService; import fr.paris.lutece.portal.service.util.AppException; import fr.paris.lutece.portal.service.util.AppLogService; +import fr.paris.lutece.portal.util.mvc.utils.ReflectionUtils; import fr.paris.lutece.portal.web.xpages.XPage; import fr.paris.lutece.portal.web.xpages.XPageApplication; import fr.paris.lutece.portal.web.xpages.XPageApplicationEntry; @@ -104,11 +106,18 @@ public static void registerXPageApplication( XPageApplicationEntry entry ) throw throw new LuteceInitException( ERROR_INSTANTIATION + entry.getId( ) + " - Could not find bean named " + applicationBeanName, new ResolutionException( applicationBeanName ) ); } + + Class clazz = CDI.current( ).getBeanManager( ).getBeans( applicationBeanName ).iterator( ).next( ).getBeanClass( ); + SecurityTokenHandler securityTokenHandler = CDI.current( ).select( SecurityTokenHandler.class ).get( ); + securityTokenHandler.registerDisabledActions( entry.getId( ), ReflectionUtils.getDeclaredMethods( clazz ) ); } else { // check that the class can be found - Class.forName( entry.getClassName( ) ).newInstance( ); + Object instance = Class.forName( entry.getClassName( ) ).newInstance( ); + + SecurityTokenHandler securityTokenHandler = CDI.current( ).select( SecurityTokenHandler.class ).get( ); + securityTokenHandler.registerDisabledActions( entry.getId( ), ReflectionUtils.getDeclaredMethods( instance.getClass( ) ) ); } _mapApplications.put( entry.getId( ), entry ); diff --git a/src/java/fr/paris/lutece/portal/service/message/SiteMessageHandler.java b/src/java/fr/paris/lutece/portal/service/message/SiteMessageHandler.java index 3e304b75ca..43800779ec 100644 --- a/src/java/fr/paris/lutece/portal/service/message/SiteMessageHandler.java +++ b/src/java/fr/paris/lutece/portal/service/message/SiteMessageHandler.java @@ -36,6 +36,7 @@ import fr.paris.lutece.portal.service.content.PageData; import fr.paris.lutece.portal.service.includes.PageInclude; import fr.paris.lutece.portal.service.includes.PageIncludeService; +import fr.paris.lutece.portal.service.security.SecurityTokenHandler; import fr.paris.lutece.portal.service.template.AppTemplateService; import fr.paris.lutece.portal.service.util.AppPathService; import fr.paris.lutece.portal.web.constants.Markers; @@ -49,6 +50,7 @@ import java.util.Map; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; import jakarta.servlet.http.HttpServletRequest; /** @@ -72,6 +74,9 @@ public class SiteMessageHandler implements ISiteMessageHandler private static final String BOOKMARK_BASE_URL = "@base_url@"; private static final String MARK_BACK_URL = "back_url"; + @Inject + private SecurityTokenHandler _securityTokenHandler; + /** * {@inheritDoc } */ @@ -113,6 +118,8 @@ public String getPage( HttpServletRequest request, int nMode ) // Delete message in session SiteMessageService.cleanMessageSession( request ); + + _securityTokenHandler.addSessionTokenValue( request, model ); HtmlTemplate template = AppTemplateService.getTemplate( TEMPLATE_MESSAGE, locale, model ); diff --git a/src/java/fr/paris/lutece/portal/service/security/SecurityTokenFilterAdmin.java b/src/java/fr/paris/lutece/portal/service/security/SecurityTokenFilterAdmin.java new file mode 100644 index 0000000000..e65b0372d5 --- /dev/null +++ b/src/java/fr/paris/lutece/portal/service/security/SecurityTokenFilterAdmin.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2002-2024, 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.security; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; + +import fr.paris.lutece.portal.service.admin.AdminTokenAccessDeniedException; +import fr.paris.lutece.portal.util.mvc.utils.MVCUtils; +import fr.paris.lutece.portal.web.LocalVariables; +import jakarta.inject.Inject; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * Filter to manage the CSRF token validation for MVCAdminJspBean requests. + */ +public class SecurityTokenFilterAdmin implements Filter +{ + private static final String EXCLUDED_RESOURCES_CONFIG = ".securityTokenFilterAdmin.exclude"; + + private Set _excludedResources = new HashSet( ); + @Inject + private SecurityTokenHandler _securityTokenHandler; + + /** + * {@inheritDoc} + */ + @Override + public void init( FilterConfig filterConfig ) throws ServletException + { + Config config = ConfigProvider.getConfig( ); + _excludedResources = StreamSupport.stream( config.getPropertyNames( ).spliterator( ), false ) + .filter( n -> n.endsWith( EXCLUDED_RESOURCES_CONFIG ) ) + .flatMap( n -> Arrays.stream( config.getValue( n, String [ ].class ) ) ) + .collect( Collectors.toSet( ) ); + } + + /** + * {@inheritDoc} + */ + @Override + public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain ) throws IOException, ServletException + { + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + LocalVariables.setLocal( null, httpServletRequest, httpServletResponse ); + _securityTokenHandler.handle( httpServletRequest ); + + if ( _securityTokenHandler.shouldNotFilter( httpServletRequest ) + || isResourceExcluded( httpServletRequest ) ) + { + chain.doFilter( request, response ); + return; + } + + String action = MVCUtils.getAction( httpServletRequest ); + if ( !_securityTokenHandler.validate( httpServletRequest, action ) ) + { + throw new ServletException( new AdminTokenAccessDeniedException( "SecurityTokenFilterSite :: Invalid security token" ) ); + } + + chain.doFilter( request, response ); + } + + /** + * Checks if the requested URI is excluded from security token validation. + * + * @param request + * The request + * @return + * True is the resource is excluded for the token validation. + */ + private boolean isResourceExcluded( HttpServletRequest request ) + { + boolean bExcluded = false; + for ( String strExcluded : _excludedResources ) + { + if ( request.getServletPath( ).contains( strExcluded ) ) + { + bExcluded = true; + break; + } + } + return bExcluded; + } + +} diff --git a/src/java/fr/paris/lutece/portal/service/security/SecurityTokenFilterSite.java b/src/java/fr/paris/lutece/portal/service/security/SecurityTokenFilterSite.java new file mode 100644 index 0000000000..9dd9cfb5fc --- /dev/null +++ b/src/java/fr/paris/lutece/portal/service/security/SecurityTokenFilterSite.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2002-2024, 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.security; + +import java.io.IOException; + +import fr.paris.lutece.portal.service.admin.AccessDeniedException; +import fr.paris.lutece.portal.util.mvc.utils.MVCUtils; +import jakarta.inject.Inject; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; + +/** + * Filter to manage the CSRF token validation for MVCApplication requests. + */ +public class SecurityTokenFilterSite implements Filter +{ + @Inject + private SecurityTokenHandler _securityTokenHandler; + + /** + * {@inheritDoc} + */ + @Override + public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain ) throws IOException, ServletException + { + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + _securityTokenHandler.handle( httpServletRequest ); + + if ( _securityTokenHandler.shouldNotFilter( httpServletRequest ) ) + { + chain.doFilter( request, response ); + return; + } + + String action = MVCUtils.getAction( httpServletRequest ); + if ( !_securityTokenHandler.validate( httpServletRequest, action ) ) + { + throw new ServletException( new AccessDeniedException( "Invalid security token" ) ); + } + + chain.doFilter( request, response ); + } + +} diff --git a/src/java/fr/paris/lutece/portal/service/security/SecurityTokenHandler.java b/src/java/fr/paris/lutece/portal/service/security/SecurityTokenHandler.java new file mode 100644 index 0000000000..6078abf085 --- /dev/null +++ b/src/java/fr/paris/lutece/portal/service/security/SecurityTokenHandler.java @@ -0,0 +1,282 @@ +/* + * Copyright (c) 2002-2024, 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.security; + +import java.lang.reflect.Method; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import fr.paris.lutece.portal.util.mvc.commons.annotations.Action; +import fr.paris.lutece.portal.util.mvc.utils.MVCUtils; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; + +/** + * Handles the Security token related to the SecurityTokenFilter. + */ +@ApplicationScoped +public class SecurityTokenHandler +{ + + public static final String MARK_CSRF_TOKEN = "_csrftoken"; + private static final HashSet ALLOWED_METHODS = new HashSet<>( Arrays.asList( "GET", "HEAD", "TRACE", "OPTIONS" ) ); + private static final String CSRF_TOKEN = SecurityTokenHandler.class.getName( ) + "_CSRF_TOKEN"; + private static final String FILTER_ENABLED = "FILTER_ENABLED_" + SecurityTokenHandler.class.getName( ); + private static final String SESSION_ATTRIBUTE_SITE_TOKEN = "LUTECE_PORTAL_CSRF_TOKEN"; + private static final String SESSION_ATTRIBUTE_ADMIN_TOKEN = "LUTECE_ADMIN_CSRF_TOKEN"; + private static final String SESSION_ATTRIBUTE_TOKENS = "tokens"; + private static final String PATTERN_FORM = "
]*\\\\/>)[^>]*>"; + private static final String TOKEN_FIELD_PATTERN = ""; + private static final String PARAMETER_PAGE = "page"; + private static final String PATH_ADMIN = "/jsp/admin"; + + private Map> _mapDisabledActionMethods = new HashMap>( ); + @Inject + private SecurityTokenService _securityTokenService; + + /** + * Registers disabled action methods from XPage or MVCAdminJspBean + */ + public void registerDisabledActions( String strName, Method [ ] methods ) + { + if ( !_mapDisabledActionMethods.containsKey( strName ) ) + { + _mapDisabledActionMethods.put( strName, new HashSet( ) ); + HashSet dis = _mapDisabledActionMethods.get( strName ); + for ( Method m : methods ) + { + if ( m.isAnnotationPresent( Action.class ) + && m.getAnnotation( Action.class ).securityTokenDisabled( ) ) + { + dis.add( m.getAnnotation( Action.class ).value( ) ); + } + } + } + } + + /** + * Activates the token handling on the request. + * + * @param request + * The request + */ + public void handle( HttpServletRequest request ) + { + request.setAttribute( FILTER_ENABLED, Boolean.TRUE ); + } + + /** + * Checks if the request should validate the security token. + * + * @param request + * The request + * @return + * True if the request should be skipped from the SecurityTokenFilter validation, false otherwise. + */ + public boolean shouldNotFilter( HttpServletRequest request ) + { + String strAction = MVCUtils.getAction( request ); + String strPageName = request.getParameter( PARAMETER_PAGE ); + String strPath = request.getServletPath( ); + + if ( null == strAction + || ALLOWED_METHODS.contains( request.getMethod( ) ) ) + { + return true; + } + + if ( ( _mapDisabledActionMethods.containsKey( strPageName ) && _mapDisabledActionMethods.get( strPageName ).contains( strAction ) ) + || ( _mapDisabledActionMethods.containsKey( strPath ) && _mapDisabledActionMethods.get( strPath ).contains( strAction ) ) ) + { + return true; + } + + return false; + } + + /** + * Handles the generation and storage of the security token. + * + * @param request + * The request + * @param methods + * The MVC methods + */ + public void handleToken( HttpServletRequest request, Method method ) + { + if ( Boolean.TRUE.equals( request.getAttribute( FILTER_ENABLED ) ) ) + { + String strView = MVCUtils.getView( request ); + if ( strView != null ) + { + String s = _securityTokenService.getToken( request, strView ); + request.setAttribute( CSRF_TOKEN, s ); + return; + } + + String strAction = MVCUtils.getAction( request ); + if ( strAction != null ) + { +// Method m = MVCUtils.findActionAnnotedMethod( request, methods, strAction ); + String secTokenAction = !"".equals( method.getAnnotation( Action.class ).securityTokenAction( ) ) + ? method.getAnnotation( Action.class ).securityTokenAction( ) + : method.getAnnotation( Action.class ).value( ); + String s = _securityTokenService.getToken( request, secTokenAction ); + request.setAttribute( CSRF_TOKEN, s ); + HttpSession session = request.getSession( true ); + if ( !request.getServletPath( ).startsWith( PATH_ADMIN ) ) + { + session.setAttribute( SESSION_ATTRIBUTE_SITE_TOKEN, s ); + } + else + { + session.setAttribute( SESSION_ATTRIBUTE_ADMIN_TOKEN, s ); + } + } + } + } + + /** + * Resolves security token value from the request. + * + * @param request + * The request + * @return + * The security token value + */ + public String resolveTokenValue( HttpServletRequest request ) + { + return (String) request.getAttribute( CSRF_TOKEN ); + } + + /** + * Adds the session security token for confirm type actions (SiteMessageHandler / AdminMessageJspBean). + * + * @param request + * The request + * @param model + * The model + */ + public void addSessionTokenValue( HttpServletRequest request, Map model ) + { + HttpSession session = request.getSession( true ); + String strTokenAttribute = SESSION_ATTRIBUTE_SITE_TOKEN; + if ( request.getServletPath( ).startsWith( PATH_ADMIN ) ) + { + strTokenAttribute = SESSION_ATTRIBUTE_ADMIN_TOKEN; + } + String s = (String) session.getAttribute( strTokenAttribute ); + model.put( MARK_CSRF_TOKEN, s ); + session.removeAttribute( strTokenAttribute ); + } + + /** + * Validates the token for the given request. + * + * @param request + * The request + * @param strAction + * The MVC action + * @return + * True if the token was valid. + */ + public boolean validate( HttpServletRequest request, String strAction ) + { + HttpSession session = request.getSession( true ); + + String strToken = request.getParameter( MARK_CSRF_TOKEN ); + + if ( ( session.getAttribute( SESSION_ATTRIBUTE_TOKENS ) != null ) + && ( (Map>) session.getAttribute( SESSION_ATTRIBUTE_TOKENS ) ).containsKey( strAction ) + && ( (Map>) session.getAttribute( SESSION_ATTRIBUTE_TOKENS ) ).get( strAction ).contains( strToken ) ) + { + ( (Map>) session.getAttribute( SESSION_ATTRIBUTE_TOKENS ) ).get( strAction ).remove( strToken ); + + return true; + } + + return false; + } + + /** + * Adds the hidden field to forms located in the given template data. + * + * @param strSource + * The template data. + * @param model + * The model + * @return + * The updated template data. + */ + public static String addSecurityToken( String strSource, Object model ) + { + String result = strSource; + + if ( null != strSource ) + { + HashMap rootmap = (HashMap) model; + String strToken = (String) rootmap.get( MARK_CSRF_TOKEN ); + if ( null != strToken ) + { + Pattern p = Pattern.compile( PATTERN_FORM ); + Matcher matcher = p.matcher( strSource ); + + if ( matcher.find( ) ) + { + StringBuffer sb = new StringBuffer( ); + + do + { + matcher.appendReplacement( sb, matcher.group( 0 ) + MessageFormat.format( TOKEN_FIELD_PATTERN, strToken ) ); + } + while ( matcher.find( ) ); + + matcher.appendTail( sb ); + result = sb.toString( ); + } + } + } + + return result; + } + +} diff --git a/src/java/fr/paris/lutece/portal/service/security/SecurityTokenService.java b/src/java/fr/paris/lutece/portal/service/security/SecurityTokenService.java index db52c0a86b..abd63f92f4 100644 --- a/src/java/fr/paris/lutece/portal/service/security/SecurityTokenService.java +++ b/src/java/fr/paris/lutece/portal/service/security/SecurityTokenService.java @@ -59,7 +59,7 @@ public class SecurityTokenService implements ISecurityTokenService /** * SecurityTokenService */ - private SecurityTokenService( ) + SecurityTokenService( ) { } diff --git a/src/java/fr/paris/lutece/portal/service/template/AppTemplateService.java b/src/java/fr/paris/lutece/portal/service/template/AppTemplateService.java index 271d4e5f22..de887884fb 100644 --- a/src/java/fr/paris/lutece/portal/service/template/AppTemplateService.java +++ b/src/java/fr/paris/lutece/portal/service/template/AppTemplateService.java @@ -38,6 +38,7 @@ import fr.paris.lutece.portal.service.i18n.I18nTemplateMethod; import fr.paris.lutece.portal.service.plugin.Plugin; import fr.paris.lutece.portal.service.plugin.PluginService; +import fr.paris.lutece.portal.service.security.SecurityTokenHandler; import fr.paris.lutece.portal.service.util.AppLogService; import fr.paris.lutece.util.html.HtmlTemplate; @@ -235,6 +236,11 @@ public static HtmlTemplate getTemplateFromStringFtl( String strFreemarkerTemplat String strLocalized = I18nService.localize( template.getHtml( ), locale ); template = new HtmlTemplate( strLocalized ); } + if ( null != model ) + { + String strLocalized = SecurityTokenHandler.addSecurityToken( template.getHtml( ), model ); + template = new HtmlTemplate( strLocalized ); + } return template; } @@ -262,6 +268,11 @@ public static HtmlTemplate getTemplateFromStringFtl( String strFreemarkerTemplat String strLocalized = I18nService.localize( template.getHtml( ), locale ); template = new HtmlTemplate( strLocalized ); } + if ( null != model ) + { + String strLocalized = SecurityTokenHandler.addSecurityToken( template.getHtml( ), model ); + template = new HtmlTemplate( strLocalized ); + } return template; } @@ -288,6 +299,11 @@ private static HtmlTemplate loadTemplate( String strPath, String strTemplate, Lo String strLocalized = I18nService.localize( template.getHtml( ), locale ); template = new HtmlTemplate( strLocalized ); } + if ( null != model ) + { + String strLocalized = SecurityTokenHandler.addSecurityToken( template.getHtml( ), model ); + template = new HtmlTemplate( strLocalized ); + } template = new HtmlTemplate( DatastoreService.replaceKeys( template.getHtml( ) ) ); diff --git a/src/java/fr/paris/lutece/portal/util/mvc/admin/MVCAdminJspBean.java b/src/java/fr/paris/lutece/portal/util/mvc/admin/MVCAdminJspBean.java index 75c8b34272..736995ac0e 100644 --- a/src/java/fr/paris/lutece/portal/util/mvc/admin/MVCAdminJspBean.java +++ b/src/java/fr/paris/lutece/portal/util/mvc/admin/MVCAdminJspBean.java @@ -33,7 +33,6 @@ */ package fr.paris.lutece.portal.util.mvc.admin; -import fr.paris.lutece.portal.service.security.AccessLogService; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; @@ -46,16 +45,18 @@ import java.util.Map; import java.util.Map.Entry; -import jakarta.inject.Inject; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.apache.logging.log4j.Logger; + import fr.paris.lutece.portal.business.user.AdminUser; import fr.paris.lutece.portal.service.admin.AccessDeniedException; import fr.paris.lutece.portal.service.admin.AdminAuthenticationService; import fr.paris.lutece.portal.service.i18n.I18nService; +import fr.paris.lutece.portal.service.security.AccessLogService; import fr.paris.lutece.portal.service.security.AccessLoggerConstants; +import fr.paris.lutece.portal.service.security.SecurityTokenHandler; import fr.paris.lutece.portal.service.template.AppTemplateService; import fr.paris.lutece.portal.service.util.AppException; import fr.paris.lutece.portal.service.util.AppLogService; @@ -63,11 +64,14 @@ import fr.paris.lutece.portal.util.mvc.utils.MVCMessage; import fr.paris.lutece.portal.util.mvc.utils.MVCUtils; import fr.paris.lutece.portal.util.mvc.utils.ReflectionUtils; +import fr.paris.lutece.portal.web.LocalVariables; import fr.paris.lutece.portal.web.admin.PluginAdminPageJspBean; import fr.paris.lutece.util.ErrorMessage; import fr.paris.lutece.util.beanvalidation.ValidationError; import fr.paris.lutece.util.html.HtmlTemplate; import fr.paris.lutece.util.url.UrlItem; +import jakarta.enterprise.inject.spi.CDI; +import jakarta.inject.Inject; /** * MVC Admin JspBean implementation let use MVC model to develop admin feature. @@ -81,9 +85,6 @@ public abstract class MVCAdminJspBean extends PluginAdminPageJspBean private static final String MARK_INFOS = "infos"; private static final String MARK_WARNINGS = "warnings"; - // properties - private static final String PROPERTY_SITE_CODE = "lutece.code"; - // instance vars private static Logger _logger = MVCUtils.getLogger( ); private List _listErrors = new ArrayList<>( ); @@ -93,6 +94,8 @@ public abstract class MVCAdminJspBean extends PluginAdminPageJspBean private HttpServletResponse _response; @Inject private transient AccessLogService _accessLogService; + @Inject + private transient SecurityTokenHandler _securityTokenHandler; /** * Process request as a controller @@ -111,7 +114,9 @@ public String processController( HttpServletRequest request, HttpServletResponse init( request, _controller.right( ) ); Method [ ] methods = ReflectionUtils.getAllDeclaredMethods( getClass( ) ); - + + getSecurityTokenHandler( ).registerDisabledActions( _controller.controllerPath( ) + _controller.controllerJsp( ), methods ); + try { // Process views @@ -123,6 +128,7 @@ public String processController( HttpServletRequest request, HttpServletResponse { getAccessLogService( ).trace( AccessLoggerConstants.EVENT_TYPE_VIEW, m.getName( ), adminUser, request.getRequestURL( ) + "?" + request.getQueryString( ), AccessLogService.ACCESS_LOG_BO ); + getSecurityTokenHandler( ).handleToken(request, m); return (String) m.invoke( this, request ); } @@ -133,6 +139,7 @@ public String processController( HttpServletRequest request, HttpServletResponse { getAccessLogService( ).debug( AccessLoggerConstants.EVENT_TYPE_ACTION, m.getName( ), adminUser, request.getRequestURL( ) + "?" + request.getQueryString( ), AccessLogService.ACCESS_LOG_BO ); + getSecurityTokenHandler( ).handleToken(request, m); return (String) m.invoke( this, request ); } @@ -141,6 +148,7 @@ public String processController( HttpServletRequest request, HttpServletResponse getAccessLogService( ).trace( AccessLoggerConstants.EVENT_TYPE_VIEW, m.getName( ), adminUser, request.getRequestURL( ) + "?" + request.getQueryString( ), AccessLogService.ACCESS_LOG_BO ); + getSecurityTokenHandler( ).handleToken(request, m); return (String) m.invoke( this, request ); } catch( InvocationTargetException e ) @@ -157,7 +165,7 @@ public String processController( HttpServletRequest request, HttpServletResponse throw new AppException( "MVC Error dispaching view and action ", e ); } } - + // ////////////////////////////////////////////////////////////////////////// // Page utils @@ -261,7 +269,8 @@ protected Map getModel( ) { Map model = new HashMap<>( ); fillCommons( model ); - + fillSecurityToken( model ); + return model; } @@ -583,8 +592,41 @@ protected void download( byte [ ] data, String strFilename, String strContentTyp } } + /** + * Returns the AccesLogService instance by privileging direct injection. Used during complete transition do CDI XPages. + * + * @return the AccessLogService instance + */ private AccessLogService getAccessLogService( ) { - return null != _accessLogService ? _accessLogService : AccessLogService.getInstance( ); + return null != _accessLogService ? _accessLogService : CDI.current( ).select( AccessLogService.class ).get( ); + } + + /** + * Returns the SecurityTokenHandler instance by privileging direct injection. Used during complete transition do CDI XPages. + * + * @return the SecurityTokenHandler instance + */ + private SecurityTokenHandler getSecurityTokenHandler( ) + { + return null != _securityTokenHandler ? _securityTokenHandler : CDI.current( ).select( SecurityTokenHandler.class ).get( ); + } + + /** + * Fill the model with security token + * + * @param model + * The model + */ + private void fillSecurityToken( Map model ) + { + if ( null != LocalVariables.getRequest( ) ) + { + String strToken = getSecurityTokenHandler( ).resolveTokenValue( LocalVariables.getRequest( ) ); + if ( null != strToken ) + { + model.put( SecurityTokenHandler.MARK_CSRF_TOKEN, strToken ); + } + } } } diff --git a/src/java/fr/paris/lutece/portal/util/mvc/commons/annotations/Action.java b/src/java/fr/paris/lutece/portal/util/mvc/commons/annotations/Action.java index 93a8523bad..1471533c3e 100644 --- a/src/java/fr/paris/lutece/portal/util/mvc/commons/annotations/Action.java +++ b/src/java/fr/paris/lutece/portal/util/mvc/commons/annotations/Action.java @@ -55,4 +55,8 @@ * @return the value */ String value( ); + + String securityTokenAction( ) default ""; + + boolean securityTokenDisabled( ) default false; } diff --git a/src/java/fr/paris/lutece/portal/util/mvc/xpage/MVCApplication.java b/src/java/fr/paris/lutece/portal/util/mvc/xpage/MVCApplication.java index 7e15d144c1..8fc4149a74 100644 --- a/src/java/fr/paris/lutece/portal/util/mvc/xpage/MVCApplication.java +++ b/src/java/fr/paris/lutece/portal/util/mvc/xpage/MVCApplication.java @@ -47,6 +47,7 @@ import java.util.Map.Entry; import java.util.Set; +import jakarta.enterprise.inject.spi.CDI; import jakarta.inject.Inject; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -60,6 +61,7 @@ import fr.paris.lutece.portal.service.security.AccessLoggerConstants; import fr.paris.lutece.portal.service.security.LuteceUser; import fr.paris.lutece.portal.service.security.SecurityService; +import fr.paris.lutece.portal.service.security.SecurityTokenHandler; import fr.paris.lutece.portal.service.security.UserNotSignedException; import fr.paris.lutece.portal.service.template.AppTemplateService; import fr.paris.lutece.portal.service.util.AppException; @@ -109,6 +111,8 @@ public abstract class MVCApplication implements XPageApplication private Controller _controller = getClass( ).getAnnotation( Controller.class ); @Inject private transient AccessLogService _accessLogService; + @Inject + private transient SecurityTokenHandler _securityTokenHandler; /** * Returns the content of the page @@ -165,6 +169,7 @@ private XPage processController( HttpServletRequest request ) throws UserNotSign { getAccessLogService( ).trace( AccessLoggerConstants.EVENT_TYPE_READ, m.getName( ), registredUser, request.getRequestURL( ) + "?" + request.getQueryString( ), AccessLogService.ACCESS_LOG_FO ); + getSecurityTokenHandler( ).handleToken(request, m); return (XPage) m.invoke( this, request ); } @@ -175,6 +180,7 @@ private XPage processController( HttpServletRequest request ) throws UserNotSign { getAccessLogService( ).debug( AccessLoggerConstants.EVENT_TYPE_ACTION, m.getName( ), registredUser, request.getRequestURL( ) + "?" + request.getQueryString( ), AccessLogService.ACCESS_LOG_FO ); + getSecurityTokenHandler( ).handleToken(request, m); return (XPage) m.invoke( this, request ); } @@ -183,6 +189,7 @@ private XPage processController( HttpServletRequest request ) throws UserNotSign getAccessLogService( ).trace( AccessLoggerConstants.EVENT_TYPE_ACTION, m.getName( ), registredUser, request.getRequestURL( ) + "?" + request.getQueryString( ), AccessLogService.ACCESS_LOG_FO ); + getSecurityTokenHandler( ).handleToken(request, m); return (XPage) m.invoke( this, request ); } catch( InvocationTargetException e ) @@ -346,7 +353,8 @@ protected Map getModel( ) { Map model = new HashMap<>( ); fillCommons( model ); - + fillSecurityToken( model ); + return model; } @@ -954,9 +962,41 @@ protected LuteceUser getRegistredUser( HttpServletRequest request ) return null; } + /** + * Returns the AccesLogService instance by privileging direct injection. Used during complete transition do CDI XPages. + * + * @return the AccessLogService instance + */ private AccessLogService getAccessLogService( ) { - return null != _accessLogService ? _accessLogService : AccessLogService.getInstance( ); + return null != _accessLogService ? _accessLogService : CDI.current( ).select( AccessLogService.class ).get( ); + } + + /** + * Returns the SecurityTokenHandler instance by privileging direct injection. Used during complete transition do CDI XPages. + * + * @return the SecurityTokenHandler instance + */ + private SecurityTokenHandler getSecurityTokenHandler( ) + { + return null != _securityTokenHandler ? _securityTokenHandler : CDI.current( ).select( SecurityTokenHandler.class ).get( ); + } + + /** + * Fill the model with security token + * + * @param model + * The model + */ + private void fillSecurityToken( Map model ) + { + if ( null != LocalVariables.getRequest( ) ) + { + String strToken = getSecurityTokenHandler( ).resolveTokenValue( LocalVariables.getRequest( ) ); + if ( null != strToken ) + { + model.put( SecurityTokenHandler.MARK_CSRF_TOKEN, strToken ); + } + } } - } diff --git a/src/java/fr/paris/lutece/portal/web/admin/AdminMessageJspBean.java b/src/java/fr/paris/lutece/portal/web/admin/AdminMessageJspBean.java index 9d8b099672..3bbe05f0fd 100644 --- a/src/java/fr/paris/lutece/portal/web/admin/AdminMessageJspBean.java +++ b/src/java/fr/paris/lutece/portal/web/admin/AdminMessageJspBean.java @@ -37,6 +37,7 @@ import fr.paris.lutece.portal.service.admin.AdminUserService; import fr.paris.lutece.portal.service.message.AdminMessage; import fr.paris.lutece.portal.service.message.AdminMessageService; +import fr.paris.lutece.portal.service.security.SecurityTokenHandler; import fr.paris.lutece.portal.service.template.AppTemplateService; import fr.paris.lutece.portal.service.util.AppPathService; import fr.paris.lutece.portal.web.constants.Messages; @@ -47,6 +48,7 @@ import java.util.Map; import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.servlet.http.HttpServletRequest; @@ -67,7 +69,9 @@ public class AdminMessageJspBean private static final String MARK_BACK_URL = "back_url"; private static final String MARK_ADMIN_URL = "admin_url"; private static final String PROPERTY_TITLE_ERROR = "portal.util.message.titleError"; - + @Inject + private transient SecurityTokenHandler _securityTokenHandler; + /** * Retrieve a message stored into a request * @@ -95,7 +99,9 @@ public String getMessage( HttpServletRequest request ) model.put( MARK_REQUEST_PARAMETERS, message.getRequestParameters( ) ); model.put( MARK_BACK_URL, message.getBackUrl( ) ); model.put( MARK_ADMIN_URL, AppPathService.getAdminMenuUrl( ) ); - + + _securityTokenHandler.addSessionTokenValue( request, model ); + HtmlTemplate template = AppTemplateService.getTemplate( TEMPLATE_MESSAGE, locale, model ); return template.getHtml( ); diff --git a/webapp/WEB-INF/conf/lutece.properties b/webapp/WEB-INF/conf/lutece.properties index 600d75f8fe..1e4e499dce 100644 --- a/webapp/WEB-INF/conf/lutece.properties +++ b/webapp/WEB-INF/conf/lutece.properties @@ -318,3 +318,8 @@ lutece.xml.user.path=/xdoc/user/ #languageUserMenuItemProvider.insertBefore=dividerUserMenuItemProvider #dividerUserMenuItemProvider.insertAfter=accessibilityModeUserMenuItemProvider #dividerUserMenuItemProvider.insertBefore= + +################################################################################ +#### SecurityTokenFilterAdmin exclusions, can contain a list of excluded resources (comma separated) +core.securityTokenFilterAdmin.exclude=ManageAutoIncludes.jsp + diff --git a/webapp/WEB-INF/web.xml b/webapp/WEB-INF/web.xml index 9eaec4b4f6..bdfb095036 100644 --- a/webapp/WEB-INF/web.xml +++ b/webapp/WEB-INF/web.xml @@ -104,7 +104,14 @@ true - + + securityTokenFilterSite + fr.paris.lutece.portal.service.security.SecurityTokenFilterSite + + + securityTokenFilterAdmin + fr.paris.lutece.portal.service.security.SecurityTokenFilterAdmin + pageSecurityHeaderFilter @@ -168,6 +175,19 @@ FORWARD REQUEST + + + securityTokenFilterSite + /jsp/site/* + FORWARD + REQUEST + + + securityTokenFilterAdmin + /jsp/admin/* + FORWARD + REQUEST + PluginsServlets @@ -238,10 +258,26 @@ /jsp/site/Error404.jsp - + + fr.paris.lutece.portal.service.admin.AdminTokenAccessDeniedException + /jsp/admin/ErrorPage.jsp + + + + + /jsp/site/Error500.jsp + + + + + +