View Javadoc
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  
36  import java.util.Enumeration;
37  
38  import javax.servlet.http.HttpServletRequest;
39  
40  import org.apache.commons.lang3.BooleanUtils;
41  import org.apache.commons.lang3.StringUtils;
42  import org.apache.logging.log4j.LogManager;
43  import org.apache.logging.log4j.Logger;
44  import org.springframework.util.AntPathMatcher;
45  
46  import fr.paris.lutece.portal.service.util.AppPathService;
47  import fr.paris.lutece.portal.service.util.AppPropertiesService;
48  import fr.paris.lutece.portal.web.LocalVariables;
49  import fr.paris.lutece.util.string.StringUtil;
50  
51  /**
52   * Security utils
53   *
54   */
55  public final class SecurityUtil
56  {
57      private static final String LOGGER_NAME = "lutece.security.http";
58      private static final String CONSTANT_HTTP_HEADER_X_FORWARDED_FOR = "X-Forwarded-For";
59      private static final String PATTERN_IP_ADDRESS = "^([0-9]{1,3}\\.){3}[0-9]{1,3}$";
60      private static final String CONSTANT_COMMA = ",";
61      private static final String [ ] XXE_TERMS = {
62              "!DOCTYPE", "!ELEMENT", "!ENTITY"
63      };
64      private static final String [ ] PATH_MANIPULATION = {
65              "..", "/", "\\"
66      };
67  
68      public static final String PROPERTY_REDIRECT_URL_SAFE_PATTERNS = "lutece.security.redirectUrlSafePatterns";
69      public static final Logger _log = LogManager.getLogger( LOGGER_NAME );
70  
71      /**
72       * Private Constructor
73       */
74      private SecurityUtil( )
75      {
76      }
77  
78      /**
79       * Scan request parameters to see if there no malicious code.
80       *
81       * @param request
82       *            The HTTP request
83       * @return true if all parameters don't contains any special characters
84       */
85      public static boolean containsCleanParameters( HttpServletRequest request )
86      {
87          return containsCleanParameters( request, null );
88      }
89  
90      /**
91       * Scan request parameters to see if there no malicious code.
92       *
93       * @param request
94       *            The HTTP request
95       * @param strXssCharacters
96       *            a String wich contain a list of Xss characters to check in strValue
97       * @return true if all parameters don't contains any special characters
98       */
99      public static boolean containsCleanParameters( HttpServletRequest request, String strXssCharacters )
100     {
101         String key;
102         String [ ] values;
103         Enumeration<String> e = request.getParameterNames( );
104 
105         while ( e.hasMoreElements( ) )
106         {
107             key = e.nextElement( );
108             values = request.getParameterValues( key );
109 
110             int length = values.length;
111 
112             for ( int i = 0; i < length; i++ )
113             {
114                 if ( SecurityUtil.containsXssCharacters( request, values [i], strXssCharacters ) || SecurityUtil.containsXssCharacters( request, key, strXssCharacters )  )
115                 {
116                     _log.warn( "SECURITY WARNING : INVALID REQUEST PARAMETERS {}", ( ) -> dumpRequest( request ) );
117 
118                     return false;
119                 }
120             }
121         }
122 
123         return true;
124     }
125 
126     /**
127      * Checks if a String contains characters that could be used for a cross-site scripting attack.
128      *
129      * @param request
130      *            The HTTP request
131      * @param strString
132      *            a character String
133      * @return true if the String contains illegal characters
134      */
135     public static boolean containsXssCharacters( HttpServletRequest request, String strString )
136     {
137         return containsXssCharacters( request, strString, null );
138     }
139 
140     /**
141      * Checks if a String contains characters that could be used for a cross-site scripting attack.
142      *
143      * @param request
144      *            The HTTP request
145      * @param strValue
146      *            a character String
147      * @param strXssCharacters
148      *            a String wich contain a list of Xss characters to check in strValue
149      * @return true if the String contains illegal characters
150      */
151     public static boolean containsXssCharacters( HttpServletRequest request, String strValue, String strXssCharacters )
152     {
153         boolean bContains = ( strXssCharacters == null ) ? StringUtil.containsXssCharacters( strValue )
154                 : StringUtil.containsXssCharacters( strValue, strXssCharacters );
155 
156         if ( bContains )
157         {
158             _log.warn( "SECURITY WARNING : XSS CHARACTERS DETECTED {}", ( ) -> dumpRequest( request ) );
159         }
160 
161         return bContains;
162     }
163 
164     /**
165      * Check if the value contains terms used for XML External Entity Injection
166      * 
167      * @param strValue
168      *            The value
169      * @return true if
170      */
171     public static boolean containsXmlExternalEntityInjectionTerms( String strValue )
172     {
173         for ( String strTerm : XXE_TERMS )
174         {
175             if ( StringUtils.indexOfIgnoreCase( strValue, strTerm ) >= 0 )
176             {
177                 _log.warn( "SECURITY WARNING : XXE TERMS DETECTED : {}", ( ) -> dumpRequest( LocalVariables.getRequest( ) ) );
178                 return true;
179             }
180         }
181         return false;
182     }
183 
184     /**
185      * Check if the value contains characters used for Path Manipulation
186      * 
187      * @param request
188      *            The Http request
189      * @param strValue
190      *            The value
191      * @return true if
192      */
193     public static boolean containsPathManipulationChars( HttpServletRequest request, String strValue )
194     {
195         for ( String strTerm : PATH_MANIPULATION )
196         {
197             if ( strValue.contains( strTerm ) )
198             {
199                 _log.warn( "SECURITY WARNING : PATH_MANIPULATION DETECTED : {}", ( ) -> dumpRequest( request ) );
200                 return true;
201             }
202         }
203         return false;
204     }
205 
206     /**
207      * Dump all request info
208      * 
209      * @param request
210      *            The HTTP request
211      * @return A report containing all request info
212      */
213     public static String dumpRequest( HttpServletRequest request )
214     {
215         StringBuilder sbDump = new StringBuilder( "\r\n Request Dump : \r\n" );
216         if ( request != null )
217         {
218             dumpTitle( sbDump, "Request variables" );
219             dumpVariables( sbDump, request );
220             dumpTitle( sbDump, "Request parameters" );
221             dumpParameters( sbDump, request );
222             dumpTitle( sbDump, "Request headers" );
223             dumpHeaders( sbDump, request );
224         }
225         else
226         {
227             sbDump.append( "no request provided." );
228         }
229 
230         return sbDump.toString( );
231     }
232 
233     /**
234      * 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.
235      * 
236      * @param request
237      *            The request
238      * @return The IP of the user that made the request
239      */
240     public static String getRealIp( HttpServletRequest request )
241     {
242         String strIPAddress = request.getHeader( CONSTANT_HTTP_HEADER_X_FORWARDED_FOR );
243 
244         if ( strIPAddress != null )
245         {
246             while ( !strIPAddress.matches( PATTERN_IP_ADDRESS ) && strIPAddress.contains( CONSTANT_COMMA ) )
247             {
248                 String strIpForwarded = strIPAddress.substring( 0, strIPAddress.indexOf( CONSTANT_COMMA ) );
249                 strIPAddress = strIPAddress.substring( strIPAddress.indexOf( CONSTANT_COMMA ) ).replaceFirst( CONSTANT_COMMA, StringUtils.EMPTY ).trim( );
250 
251                 if ( ( strIpForwarded != null ) && strIpForwarded.matches( PATTERN_IP_ADDRESS ) )
252                 {
253                     strIPAddress = strIpForwarded;
254                 }
255             }
256 
257             if ( !strIPAddress.matches( PATTERN_IP_ADDRESS ) )
258             {
259                 strIPAddress = request.getRemoteAddr( );
260             }
261         }
262         else
263         {
264             strIPAddress = request.getRemoteAddr( );
265         }
266 
267         return strIPAddress;
268     }
269 
270     /**
271      * Validate a forward URL to avoid open redirect with url safe patterns found in properties
272      * 
273      * @see SecurityUtil#isInternalRedirectUrlSafe(java.lang.String, javax.servlet.http.HttpServletRequest, java.lang.String)
274      * 
275      * @param strUrl
276      * @param request
277      * @return true if valid
278      */
279     public static boolean isInternalRedirectUrlSafe( String strUrl, HttpServletRequest request )
280     {
281         String strAntPathMatcherPatternsValues = AppPropertiesService.getProperty( SecurityUtil.PROPERTY_REDIRECT_URL_SAFE_PATTERNS );
282 
283         return isInternalRedirectUrlSafe( strUrl, request, strAntPathMatcherPatternsValues );
284     }
285 
286     /**
287      * 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
288      * external url redirection control, use the plugin plugin-verifybackurl)
289      * 
290      * 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
291      * pattern list
292      * 
293      * 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 :
294      * http://anothersite.com , https://anothersite.com , //anothersite.com , file://my.txt , ...
295      * 
296      * 
297      * @param strUrl
298      *            the Url to validate
299      * @param request
300      *            the current request (containing the baseUrl)
301      * @param strAntPathMatcherPatterns
302      *            a comma separated list of AntPathMatcher patterns, as "https://**.lutece.com"" target="alexandria_uri">http://**.lutece.com,https://**.lutece.com"
303      * @return true if valid
304      */
305     public static boolean isInternalRedirectUrlSafe( String strUrl, HttpServletRequest request, String strAntPathMatcherPatterns )
306     {
307 
308         if ( StringUtils.isBlank( strUrl ) )
309         {
310             return true; // this is not a valid redirect Url, but it is not unsafe
311         }
312 
313         // filter schemes
314         boolean [ ] conditions = new boolean [ ] {
315                 !strUrl.startsWith( "//" ), !strUrl.startsWith( "http:" ), !strUrl.startsWith( "https:" ), !strUrl.contains( "://" ),
316                 !strUrl.startsWith( "javascript:" )
317         };
318 
319         if ( BooleanUtils.and( conditions ) )
320         {
321             return true; // should be a relative path
322         }
323 
324         // compare with current baseUrl
325         if ( strUrl.startsWith( AppPathService.getBaseUrl( request ) ) )
326         {
327             return true;
328         }
329 
330         // compare with allowed url patterns
331         if ( !StringUtils.isBlank( strAntPathMatcherPatterns ) )
332         {
333             AntPathMatcher pathMatcher = new AntPathMatcher( );
334 
335             String [ ] strAntPathMatcherPatternsTab = strAntPathMatcherPatterns.split( CONSTANT_COMMA );
336             for ( String pattern : strAntPathMatcherPatternsTab )
337             {
338                 if ( pattern != null && pathMatcher.match( pattern, strUrl ) )
339                 {
340                     return true;
341                 }
342             }
343         }
344 
345         // the Url does not match the allowed patterns
346         _log.warn( "SECURITY WARNING : OPEN_REDIRECT DETECTED : {}", ( ) -> dumpRequest( request ) );
347 
348         return false;
349 
350     }
351 
352     /**
353      * Identify user data saved in log files to prevent Log Forging attacks
354      * 
355      * @param strUserInputData
356      *            User Input Data
357      * @return The User Data to log
358      */
359     public static String logForgingProtect( String strUserInputData )
360     {
361         int nCharCount = strUserInputData.length( );
362         int nLineCount = StringUtils.countMatches( strUserInputData, "\n" );
363         String strPrefixedLines = strUserInputData.replace( "\n", "\n** " );
364         return "\n** USER INPUT DATA : BEGIN (" + nLineCount + " lines and " + nCharCount + " chars) ** \n" + strPrefixedLines + "\n** USER INPUT DATA : END\n";
365     }
366 
367     /**
368      * Write a title into the dump stringbuffer
369      * 
370      * @param sbDump
371      *            The dump stringbuffer
372      * @param strTitle
373      *            The title
374      */
375     private static void dumpTitle( StringBuilder sbDump, String strTitle )
376     {
377         sbDump.append( "** " );
378         sbDump.append( strTitle );
379         sbDump.append( "  **\r\n" );
380     }
381 
382     /**
383      * Write request variables into the dump stringbuffer
384      * 
385      * @param sb
386      *            The dump stringbuffer
387      * @param request
388      *            The HTTP request
389      */
390     private static void dumpVariables( StringBuilder sb, HttpServletRequest request )
391     {
392         dumpVariable( sb, "AUTH_TYPE", request.getAuthType( ) );
393         dumpVariable( sb, "REQUEST_METHOD", request.getMethod( ) );
394         dumpVariable( sb, "PATH_INFO", request.getPathInfo( ) );
395         dumpVariable( sb, "PATH_TRANSLATED", request.getPathTranslated( ) );
396         dumpVariable( sb, "QUERY_STRING", request.getQueryString( ) );
397         dumpVariable( sb, "REQUEST_URI", request.getRequestURI( ) );
398         dumpVariable( sb, "SCRIPT_NAME", request.getServletPath( ) );
399         dumpVariable( sb, "LOCAL_ADDR", request.getLocalAddr( ) );
400         dumpVariable( sb, "SERVER_PROTOCOL", request.getProtocol( ) );
401         dumpVariable( sb, "REMOTE_ADDR", request.getRemoteAddr( ) );
402         dumpVariable( sb, "REMOTE_HOST", request.getRemoteHost( ) );
403         dumpVariable( sb, "HTTPS", request.getScheme( ) );
404         dumpVariable( sb, "SERVER_NAME", request.getServerName( ) );
405         dumpVariable( sb, "SERVER_PORT", String.valueOf( request.getServerPort( ) ) );
406     }
407 
408     /**
409      * Write request headers infos into the dump stringbuffer
410      * 
411      * @param sb
412      *            The dump stringbuffer
413      * @param request
414      *            The HTTP request
415      */
416     private static void dumpHeaders( StringBuilder sb, HttpServletRequest request )
417     {
418         Enumeration<String> values;
419         String key;
420         Enumeration<String> headers = request.getHeaderNames( );
421 
422         while ( headers.hasMoreElements( ) )
423         {
424             key = headers.nextElement( );
425             values = request.getHeaders( key );
426 
427             while ( values.hasMoreElements( ) )
428             {
429                 dumpVariable( sb, key, values.nextElement( ) );
430             }
431         }
432     }
433 
434     /**
435      * Write request parameters infos into the dump stringbuffer
436      * 
437      * @param sb
438      *            The dump stringbuffer
439      * @param request
440      *            The HTTP request
441      */
442     private static void dumpParameters( StringBuilder sb, HttpServletRequest request )
443     {
444         String key;
445         String [ ] values;
446 
447         Enumeration<String> e = request.getParameterNames( );
448 
449         while ( e.hasMoreElements( ) )
450         {
451             key = e.nextElement( );
452             values = request.getParameterValues( key );
453 
454             int length = values.length;
455 
456             for ( int i = 0; i < length; i++ )
457             {
458                 dumpVariable( sb, key, values [i] );
459             }
460         }
461     }
462 
463     /**
464      * Write name / value infos into the dump stringbuffer
465      * 
466      * @param sb
467      *            The dump string buffer
468      * @param strName
469      *            The info name
470      * @param strValue
471      *            The info value
472      */
473     private static void dumpVariable( StringBuilder sb, String strName, String strValue )
474     {
475         sb.append( strName );
476         sb.append( " : \"" );
477         sb.append( strValue );
478         sb.append( "\"\r\n" );
479     }
480 }