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