SecurityUtil.java

  1. /*
  2.  * Copyright (c) 2002-2022, City of Paris
  3.  * All rights reserved.
  4.  *
  5.  * Redistribution and use in source and binary forms, with or without
  6.  * modification, are permitted provided that the following conditions
  7.  * are met:
  8.  *
  9.  *  1. Redistributions of source code must retain the above copyright notice
  10.  *     and the following disclaimer.
  11.  *
  12.  *  2. Redistributions in binary form must reproduce the above copyright notice
  13.  *     and the following disclaimer in the documentation and/or other materials
  14.  *     provided with the distribution.
  15.  *
  16.  *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
  17.  *     contributors may be used to endorse or promote products derived from
  18.  *     this software without specific prior written permission.
  19.  *
  20.  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  21.  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  22.  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  23.  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
  24.  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  25.  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  26.  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  27.  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  28.  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  29.  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  30.  * POSSIBILITY OF SUCH DAMAGE.
  31.  *
  32.  * License 1.0
  33.  */
  34. package fr.paris.lutece.util.http;

  35. import java.util.Enumeration;

  36. import javax.servlet.http.HttpServletRequest;

  37. import org.apache.commons.lang3.BooleanUtils;
  38. import org.apache.commons.lang3.StringUtils;
  39. import org.apache.logging.log4j.LogManager;
  40. import org.apache.logging.log4j.Logger;
  41. import org.springframework.util.AntPathMatcher;

  42. import fr.paris.lutece.portal.service.util.AppPathService;
  43. import fr.paris.lutece.portal.service.util.AppPropertiesService;
  44. import fr.paris.lutece.portal.web.LocalVariables;
  45. import fr.paris.lutece.util.string.StringUtil;

  46. /**
  47.  * Security utils
  48.  *
  49.  */
  50. public final class SecurityUtil
  51. {
  52.     private static final String LOGGER_NAME = "lutece.security.http";
  53.     private static final String CONSTANT_HTTP_HEADER_X_FORWARDED_FOR = "X-Forwarded-For";
  54.     private static final String PATTERN_IP_ADDRESS = "^([0-9]{1,3}\\.){3}[0-9]{1,3}$";
  55.     private static final String CONSTANT_COMMA = ",";
  56.     private static final String [ ] XXE_TERMS = {
  57.             "!DOCTYPE", "!ELEMENT", "!ENTITY"
  58.     };
  59.     private static final String [ ] PATH_MANIPULATION = {
  60.             "..", "/", "\\"
  61.     };

  62.     public static final String PROPERTY_REDIRECT_URL_SAFE_PATTERNS = "lutece.security.redirectUrlSafePatterns";
  63.     public static final Logger _log = LogManager.getLogger( LOGGER_NAME );

  64.     /**
  65.      * Private Constructor
  66.      */
  67.     private SecurityUtil( )
  68.     {
  69.     }

  70.     /**
  71.      * Scan request parameters to see if there no malicious code.
  72.      *
  73.      * @param request
  74.      *            The HTTP request
  75.      * @return true if all parameters don't contains any special characters
  76.      */
  77.     public static boolean containsCleanParameters( HttpServletRequest request )
  78.     {
  79.         return containsCleanParameters( request, null );
  80.     }

  81.     /**
  82.      * Scan request parameters to see if there no malicious code.
  83.      *
  84.      * @param request
  85.      *            The HTTP request
  86.      * @param strXssCharacters
  87.      *            a String wich contain a list of Xss characters to check in strValue
  88.      * @return true if all parameters don't contains any special characters
  89.      */
  90.     public static boolean containsCleanParameters( HttpServletRequest request, String strXssCharacters )
  91.     {
  92.         String key;
  93.         String [ ] values;
  94.         Enumeration<String> e = request.getParameterNames( );

  95.         while ( e.hasMoreElements( ) )
  96.         {
  97.             key = e.nextElement( );
  98.             values = request.getParameterValues( key );

  99.             int length = values.length;

  100.             for ( int i = 0; i < length; i++ )
  101.             {
  102.                 if ( SecurityUtil.containsXssCharacters( request, values [i], strXssCharacters ) || SecurityUtil.containsXssCharacters( request, key, strXssCharacters )  )
  103.                 {
  104.                     _log.warn( "SECURITY WARNING : INVALID REQUEST PARAMETERS {}", ( ) -> dumpRequest( request ) );

  105.                     return false;
  106.                 }
  107.             }
  108.         }

  109.         return true;
  110.     }

  111.     /**
  112.      * Checks if a String contains characters that could be used for a cross-site scripting attack.
  113.      *
  114.      * @param request
  115.      *            The HTTP request
  116.      * @param strString
  117.      *            a character String
  118.      * @return true if the String contains illegal characters
  119.      */
  120.     public static boolean containsXssCharacters( HttpServletRequest request, String strString )
  121.     {
  122.         return containsXssCharacters( request, strString, null );
  123.     }

  124.     /**
  125.      * Checks if a String contains characters that could be used for a cross-site scripting attack.
  126.      *
  127.      * @param request
  128.      *            The HTTP request
  129.      * @param strValue
  130.      *            a character String
  131.      * @param strXssCharacters
  132.      *            a String wich contain a list of Xss characters to check in strValue
  133.      * @return true if the String contains illegal characters
  134.      */
  135.     public static boolean containsXssCharacters( HttpServletRequest request, String strValue, String strXssCharacters )
  136.     {
  137.         boolean bContains = ( strXssCharacters == null ) ? StringUtil.containsXssCharacters( strValue )
  138.                 : StringUtil.containsXssCharacters( strValue, strXssCharacters );

  139.         if ( bContains )
  140.         {
  141.             _log.warn( "SECURITY WARNING : XSS CHARACTERS DETECTED {}", ( ) -> dumpRequest( request ) );
  142.         }

  143.         return bContains;
  144.     }

  145.     /**
  146.      * Check if the value contains terms used for XML External Entity Injection
  147.      *
  148.      * @param strValue
  149.      *            The value
  150.      * @return true if
  151.      */
  152.     public static boolean containsXmlExternalEntityInjectionTerms( String strValue )
  153.     {
  154.         for ( String strTerm : XXE_TERMS )
  155.         {
  156.             if ( StringUtils.indexOfIgnoreCase( strValue, strTerm ) >= 0 )
  157.             {
  158.                 _log.warn( "SECURITY WARNING : XXE TERMS DETECTED : {}", ( ) -> dumpRequest( LocalVariables.getRequest( ) ) );
  159.                 return true;
  160.             }
  161.         }
  162.         return false;
  163.     }

  164.     /**
  165.      * Check if the value contains characters used for Path Manipulation
  166.      *
  167.      * @param request
  168.      *            The Http request
  169.      * @param strValue
  170.      *            The value
  171.      * @return true if
  172.      */
  173.     public static boolean containsPathManipulationChars( HttpServletRequest request, String strValue )
  174.     {
  175.         for ( String strTerm : PATH_MANIPULATION )
  176.         {
  177.             if ( strValue.contains( strTerm ) )
  178.             {
  179.                 _log.warn( "SECURITY WARNING : PATH_MANIPULATION DETECTED : {}", ( ) -> dumpRequest( request ) );
  180.                 return true;
  181.             }
  182.         }
  183.         return false;
  184.     }

  185.     /**
  186.      * Dump all request info
  187.      *
  188.      * @param request
  189.      *            The HTTP request
  190.      * @return A report containing all request info
  191.      */
  192.     public static String dumpRequest( HttpServletRequest request )
  193.     {
  194.         StringBuilder sbDump = new StringBuilder( "\r\n Request Dump : \r\n" );
  195.         if ( request != null )
  196.         {
  197.             dumpTitle( sbDump, "Request variables" );
  198.             dumpVariables( sbDump, request );
  199.             dumpTitle( sbDump, "Request parameters" );
  200.             dumpParameters( sbDump, request );
  201.             dumpTitle( sbDump, "Request headers" );
  202.             dumpHeaders( sbDump, request );
  203.         }
  204.         else
  205.         {
  206.             sbDump.append( "no request provided." );
  207.         }

  208.         return sbDump.toString( );
  209.     }

  210.     /**
  211.      * 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.
  212.      *
  213.      * @param request
  214.      *            The request
  215.      * @return The IP of the user that made the request
  216.      */
  217.     public static String getRealIp( HttpServletRequest request )
  218.     {
  219.         String strIPAddress = request.getHeader( CONSTANT_HTTP_HEADER_X_FORWARDED_FOR );

  220.         if ( strIPAddress != null )
  221.         {
  222.             while ( !strIPAddress.matches( PATTERN_IP_ADDRESS ) && strIPAddress.contains( CONSTANT_COMMA ) )
  223.             {
  224.                 String strIpForwarded = strIPAddress.substring( 0, strIPAddress.indexOf( CONSTANT_COMMA ) );
  225.                 strIPAddress = strIPAddress.substring( strIPAddress.indexOf( CONSTANT_COMMA ) ).replaceFirst( CONSTANT_COMMA, StringUtils.EMPTY ).trim( );

  226.                 if ( ( strIpForwarded != null ) && strIpForwarded.matches( PATTERN_IP_ADDRESS ) )
  227.                 {
  228.                     strIPAddress = strIpForwarded;
  229.                 }
  230.             }

  231.             if ( !strIPAddress.matches( PATTERN_IP_ADDRESS ) )
  232.             {
  233.                 strIPAddress = request.getRemoteAddr( );
  234.             }
  235.         }
  236.         else
  237.         {
  238.             strIPAddress = request.getRemoteAddr( );
  239.         }

  240.         return strIPAddress;
  241.     }

  242.     /**
  243.      * Validate a forward URL to avoid open redirect with url safe patterns found in properties
  244.      *
  245.      * @see SecurityUtil#isInternalRedirectUrlSafe(java.lang.String, javax.servlet.http.HttpServletRequest, java.lang.String)
  246.      *
  247.      * @param strUrl
  248.      * @param request
  249.      * @return true if valid
  250.      */
  251.     public static boolean isInternalRedirectUrlSafe( String strUrl, HttpServletRequest request )
  252.     {
  253.         String strAntPathMatcherPatternsValues = AppPropertiesService.getProperty( SecurityUtil.PROPERTY_REDIRECT_URL_SAFE_PATTERNS );

  254.         return isInternalRedirectUrlSafe( strUrl, request, strAntPathMatcherPatternsValues );
  255.     }

  256.     /**
  257.      * 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
  258.      * external url redirection control, use the plugin plugin-verifybackurl)
  259.      *
  260.      * 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
  261.      * pattern list
  262.      *
  263.      * 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 :
  264.      * http://anothersite.com , https://anothersite.com , //anothersite.com , file://my.txt , ...
  265.      *
  266.      *
  267.      * @param strUrl
  268.      *            the Url to validate
  269.      * @param request
  270.      *            the current request (containing the baseUrl)
  271.      * @param strAntPathMatcherPatterns
  272.      *            a comma separated list of AntPathMatcher patterns, as "http://**.lutece.com,https://**.lutece.com"
  273.      * @return true if valid
  274.      */
  275.     public static boolean isInternalRedirectUrlSafe( String strUrl, HttpServletRequest request, String strAntPathMatcherPatterns )
  276.     {

  277.         if ( StringUtils.isBlank( strUrl ) )
  278.         {
  279.             return true; // this is not a valid redirect Url, but it is not unsafe
  280.         }

  281.         // filter schemes
  282.         boolean [ ] conditions = new boolean [ ] {
  283.                 !strUrl.startsWith( "//" ), !strUrl.startsWith( "http:" ), !strUrl.startsWith( "https:" ), !strUrl.contains( "://" ),
  284.                 !strUrl.startsWith( "javascript:" )
  285.         };

  286.         if ( BooleanUtils.and( conditions ) )
  287.         {
  288.             return true; // should be a relative path
  289.         }

  290.         // compare with current baseUrl
  291.         if ( strUrl.startsWith( AppPathService.getBaseUrl( request ) ) )
  292.         {
  293.             return true;
  294.         }

  295.         // compare with allowed url patterns
  296.         if ( !StringUtils.isBlank( strAntPathMatcherPatterns ) )
  297.         {
  298.             AntPathMatcher pathMatcher = new AntPathMatcher( );

  299.             String [ ] strAntPathMatcherPatternsTab = strAntPathMatcherPatterns.split( CONSTANT_COMMA );
  300.             for ( String pattern : strAntPathMatcherPatternsTab )
  301.             {
  302.                 if ( pattern != null && pathMatcher.match( pattern, strUrl ) )
  303.                 {
  304.                     return true;
  305.                 }
  306.             }
  307.         }

  308.         // the Url does not match the allowed patterns
  309.         _log.warn( "SECURITY WARNING : OPEN_REDIRECT DETECTED : {}", ( ) -> dumpRequest( request ) );

  310.         return false;

  311.     }

  312.     /**
  313.      * Identify user data saved in log files to prevent Log Forging attacks
  314.      *
  315.      * @param strUserInputData
  316.      *            User Input Data
  317.      * @return The User Data to log
  318.      */
  319.     public static String logForgingProtect( String strUserInputData )
  320.     {
  321.         int nCharCount = strUserInputData.length( );
  322.         int nLineCount = StringUtils.countMatches( strUserInputData, "\n" );
  323.         String strPrefixedLines = strUserInputData.replace( "\n", "\n** " );
  324.         return "\n** USER INPUT DATA : BEGIN (" + nLineCount + " lines and " + nCharCount + " chars) ** \n" + strPrefixedLines + "\n** USER INPUT DATA : END\n";
  325.     }

  326.     /**
  327.      * Write a title into the dump stringbuffer
  328.      *
  329.      * @param sbDump
  330.      *            The dump stringbuffer
  331.      * @param strTitle
  332.      *            The title
  333.      */
  334.     private static void dumpTitle( StringBuilder sbDump, String strTitle )
  335.     {
  336.         sbDump.append( "** " );
  337.         sbDump.append( strTitle );
  338.         sbDump.append( "  **\r\n" );
  339.     }

  340.     /**
  341.      * Write request variables into the dump stringbuffer
  342.      *
  343.      * @param sb
  344.      *            The dump stringbuffer
  345.      * @param request
  346.      *            The HTTP request
  347.      */
  348.     private static void dumpVariables( StringBuilder sb, HttpServletRequest request )
  349.     {
  350.         dumpVariable( sb, "AUTH_TYPE", request.getAuthType( ) );
  351.         dumpVariable( sb, "REQUEST_METHOD", request.getMethod( ) );
  352.         dumpVariable( sb, "PATH_INFO", request.getPathInfo( ) );
  353.         dumpVariable( sb, "PATH_TRANSLATED", request.getPathTranslated( ) );
  354.         dumpVariable( sb, "QUERY_STRING", request.getQueryString( ) );
  355.         dumpVariable( sb, "REQUEST_URI", request.getRequestURI( ) );
  356.         dumpVariable( sb, "SCRIPT_NAME", request.getServletPath( ) );
  357.         dumpVariable( sb, "LOCAL_ADDR", request.getLocalAddr( ) );
  358.         dumpVariable( sb, "SERVER_PROTOCOL", request.getProtocol( ) );
  359.         dumpVariable( sb, "REMOTE_ADDR", request.getRemoteAddr( ) );
  360.         dumpVariable( sb, "REMOTE_HOST", request.getRemoteHost( ) );
  361.         dumpVariable( sb, "HTTPS", request.getScheme( ) );
  362.         dumpVariable( sb, "SERVER_NAME", request.getServerName( ) );
  363.         dumpVariable( sb, "SERVER_PORT", String.valueOf( request.getServerPort( ) ) );
  364.     }

  365.     /**
  366.      * Write request headers infos into the dump stringbuffer
  367.      *
  368.      * @param sb
  369.      *            The dump stringbuffer
  370.      * @param request
  371.      *            The HTTP request
  372.      */
  373.     private static void dumpHeaders( StringBuilder sb, HttpServletRequest request )
  374.     {
  375.         Enumeration<String> values;
  376.         String key;
  377.         Enumeration<String> headers = request.getHeaderNames( );

  378.         while ( headers.hasMoreElements( ) )
  379.         {
  380.             key = headers.nextElement( );
  381.             values = request.getHeaders( key );

  382.             while ( values.hasMoreElements( ) )
  383.             {
  384.                 dumpVariable( sb, key, values.nextElement( ) );
  385.             }
  386.         }
  387.     }

  388.     /**
  389.      * Write request parameters infos into the dump stringbuffer
  390.      *
  391.      * @param sb
  392.      *            The dump stringbuffer
  393.      * @param request
  394.      *            The HTTP request
  395.      */
  396.     private static void dumpParameters( StringBuilder sb, HttpServletRequest request )
  397.     {
  398.         String key;
  399.         String [ ] values;

  400.         Enumeration<String> e = request.getParameterNames( );

  401.         while ( e.hasMoreElements( ) )
  402.         {
  403.             key = e.nextElement( );
  404.             values = request.getParameterValues( key );

  405.             int length = values.length;

  406.             for ( int i = 0; i < length; i++ )
  407.             {
  408.                 dumpVariable( sb, key, values [i] );
  409.             }
  410.         }
  411.     }

  412.     /**
  413.      * Write name / value infos into the dump stringbuffer
  414.      *
  415.      * @param sb
  416.      *            The dump string buffer
  417.      * @param strName
  418.      *            The info name
  419.      * @param strValue
  420.      *            The info value
  421.      */
  422.     private static void dumpVariable( StringBuilder sb, String strName, String strValue )
  423.     {
  424.         sb.append( strName );
  425.         sb.append( " : \"" );
  426.         sb.append( strValue );
  427.         sb.append( "\"\r\n" );
  428.     }
  429. }