SecurityUtil.java
/*
* 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.util.http;
import java.util.Enumeration;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.util.AntPathMatcher;
import fr.paris.lutece.portal.service.util.AppPathService;
import fr.paris.lutece.portal.service.util.AppPropertiesService;
import fr.paris.lutece.portal.web.LocalVariables;
import fr.paris.lutece.util.string.StringUtil;
/**
* Security utils
*
*/
public final class SecurityUtil
{
private static final String LOGGER_NAME = "lutece.security.http";
private static final String CONSTANT_HTTP_HEADER_X_FORWARDED_FOR = "X-Forwarded-For";
private static final String PATTERN_IP_ADDRESS = "^([0-9]{1,3}\\.){3}[0-9]{1,3}$";
private static final String CONSTANT_COMMA = ",";
private static final String [ ] XXE_TERMS = {
"!DOCTYPE", "!ELEMENT", "!ENTITY"
};
private static final String [ ] PATH_MANIPULATION = {
"..", "/", "\\"
};
public static final String PROPERTY_REDIRECT_URL_SAFE_PATTERNS = "lutece.security.redirectUrlSafePatterns";
public static final Logger _log = LogManager.getLogger( LOGGER_NAME );
/**
* Private Constructor
*/
private SecurityUtil( )
{
}
/**
* Scan request parameters to see if there no malicious code.
*
* @param request
* The HTTP request
* @return true if all parameters don't contains any special characters
*/
public static boolean containsCleanParameters( HttpServletRequest request )
{
return containsCleanParameters( request, null );
}
/**
* Scan request parameters to see if there no malicious code.
*
* @param request
* The HTTP request
* @param strXssCharacters
* a String wich contain a list of Xss characters to check in strValue
* @return true if all parameters don't contains any special characters
*/
public static boolean containsCleanParameters( HttpServletRequest request, String strXssCharacters )
{
String key;
String [ ] values;
Enumeration<String> e = request.getParameterNames( );
while ( e.hasMoreElements( ) )
{
key = e.nextElement( );
values = request.getParameterValues( key );
int length = values.length;
for ( int i = 0; i < length; i++ )
{
if ( SecurityUtil.containsXssCharacters( request, values [i], strXssCharacters ) || SecurityUtil.containsXssCharacters( request, key, strXssCharacters ) )
{
_log.warn( "SECURITY WARNING : INVALID REQUEST PARAMETERS {}", ( ) -> dumpRequest( request ) );
return false;
}
}
}
return true;
}
/**
* Checks if a String contains characters that could be used for a cross-site scripting attack.
*
* @param request
* The HTTP request
* @param strString
* a character String
* @return true if the String contains illegal characters
*/
public static boolean containsXssCharacters( HttpServletRequest request, String strString )
{
return containsXssCharacters( request, strString, null );
}
/**
* Checks if a String contains characters that could be used for a cross-site scripting attack.
*
* @param request
* The HTTP request
* @param strValue
* a character String
* @param strXssCharacters
* a String wich contain a list of Xss characters to check in strValue
* @return true if the String contains illegal characters
*/
public static boolean containsXssCharacters( HttpServletRequest request, String strValue, String strXssCharacters )
{
boolean bContains = ( strXssCharacters == null ) ? StringUtil.containsXssCharacters( strValue )
: StringUtil.containsXssCharacters( strValue, strXssCharacters );
if ( bContains )
{
_log.warn( "SECURITY WARNING : XSS CHARACTERS DETECTED {}", ( ) -> dumpRequest( request ) );
}
return bContains;
}
/**
* Check if the value contains terms used for XML External Entity Injection
*
* @param strValue
* The value
* @return true if
*/
public static boolean containsXmlExternalEntityInjectionTerms( String strValue )
{
for ( String strTerm : XXE_TERMS )
{
if ( StringUtils.indexOfIgnoreCase( strValue, strTerm ) >= 0 )
{
_log.warn( "SECURITY WARNING : XXE TERMS DETECTED : {}", ( ) -> dumpRequest( LocalVariables.getRequest( ) ) );
return true;
}
}
return false;
}
/**
* Check if the value contains characters used for Path Manipulation
*
* @param request
* The Http request
* @param strValue
* The value
* @return true if
*/
public static boolean containsPathManipulationChars( HttpServletRequest request, String strValue )
{
for ( String strTerm : PATH_MANIPULATION )
{
if ( strValue.contains( strTerm ) )
{
_log.warn( "SECURITY WARNING : PATH_MANIPULATION DETECTED : {}", ( ) -> dumpRequest( request ) );
return true;
}
}
return false;
}
/**
* Dump all request info
*
* @param request
* The HTTP request
* @return A report containing all request info
*/
public static String dumpRequest( HttpServletRequest request )
{
StringBuilder sbDump = new StringBuilder( "\r\n Request Dump : \r\n" );
if ( request != null )
{
dumpTitle( sbDump, "Request variables" );
dumpVariables( sbDump, request );
dumpTitle( sbDump, "Request parameters" );
dumpParameters( sbDump, request );
dumpTitle( sbDump, "Request headers" );
dumpHeaders( sbDump, request );
}
else
{
sbDump.append( "no request provided." );
}
return sbDump.toString( );
}
/**
* Get the IP of the user from a request. If the user is behind an apache server, return the ip of the user instead of the ip of the server.
*
* @param request
* The request
* @return The IP of the user that made the request
*/
public static String getRealIp( HttpServletRequest request )
{
String strIPAddress = request.getHeader( CONSTANT_HTTP_HEADER_X_FORWARDED_FOR );
if ( strIPAddress != null )
{
while ( !strIPAddress.matches( PATTERN_IP_ADDRESS ) && strIPAddress.contains( CONSTANT_COMMA ) )
{
String strIpForwarded = strIPAddress.substring( 0, strIPAddress.indexOf( CONSTANT_COMMA ) );
strIPAddress = strIPAddress.substring( strIPAddress.indexOf( CONSTANT_COMMA ) ).replaceFirst( CONSTANT_COMMA, StringUtils.EMPTY ).trim( );
if ( ( strIpForwarded != null ) && strIpForwarded.matches( PATTERN_IP_ADDRESS ) )
{
strIPAddress = strIpForwarded;
}
}
if ( !strIPAddress.matches( PATTERN_IP_ADDRESS ) )
{
strIPAddress = request.getRemoteAddr( );
}
}
else
{
strIPAddress = request.getRemoteAddr( );
}
return strIPAddress;
}
/**
* Validate a forward URL to avoid open redirect with url safe patterns found in properties
*
* @see SecurityUtil#isInternalRedirectUrlSafe(java.lang.String, javax.servlet.http.HttpServletRequest, java.lang.String)
*
* @param strUrl
* @param request
* @return true if valid
*/
public static boolean isInternalRedirectUrlSafe( String strUrl, HttpServletRequest request )
{
String strAntPathMatcherPatternsValues = AppPropertiesService.getProperty( SecurityUtil.PROPERTY_REDIRECT_URL_SAFE_PATTERNS );
return isInternalRedirectUrlSafe( strUrl, request, strAntPathMatcherPatternsValues );
}
/**
* Validate an internal redirect URL to avoid internal open redirect. (Use this function only if the use of internal url redirect keys is not possible. For
* external url redirection control, use the plugin plugin-verifybackurl)
*
* the url should : - not be blank (null or empty string or spaces) - not start with "http://" or "https://" or "//" OR match the base URL or any URL in the
* pattern list
*
* example with a base url "https://lutece.fr/ : - valid : myapp/jsp/site/Portal.jsp , Another.jsp , https://lutece.fr/myapp/jsp/site/Portal.jsp - invalid :
* http://anothersite.com , https://anothersite.com , //anothersite.com , file://my.txt , ...
*
*
* @param strUrl
* the Url to validate
* @param request
* the current request (containing the baseUrl)
* @param strAntPathMatcherPatterns
* a comma separated list of AntPathMatcher patterns, as "http://**.lutece.com,https://**.lutece.com"
* @return true if valid
*/
public static boolean isInternalRedirectUrlSafe( String strUrl, HttpServletRequest request, String strAntPathMatcherPatterns )
{
if ( StringUtils.isBlank( strUrl ) )
{
return true; // this is not a valid redirect Url, but it is not unsafe
}
// filter schemes
boolean [ ] conditions = new boolean [ ] {
!strUrl.startsWith( "//" ), !strUrl.startsWith( "http:" ), !strUrl.startsWith( "https:" ), !strUrl.contains( "://" ),
!strUrl.startsWith( "javascript:" )
};
if ( BooleanUtils.and( conditions ) )
{
return true; // should be a relative path
}
// compare with current baseUrl
if ( strUrl.startsWith( AppPathService.getBaseUrl( request ) ) )
{
return true;
}
// compare with allowed url patterns
if ( !StringUtils.isBlank( strAntPathMatcherPatterns ) )
{
AntPathMatcher pathMatcher = new AntPathMatcher( );
String [ ] strAntPathMatcherPatternsTab = strAntPathMatcherPatterns.split( CONSTANT_COMMA );
for ( String pattern : strAntPathMatcherPatternsTab )
{
if ( pattern != null && pathMatcher.match( pattern, strUrl ) )
{
return true;
}
}
}
// the Url does not match the allowed patterns
_log.warn( "SECURITY WARNING : OPEN_REDIRECT DETECTED : {}", ( ) -> dumpRequest( request ) );
return false;
}
/**
* Identify user data saved in log files to prevent Log Forging attacks
*
* @param strUserInputData
* User Input Data
* @return The User Data to log
*/
public static String logForgingProtect( String strUserInputData )
{
int nCharCount = strUserInputData.length( );
int nLineCount = StringUtils.countMatches( strUserInputData, "\n" );
String strPrefixedLines = strUserInputData.replace( "\n", "\n** " );
return "\n** USER INPUT DATA : BEGIN (" + nLineCount + " lines and " + nCharCount + " chars) ** \n" + strPrefixedLines + "\n** USER INPUT DATA : END\n";
}
/**
* Write a title into the dump stringbuffer
*
* @param sbDump
* The dump stringbuffer
* @param strTitle
* The title
*/
private static void dumpTitle( StringBuilder sbDump, String strTitle )
{
sbDump.append( "** " );
sbDump.append( strTitle );
sbDump.append( " **\r\n" );
}
/**
* Write request variables into the dump stringbuffer
*
* @param sb
* The dump stringbuffer
* @param request
* The HTTP request
*/
private static void dumpVariables( StringBuilder sb, HttpServletRequest request )
{
dumpVariable( sb, "AUTH_TYPE", request.getAuthType( ) );
dumpVariable( sb, "REQUEST_METHOD", request.getMethod( ) );
dumpVariable( sb, "PATH_INFO", request.getPathInfo( ) );
dumpVariable( sb, "PATH_TRANSLATED", request.getPathTranslated( ) );
dumpVariable( sb, "QUERY_STRING", request.getQueryString( ) );
dumpVariable( sb, "REQUEST_URI", request.getRequestURI( ) );
dumpVariable( sb, "SCRIPT_NAME", request.getServletPath( ) );
dumpVariable( sb, "LOCAL_ADDR", request.getLocalAddr( ) );
dumpVariable( sb, "SERVER_PROTOCOL", request.getProtocol( ) );
dumpVariable( sb, "REMOTE_ADDR", request.getRemoteAddr( ) );
dumpVariable( sb, "REMOTE_HOST", request.getRemoteHost( ) );
dumpVariable( sb, "HTTPS", request.getScheme( ) );
dumpVariable( sb, "SERVER_NAME", request.getServerName( ) );
dumpVariable( sb, "SERVER_PORT", String.valueOf( request.getServerPort( ) ) );
}
/**
* Write request headers infos into the dump stringbuffer
*
* @param sb
* The dump stringbuffer
* @param request
* The HTTP request
*/
private static void dumpHeaders( StringBuilder sb, HttpServletRequest request )
{
Enumeration<String> values;
String key;
Enumeration<String> headers = request.getHeaderNames( );
while ( headers.hasMoreElements( ) )
{
key = headers.nextElement( );
values = request.getHeaders( key );
while ( values.hasMoreElements( ) )
{
dumpVariable( sb, key, values.nextElement( ) );
}
}
}
/**
* Write request parameters infos into the dump stringbuffer
*
* @param sb
* The dump stringbuffer
* @param request
* The HTTP request
*/
private static void dumpParameters( StringBuilder sb, HttpServletRequest request )
{
String key;
String [ ] values;
Enumeration<String> e = request.getParameterNames( );
while ( e.hasMoreElements( ) )
{
key = e.nextElement( );
values = request.getParameterValues( key );
int length = values.length;
for ( int i = 0; i < length; i++ )
{
dumpVariable( sb, key, values [i] );
}
}
}
/**
* Write name / value infos into the dump stringbuffer
*
* @param sb
* The dump string buffer
* @param strName
* The info name
* @param strValue
* The info value
*/
private static void dumpVariable( StringBuilder sb, String strName, String strValue )
{
sb.append( strName );
sb.append( " : \"" );
sb.append( strValue );
sb.append( "\"\r\n" );
}
}