@@ -384,30 +384,135 @@ private class FileConstructorChildArgumentStep extends AdditionalTaintStep {
384384 }
385385}
386386
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 in 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+
387468/**
388469 * 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.
390471 */
391- private class ReplaceDirectoryCharactersSanitizer extends MethodCall {
472+ private class ReplaceDirectoryCharactersSanitizer extends StringReplaceOrReplaceAllCall {
392473 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 `.`, '/', '\'
397497 (
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 ( "%[^%]%" )
399501 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 ( "[^%\\\\\\\\%]%" )
405505 ) 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
409514 )
410- }
515+ )
411516}
412517
413518/**
@@ -416,28 +521,21 @@ private class ReplaceDirectoryCharactersSanitizer extends MethodCall {
416521 */
417522private class DirectoryCharactersGuard extends PathGuard {
418523 Expr checkedExpr ;
524+ boolean branch ;
419525
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 ) }
430527
431528 override Expr getCheckedExpr ( ) { result = checkedExpr }
529+
530+ boolean getBranch ( ) { result = branch }
432531}
433532
434533/**
435534 * Holds if `g` is a guard that considers a path safe because it is checked to make
436535 * sure it does not contain any directory characters: '..', '/', and '\'.
437536 */
438537private predicate directoryCharactersGuard ( Guard g , Expr e , boolean branch ) {
439- branch = true and
440- g instanceof DirectoryCharactersGuard and
538+ branch = g .( DirectoryCharactersGuard ) .getBranch ( ) and
441539 localTaintFlowToPathGuard ( e , g )
442540}
443541
0 commit comments