26
26
import org .apache .commons .lang3 .time .DurationFormatUtils ;
27
27
import org .silverpeas .core .admin .user .model .User ;
28
28
import org .silverpeas .core .cache .service .CacheAccessorProvider ;
29
+ import org .silverpeas .core .jcr .webdav .WebDavProtocol ;
29
30
import org .silverpeas .core .persistence .jdbc .DBUtil ;
30
- import org .silverpeas .kernel .util .StringUtil ;
31
31
import org .silverpeas .core .util .URLUtil ;
32
- import org .silverpeas .kernel .logging .SilverLogger ;
33
32
import org .silverpeas .core .util .security .SecuritySettings ;
34
33
import org .silverpeas .core .web .SilverpeasWebResource ;
35
34
import org .silverpeas .core .web .filter .exception .WebSecurityException ;
36
35
import org .silverpeas .core .web .filter .exception .WebSqlInjectionSecurityException ;
37
36
import org .silverpeas .core .web .filter .exception .WebXssInjectionSecurityException ;
38
37
import org .silverpeas .core .web .http .HttpRequest ;
39
- import org .silverpeas .core .jcr .webdav .WebDavProtocol ;
38
+ import org .silverpeas .kernel .annotation .NonNull ;
39
+ import org .silverpeas .kernel .logging .SilverLogger ;
40
+ import org .silverpeas .kernel .util .StringUtil ;
40
41
41
- import javax .servlet .Filter ;
42
- import javax .servlet .FilterChain ;
43
- import javax .servlet .FilterConfig ;
44
- import javax .servlet .ServletException ;
45
- import javax .servlet .ServletRequest ;
46
- import javax .servlet .ServletResponse ;
42
+ import javax .servlet .*;
47
43
import javax .servlet .http .HttpServletRequest ;
48
44
import javax .servlet .http .HttpServletResponse ;
45
+ import javax .ws .rs .InternalServerErrorException ;
49
46
import javax .ws .rs .core .UriBuilder ;
47
+ import java .io .BufferedInputStream ;
50
48
import java .io .IOException ;
49
+ import java .io .InputStream ;
50
+ import java .io .OutputStream ;
51
51
import java .util .ArrayList ;
52
52
import java .util .List ;
53
53
import java .util .Map ;
54
+ import java .util .Optional ;
54
55
import java .util .regex .Matcher ;
55
56
import java .util .regex .Pattern ;
56
57
62
63
* <p>
63
64
* For now, this filter ensures HTTPS is used in secured connections, blocks content sniffing of web
64
65
* browsers, and checks XSS and SQL injections in URLs.
66
+ *
65
67
* @author Yohann Chastagnier
66
68
*/
67
69
public class MassiveWebSecurityFilter implements Filter {
@@ -128,7 +130,7 @@ public class MassiveWebSecurityFilter implements Filter {
128
130
@ Override
129
131
public void doFilter (final ServletRequest request , final ServletResponse response ,
130
132
final FilterChain chain ) throws IOException , ServletException {
131
- final HttpRequest httpRequest = ( HttpRequest ) request ;
133
+ final HttpRequest httpRequest = new HttpRequestWrapper (( HttpRequest ) request ) ;
132
134
final HttpServletResponse httpResponse = (HttpServletResponse ) response ;
133
135
try {
134
136
setDefaultSecurity (httpRequest , httpResponse );
@@ -205,11 +207,40 @@ private void checkWebInjection(final HttpRequest httpRequest,
205
207
// this header isn't taken in charge by all web browsers.
206
208
httpResponse .setHeader ("X-XSS-Protection" , "1" );
207
209
}
210
+ checkRequestEntityForInjection (httpRequest );
208
211
checkRequestParametersForInjection (httpRequest , isWebSqlInjectionSecurityEnabled ,
209
212
isWebXssInjectionSecurityEnabled );
210
213
}
211
214
}
212
215
216
+ private void checkRequestEntityForInjection (final HttpRequest request )
217
+ throws WebSqlInjectionSecurityException , WebXssInjectionSecurityException {
218
+ long start = System .currentTimeMillis ();
219
+ try {
220
+ boolean hasSupportedWebEntity = Optional .ofNullable (request .getContentType ())
221
+ .map (String ::toLowerCase )
222
+ .filter (c -> c .contains ("json" ) || c .contains ("xml" ))
223
+ .isPresent ();
224
+ if (hasSupportedWebEntity ) {
225
+ String charset = request .getCharacterEncoding () == null ? "UTF-8" :
226
+ request .getCharacterEncoding ();
227
+ InputStream body = request .getInputStream ();
228
+ if (body .markSupported ()) {
229
+ body .mark (Integer .MAX_VALUE );
230
+ String entity = new String (body .readAllBytes (), charset );
231
+ checkValueForInjection (entity , true , true );
232
+ body .reset ();
233
+ }
234
+ }
235
+ } catch (IOException e ) {
236
+ throw new InternalServerErrorException (e );
237
+ } finally {
238
+ long end = System .currentTimeMillis ();
239
+ logger .debug ("Massive Web Security Verify on request entity: " +
240
+ DurationFormatUtils .formatDurationHMS (end - start ));
241
+ }
242
+ }
243
+
213
244
private void checkRequestParametersForInjection (final HttpRequest httpRequest ,
214
245
final boolean isWebSqlInjectionSecurityEnabled ,
215
246
final boolean isWebXssInjectionSecurityEnabled )
@@ -231,44 +262,50 @@ private void checkRequestParametersForInjection(final HttpRequest httpRequest,
231
262
}
232
263
} finally {
233
264
long end = System .currentTimeMillis ();
234
- logger .debug ("Massive Web Security Verify duration : " +
265
+ logger .debug ("Massive Web Security Verify on request parameters : " +
235
266
DurationFormatUtils .formatDurationHMS (end - start ));
236
267
}
237
268
}
238
269
239
270
private void checkParameterValues (final Map .Entry <String , String []> parameterEntry ,
240
271
final boolean sqlInjectionToVerify , final boolean xssInjectionToVerify )
241
272
throws WebSqlInjectionSecurityException , WebXssInjectionSecurityException {
242
- Matcher patternMatcherFound ;
243
273
for (String parameterValue : parameterEntry .getValue ()) {
274
+ checkValueForInjection (parameterValue , sqlInjectionToVerify , xssInjectionToVerify );
275
+ }
276
+ }
244
277
245
- // Each sequence of spaces is replaced by one space
246
- parameterValue = parameterValue . replaceAll ( " \\ s+" , " " );
247
-
248
- // SQL injections?
249
- if ( sqlInjectionToVerify && ( patternMatcherFound =
250
- findPatternMatcherFromString ( SQL_PATTERNS , parameterValue , true )) != null ) {
278
+ private void checkValueForInjection ( String value , boolean sqlInjectionToVerify ,
279
+ boolean xssInjectionToVerify ) throws WebSqlInjectionSecurityException ,
280
+ WebXssInjectionSecurityException {
281
+ Matcher patternMatcherFound ;
282
+ // Each sequence of spaces is replaced by one space
283
+ value = value . replaceAll ( " \\ s+" , " " );
251
284
252
- if (! verifySqlDeeply ( patternMatcherFound , parameterValue )) {
253
- patternMatcherFound = null ;
254
- }
285
+ // SQL injections?
286
+ if ( sqlInjectionToVerify && ( patternMatcherFound =
287
+ findPatternMatcherFromString ( SQL_PATTERNS , value , true )) != null ) {
255
288
256
- if (patternMatcherFound != null ) {
257
- throw new WebSqlInjectionSecurityException ();
258
- }
289
+ if (!verifySqlDeeply (patternMatcherFound , value )) {
290
+ patternMatcherFound = null ;
259
291
}
260
292
261
- // XSS injections?
262
- if (xssInjectionToVerify &&
263
- findPatternMatcherFromString (XSS_PATTERNS , parameterValue , false ) != null ) {
264
- throw new WebXssInjectionSecurityException ();
293
+ if (patternMatcherFound != null ) {
294
+ throw new WebSqlInjectionSecurityException ();
265
295
}
266
296
}
297
+
298
+ // XSS injections?
299
+ if (xssInjectionToVerify &&
300
+ findPatternMatcherFromString (XSS_PATTERNS , value , false ) != null ) {
301
+ throw new WebXssInjectionSecurityException ();
302
+ }
267
303
}
268
304
269
305
/**
270
306
* Verifies deeply a matched SQL string. Indeed, throwing an exception of XSS attack only on SQL
271
307
* detection is not enough. This method tries to detect a known table name from the SQL string.
308
+ *
272
309
* @param matcherFound a pattern matcher
273
310
* @param statement a SQL statement to check
274
311
* @return true of the SQL statement is considered as safe. False otherwise.
@@ -297,6 +334,7 @@ private boolean verifySqlDeeply(final Matcher matcherFound, String statement) {
297
334
/**
298
335
* Extracts the whole table name matching the given pattern. Indeed, the matcher can find a table
299
336
* name that is a part of another one.
337
+ *
300
338
* @param matcher a pattern matcher.
301
339
* @param matchedString a SQL statement part
302
340
* @return a whole table name
@@ -330,6 +368,7 @@ private String extractTableNameWholeWord(Matcher matcher, String matchedString)
330
368
/**
331
369
* Gets a pattern that permits to check deeply a detected SELECT FROM with known table names. A
332
370
* cache is handled by this method in order to avoid building at every call the same pattern.
371
+ *
333
372
* @return a regexp pattern.
334
373
*/
335
374
private synchronized Pattern getSqlTableNamesPattern () {
@@ -357,6 +396,7 @@ private synchronized Pattern getSqlTableNamesPattern() {
357
396
358
397
/**
359
398
* Must the given parameter be skipped from SQL injection verifying?
399
+ *
360
400
* @param parameterName name of a parameter.
361
401
* @return true if the given parameter has to be skipped. False otherwise.
362
402
*/
@@ -367,6 +407,7 @@ private boolean mustTheParameterBeVerifiedForSqlVerifications(String parameterNa
367
407
368
408
/**
369
409
* Must the given parameter be skipped from XSS injection verifying?
410
+ *
370
411
* @param parameterName name of a parameter.
371
412
* @return true of the given parameter has to be skipped. False otherwise.
372
413
*/
@@ -378,6 +419,7 @@ private boolean mustTheParameterBeVerifiedForXssVerifications(String parameterNa
378
419
/**
379
420
* Gets the matcher corresponding to the pattern in the given list of patterns and for which the
380
421
* specified string is compliant.
422
+ *
381
423
* @param patterns a list of pattern to apply on the given string.
382
424
* @param string a string to check.
383
425
* @param startsAndEndsByWholeWord a flag indicating the pattern should match for the first and
@@ -401,6 +443,7 @@ private Matcher findPatternMatcherFromString(List<Pattern> patterns, String stri
401
443
402
444
/**
403
445
* Verifies that the first word of matching starts with a whole word.
446
+ *
404
447
* @param matcher a matcher.
405
448
* @param matchedString a string.
406
449
* @return true if the first word of matching starts with a whole word
@@ -412,6 +455,7 @@ private boolean verifyMatcherStartingByAWord(Matcher matcher, String matchedStri
412
455
413
456
/**
414
457
* Verifies that the first word of matching ends with a whole word.
458
+ *
415
459
* @param matcher a matcher
416
460
* @param matchedString a string
417
461
* @return true if the first word of matching ends with a whole word.
@@ -435,4 +479,136 @@ public void init(final FilterConfig filterConfig) throws ServletException {
435
479
public void destroy () {
436
480
// Nothing to do.
437
481
}
482
+
483
+ /**
484
+ * Wrapper of an {@link HttpRequest} to buffer the input stream on its body in order to
485
+ * allow access and back-and-forth navigation within the body content through the input
486
+ * stream.
487
+ */
488
+ private static class HttpRequestWrapper extends HttpRequest {
489
+
490
+ private BufferedServletInputStream input ;
491
+
492
+ /**
493
+ * Constructs a request object wrapping the given request.
494
+ *
495
+ * @param request the {@link HttpServletRequest} to be wrapped.
496
+ * @throws IllegalArgumentException if the request is null
497
+ */
498
+ public HttpRequestWrapper (HttpRequest request ) {
499
+ super (request );
500
+ }
501
+
502
+ /**
503
+ * Gets the input stream on the content of the request's body. The input stream is buffered and,
504
+ * as such, position in the stream can be marked and hence reset to the last mark (last
505
+ * marked position in the stream).
506
+ * @return a buffered {@link ServletInputStream}.
507
+ * @throws IOException if an error occurs while opening an input stream on the content of the
508
+ * request's body.
509
+ */
510
+ @ Override
511
+ public ServletInputStream getInputStream () throws IOException {
512
+ if (input == null ) {
513
+ input = new BufferedServletInputStream (super .getInputStream ());
514
+ }
515
+ return input ;
516
+ }
517
+
518
+ private static class BufferedServletInputStream extends ServletInputStream {
519
+
520
+ private final ServletInputStream inputStream ;
521
+ private final BufferedInputStream buffer ;
522
+
523
+ private BufferedServletInputStream (ServletInputStream inputStream ) {
524
+ this .inputStream = inputStream ;
525
+ this .buffer = new BufferedInputStream (inputStream );
526
+ }
527
+
528
+ @ Override
529
+ public boolean isFinished () {
530
+ try {
531
+ return this .buffer .available () == 0 ;
532
+ } catch (IOException e ) {
533
+ return true ;
534
+ }
535
+ }
536
+
537
+ @ Override
538
+ public boolean isReady () {
539
+ return !isFinished ();
540
+ }
541
+
542
+ @ Override
543
+ public void setReadListener (ReadListener readListener ) {
544
+ this .inputStream .setReadListener (readListener );
545
+ }
546
+
547
+ @ Override
548
+ public int read () throws IOException {
549
+ return buffer .read ();
550
+ }
551
+
552
+ @ Override
553
+ public int read (@ NonNull byte [] b , int off , int len ) throws IOException {
554
+ return buffer .read (b , off , len );
555
+ }
556
+
557
+ @ Override
558
+ public long skip (long n ) throws IOException {
559
+ return buffer .skip (n );
560
+ }
561
+
562
+ @ Override
563
+ public int available () throws IOException {
564
+ return buffer .available ();
565
+ }
566
+
567
+ @ Override
568
+ public synchronized void mark (int readLimit ) {
569
+ buffer .mark (readLimit );
570
+ }
571
+
572
+ @ Override
573
+ public synchronized void reset () throws IOException {
574
+ buffer .reset ();
575
+ }
576
+
577
+ @ Override
578
+ public boolean markSupported () {
579
+ return buffer .markSupported ();
580
+ }
581
+
582
+ @ Override
583
+ public void close () throws IOException {
584
+ buffer .close ();
585
+ }
586
+
587
+ @ Override
588
+ public int read (@ NonNull byte [] b ) throws IOException {
589
+ return buffer .read (b );
590
+ }
591
+
592
+ @ Override
593
+ public byte [] readAllBytes () throws IOException {
594
+ return buffer .readAllBytes ();
595
+ }
596
+
597
+ @ Override
598
+ public byte [] readNBytes (int len ) throws IOException {
599
+ return buffer .readNBytes (len );
600
+ }
601
+
602
+ @ Override
603
+ public int readNBytes (byte [] b , int off , int len ) throws IOException {
604
+ return buffer .readNBytes (b , off , len );
605
+ }
606
+
607
+ @ Override
608
+ public long transferTo (OutputStream out ) throws IOException {
609
+ return buffer .transferTo (out );
610
+ }
611
+
612
+ }
613
+ }
438
614
}
0 commit comments