@@ -384,30 +384,135 @@ private class FileConstructorChildArgumentStep extends AdditionalTaintStep {
384
384
}
385
385
}
386
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 isSingleReplaceAll ( StringReplaceAllCall replaceAllCall ) {
418
+ exists ( CompileTimeConstantExpr target , string targetValue |
419
+ isReplaceAllTarget ( replaceAllCall , target ) and
420
+ target .getStringValue ( ) = targetValue
421
+ |
422
+ not targetValue .matches ( "%[^%]%" ) and
423
+ targetValue .matches ( "[%.%]" ) and
424
+ targetValue .matches ( "[%/%]" ) and
425
+ // Search for "\\\\" (needs extra backslashes to avoid escaping the '%')
426
+ targetValue .matches ( "[%\\\\\\\\%]" )
427
+ or
428
+ targetValue .matches ( "%|%" ) and
429
+ targetValue .matches ( "%" + [ "\\.\\." , "[.][.]" , "\\." ] + "%" ) and
430
+ targetValue .matches ( "%/%" ) and
431
+ targetValue .matches ( "%\\\\\\\\%" )
432
+ )
433
+ }
434
+
435
+ /**
436
+ * Holds if there are two chained replacement calls, `rc1` and `rc2`, that replace
437
+ * '.' and one of '/' or '\'.
438
+ */
439
+ private predicate isDoubleReplaceOrReplaceAll ( StringReplaceOrReplaceAllCall rc1 ) {
440
+ exists (
441
+ CompileTimeConstantExpr target1 , string targetValue1 , StringReplaceOrReplaceAllCall rc2 ,
442
+ CompileTimeConstantExpr target2 , string targetValue2
443
+ |
444
+ rc1 instanceof StringReplaceAllCall and
445
+ isReplaceAllTarget ( rc1 , target1 ) and
446
+ isReplaceAllTarget ( rc2 , target2 ) and
447
+ targetValue1 = getADirRegexChar ( ) and
448
+ targetValue2 = getADirRegexChar ( )
449
+ or
450
+ rc1 instanceof StringReplaceCall and
451
+ isReplaceTarget ( rc1 , target1 ) and
452
+ isReplaceTarget ( rc2 , target2 ) and
453
+ targetValue1 = getADirChar ( ) and
454
+ targetValue2 = getADirChar ( )
455
+ |
456
+ rc2 .getQualifier ( ) = rc1 and
457
+ target1 .getStringValue ( ) = targetValue1 and
458
+ target2 .getStringValue ( ) = targetValue2 and
459
+ rc2 .getArgument ( 1 ) .( CompileTimeConstantExpr ) .getStringValue ( ) = getAReplacementChar ( ) and
460
+ // make sure the calls replace different characters
461
+ targetValue2 != targetValue1 and
462
+ // make sure one of the calls replaces '.'
463
+ // then the other call must replace one of '/' or '\' if they are not equal
464
+ ( targetValue2 .matches ( "%.%" ) or targetValue1 .matches ( "%.%" ) )
465
+ )
466
+ }
467
+
387
468
/**
388
469
* A complementary sanitizer that protects against path injection vulnerabilities
389
- * by replacing all directory characters ('..', '/', and '\') with safe characters.
470
+ * by replacing directory characters ('..', '/', and '\') with safe characters.
390
471
*/
391
- private class ReplaceDirectoryCharactersSanitizer extends MethodCall {
472
+ private class ReplaceDirectoryCharactersSanitizer extends StringReplaceOrReplaceAllCall {
392
473
ReplaceDirectoryCharactersSanitizer ( ) {
393
- exists ( MethodCall mc |
394
- // TODO: "java.lang.String.replace" as well
395
- mc .getMethod ( ) .hasQualifiedName ( "java.lang" , "String" , "replaceAll" ) and
396
- // TODO: unhardcode all of the below to handle more valid replacements and several calls
474
+ isSingleReplaceAll ( this ) or
475
+ isDoubleReplaceOrReplaceAll ( this )
476
+ }
477
+ }
478
+
479
+ /** Holds if `target` is the first argument of `matchesCall`. */
480
+ private predicate isMatchesTarget ( StringMatchesCall matchesCall , CompileTimeConstantExpr target ) {
481
+ target = matchesCall .getArgument ( 0 )
482
+ }
483
+
484
+ /**
485
+ * Holds if `matchesCall` confirms that `checkedExpr` does not contain any directory characters
486
+ * on the given `branch`.
487
+ */
488
+ private predicate isMatchesCall ( StringMatchesCall matchesCall , Expr checkedExpr , boolean branch ) {
489
+ exists ( CompileTimeConstantExpr target , string targetValue |
490
+ isMatchesTarget ( matchesCall , target ) and
491
+ target .getStringValue ( ) = targetValue and
492
+ checkedExpr = matchesCall .getQualifier ( )
493
+ |
494
+ targetValue .matches ( [ "[%]*" , "[%]+" , "[%]{%}" ] ) and
495
+ (
496
+ // Allow anything except `.`, '/', '\'
397
497
(
398
- mc .getArgument ( 0 ) .( CompileTimeConstantExpr ) .getStringValue ( ) = "\\.\\.|[/\\\\]"
498
+ // Note: we do not account for when '.', '/', '\' are inside a character range
499
+ not targetValue .matches ( "[%" + [ "." , "/" , "\\\\\\\\" ] + "%]%" ) and
500
+ not targetValue .matches ( "%[^%]%" )
399
501
or
400
- exists ( MethodCall mc2 |
401
- mc2 .getMethod ( ) .hasQualifiedName ( "java.lang" , "String" , "replaceAll" ) and
402
- mc .getArgument ( 0 ) .( CompileTimeConstantExpr ) .getStringValue ( ) = "\\." and
403
- mc2 .getArgument ( 0 ) .( CompileTimeConstantExpr ) .getStringValue ( ) = "/"
404
- )
502
+ targetValue .matches ( "[^%.%]%" ) and
503
+ targetValue .matches ( "[^%/%]%" ) and
504
+ targetValue .matches ( "[^%\\\\\\\\%]%" )
405
505
) and
406
- // TODO: accept more replacement characters?
407
- mc .getArgument ( 1 ) .( CompileTimeConstantExpr ) .getStringValue ( ) = [ "" , "_" ] and
408
- this = mc
506
+ branch = true
507
+ or
508
+ // Disallow `.`, '/', '\'
509
+ targetValue .matches ( "[%.%]%" ) and
510
+ targetValue .matches ( "[%/%]%" ) and
511
+ targetValue .matches ( "[%\\\\\\\\%]%" ) and
512
+ not targetValue .matches ( "%[^%]%" ) and
513
+ branch = false
409
514
)
410
- }
515
+ )
411
516
}
412
517
413
518
/**
@@ -416,28 +521,21 @@ private class ReplaceDirectoryCharactersSanitizer extends MethodCall {
416
521
*/
417
522
private class DirectoryCharactersGuard extends PathGuard {
418
523
Expr checkedExpr ;
524
+ boolean branch ;
419
525
420
- DirectoryCharactersGuard ( ) {
421
- exists ( MethodCall mc , Method m | m = mc .getMethod ( ) |
422
- m .getDeclaringType ( ) instanceof TypeString and
423
- m .hasName ( "matches" ) and
424
- // TODO: unhardcode to handle more valid matches
425
- mc .getAnArgument ( ) .( CompileTimeConstantExpr ) .getStringValue ( ) = "[0-9a-fA-F]{20,}" and
426
- checkedExpr = mc .getQualifier ( ) and
427
- this = mc
428
- )
429
- }
526
+ DirectoryCharactersGuard ( ) { isMatchesCall ( this , checkedExpr , branch ) }
430
527
431
528
override Expr getCheckedExpr ( ) { result = checkedExpr }
529
+
530
+ boolean getBranch ( ) { result = branch }
432
531
}
433
532
434
533
/**
435
534
* Holds if `g` is a guard that considers a path safe because it is checked to make
436
535
* sure it does not contain any directory characters: '..', '/', and '\'.
437
536
*/
438
537
private predicate directoryCharactersGuard ( Guard g , Expr e , boolean branch ) {
439
- branch = true and
440
- g instanceof DirectoryCharactersGuard and
538
+ branch = g .( DirectoryCharactersGuard ) .getBranch ( ) and
441
539
localTaintFlowToPathGuard ( e , g )
442
540
}
443
541
0 commit comments