@@ -383,3 +383,178 @@ private class FileConstructorChildArgumentStep extends AdditionalTaintStep {
383
383
)
384
384
}
385
385
}
386
+
387
+ /** A call to `java.lang.String.replace` or `java.lang.String.replaceAll`. */
388
+ private class StringReplaceOrReplaceAllCall extends MethodCall {
389
+ StringReplaceOrReplaceAllCall ( ) {
390
+ this instanceof StringReplaceCall or
391
+ this instanceof StringReplaceAllCall
392
+ }
393
+ }
394
+
395
+ /** Gets a character used for replacement. */
396
+ private string getAReplacementChar ( ) { result = [ "" , "_" , "-" ] }
397
+
398
+ /** Gets a directory character represented as regex. */
399
+ private string getADirRegexChar ( ) { result = [ "\\." , "/" , "\\\\" ] }
400
+
401
+ /** Gets a directory character represented as a char. */
402
+ private string getADirChar ( ) { result = [ "." , "/" , "\\" ] }
403
+
404
+ /** Holds if `target` is the first argument of `replaceAllCall`. */
405
+ private predicate isReplaceAllTarget (
406
+ StringReplaceAllCall replaceAllCall , CompileTimeConstantExpr target
407
+ ) {
408
+ target = replaceAllCall .getArgument ( 0 )
409
+ }
410
+
411
+ /** Holds if `target` is the first argument of `replaceCall`. */
412
+ private predicate isReplaceTarget ( StringReplaceCall replaceCall , CompileTimeConstantExpr target ) {
413
+ target = replaceCall .getArgument ( 0 )
414
+ }
415
+
416
+ /** Holds if a single `replaceAllCall` replaces all directory characters. */
417
+ private predicate replacesDirectoryCharactersWithSingleReplaceAll (
418
+ StringReplaceAllCall replaceAllCall
419
+ ) {
420
+ exists ( CompileTimeConstantExpr target , string targetValue |
421
+ isReplaceAllTarget ( replaceAllCall , target ) and
422
+ target .getStringValue ( ) = targetValue and
423
+ replaceAllCall .getArgument ( 1 ) .( CompileTimeConstantExpr ) .getStringValue ( ) = getAReplacementChar ( )
424
+ |
425
+ not targetValue .matches ( "%[^%]%" ) and
426
+ targetValue .matches ( "[%.%]" ) and
427
+ targetValue .matches ( "[%/%]" ) and
428
+ // Search for "\\\\" (needs extra backslashes to avoid escaping the '%')
429
+ targetValue .matches ( "[%\\\\\\\\%]" )
430
+ or
431
+ targetValue .matches ( "%|%" ) and
432
+ targetValue .matches ( "%" + [ "[.]" , "\\." ] + "%" ) and
433
+ targetValue .matches ( "%/%" ) and
434
+ targetValue .matches ( "%\\\\\\\\%" )
435
+ )
436
+ }
437
+
438
+ /**
439
+ * Holds if there are two chained replacement calls, `rc1` and `rc2`, that replace
440
+ * '.' and one of '/' or '\'.
441
+ */
442
+ private predicate replacesDirectoryCharactersWithDoubleReplaceOrReplaceAll (
443
+ StringReplaceOrReplaceAllCall rc1
444
+ ) {
445
+ exists (
446
+ CompileTimeConstantExpr target1 , string targetValue1 , StringReplaceOrReplaceAllCall rc2 ,
447
+ CompileTimeConstantExpr target2 , string targetValue2
448
+ |
449
+ rc1 instanceof StringReplaceAllCall and
450
+ isReplaceAllTarget ( rc1 , target1 ) and
451
+ isReplaceAllTarget ( rc2 , target2 ) and
452
+ targetValue1 = getADirRegexChar ( ) and
453
+ targetValue2 = getADirRegexChar ( )
454
+ or
455
+ rc1 instanceof StringReplaceCall and
456
+ isReplaceTarget ( rc1 , target1 ) and
457
+ isReplaceTarget ( rc2 , target2 ) and
458
+ targetValue1 = getADirChar ( ) and
459
+ targetValue2 = getADirChar ( )
460
+ |
461
+ rc2 .getQualifier ( ) = rc1 and
462
+ target1 .getStringValue ( ) = targetValue1 and
463
+ target2 .getStringValue ( ) = targetValue2 and
464
+ rc1 .getArgument ( 1 ) .( CompileTimeConstantExpr ) .getStringValue ( ) = getAReplacementChar ( ) and
465
+ rc2 .getArgument ( 1 ) .( CompileTimeConstantExpr ) .getStringValue ( ) = getAReplacementChar ( ) and
466
+ // make sure the calls replace different characters
467
+ targetValue2 != targetValue1 and
468
+ // make sure one of the calls replaces '.'
469
+ // then the other call must replace one of '/' or '\' if they are not equal
470
+ ( targetValue2 .matches ( "%.%" ) or targetValue1 .matches ( "%.%" ) )
471
+ )
472
+ }
473
+
474
+ /**
475
+ * A sanitizer that protects against path injection vulnerabilities by replacing
476
+ * directory characters ('..', '/', and '\') with safe characters.
477
+ */
478
+ private class ReplaceDirectoryCharactersSanitizer extends StringReplaceOrReplaceAllCall {
479
+ ReplaceDirectoryCharactersSanitizer ( ) {
480
+ replacesDirectoryCharactersWithSingleReplaceAll ( this ) or
481
+ replacesDirectoryCharactersWithDoubleReplaceOrReplaceAll ( this )
482
+ }
483
+ }
484
+
485
+ /** Holds if `target` is the first argument of `matchesCall`. */
486
+ private predicate isMatchesTarget ( StringMatchesCall matchesCall , CompileTimeConstantExpr target ) {
487
+ target = matchesCall .getArgument ( 0 )
488
+ }
489
+
490
+ /**
491
+ * Holds if `matchesCall` confirms that `checkedExpr` does not contain any directory characters
492
+ * on the given `branch`.
493
+ */
494
+ private predicate isMatchesCall ( StringMatchesCall matchesCall , Expr checkedExpr , boolean branch ) {
495
+ exists ( CompileTimeConstantExpr target , string targetValue |
496
+ isMatchesTarget ( matchesCall , target ) and
497
+ target .getStringValue ( ) = targetValue and
498
+ checkedExpr = matchesCall .getQualifier ( )
499
+ |
500
+ (
501
+ // Allow anything except `.`, '/', '\'
502
+ targetValue .matches ( [ "[%]*" , "[%]+" , "[%]{%}" ] ) and
503
+ (
504
+ // Note: we do not account for when '.', '/', '\' are inside a character range
505
+ not targetValue .matches ( "[%" + [ "." , "/" , "\\\\\\\\" ] + "%]%" ) and
506
+ not targetValue .matches ( "%[^%]%" )
507
+ or
508
+ targetValue .matches ( "[^%.%]%" ) and
509
+ targetValue .matches ( "[^%/%]%" ) and
510
+ targetValue .matches ( "[^%\\\\\\\\%]%" )
511
+ ) and
512
+ branch = true
513
+ or
514
+ // Disallow `.`, '/', '\'
515
+ targetValue .matches ( [ ".*[%].*" , ".+[%].+" ] ) and
516
+ targetValue .matches ( "%[%.%]%" ) and
517
+ targetValue .matches ( "%[%/%]%" ) and
518
+ targetValue .matches ( "%[%\\\\\\\\%]%" ) and
519
+ not targetValue .matches ( "%[^%]%" ) and
520
+ branch = false
521
+ )
522
+ )
523
+ }
524
+
525
+ /**
526
+ * A guard that protects against path traversal by looking for patterns
527
+ * that exclude directory characters: `..`, '/', and '\'.
528
+ */
529
+ private class DirectoryCharactersGuard extends PathGuard {
530
+ Expr checkedExpr ;
531
+ boolean branch ;
532
+
533
+ DirectoryCharactersGuard ( ) { isMatchesCall ( this , checkedExpr , branch ) }
534
+
535
+ override Expr getCheckedExpr ( ) { result = checkedExpr }
536
+
537
+ boolean getBranch ( ) { result = branch }
538
+ }
539
+
540
+ /**
541
+ * Holds if `g` is a guard that considers a path safe because it is checked to make
542
+ * sure it does not contain any directory characters: '..', '/', and '\'.
543
+ */
544
+ private predicate directoryCharactersGuard ( Guard g , Expr e , boolean branch ) {
545
+ branch = g .( DirectoryCharactersGuard ) .getBranch ( ) and
546
+ localTaintFlowToPathGuard ( e , g )
547
+ }
548
+
549
+ /**
550
+ * A sanitizer that protects against path injection vulnerabilities
551
+ * by ensuring that the path does not contain any directory characters:
552
+ * '..', '/', and '\'.
553
+ */
554
+ private class DirectoryCharactersSanitizer extends PathInjectionSanitizer {
555
+ DirectoryCharactersSanitizer ( ) {
556
+ this .asExpr ( ) instanceof ReplaceDirectoryCharactersSanitizer or
557
+ this = DataFlow:: BarrierGuard< directoryCharactersGuard / 3 > :: getABarrierNode ( ) or
558
+ this = ValidationMethod< directoryCharactersGuard / 3 > :: getAValidatedNode ( )
559
+ }
560
+ }
0 commit comments