@@ -111,7 +111,9 @@ private predicate dotDotCheckGuard(Guard g, Expr e, boolean branch) {
111111 exists ( Guard previousGuard |
112112 previousGuard .( AllowedPrefixGuard ) .controls ( g .getBasicBlock ( ) , true )
113113 or
114- previousGuard .( BlockListGuard ) .controls ( g .getBasicBlock ( ) , false )
114+ previousGuard
115+ .( BlockListGuard )
116+ .controls ( g .getBasicBlock ( ) , previousGuard .( BlockListGuard ) .getBranch ( ) )
115117 )
116118 or
117119 // `File.getName` strips any directory prefix, returning only the final path
@@ -128,13 +130,31 @@ private class DotDotCheckSanitizer extends PathInjectionSanitizer {
128130 DotDotCheckSanitizer ( ) { this = DataFlow:: BarrierGuard< dotDotCheckGuard / 3 > :: getABarrierNode ( ) }
129131}
130132
131- private class BlockListGuard extends PathGuard instanceof MethodCall {
133+ private class BlockListGuard extends PathGuard {
134+ Expr checkedExpr ;
135+ boolean branch ;
136+
132137 BlockListGuard ( ) {
133- ( isStringPartialMatch ( this ) or isPathPrefixMatch ( this ) ) and
134- isDisallowedWord ( super .getAnArgument ( ) )
138+ exists ( StringSubstringMatch match , boolean matchingBranch |
139+ this = match and
140+ checkedExpr = match .getInputString ( ) and
141+ isDisallowedWord ( match .getMatchExpression ( ) ) and
142+ match .hasSubstringOnBranch ( matchingBranch ) and
143+ branch = matchingBranch .booleanNot ( )
144+ )
145+ or
146+ exists ( MethodCall ma |
147+ this = ma and
148+ ( isStringPartialMatch ( ma ) or isPathPrefixMatch ( ma ) ) and
149+ isDisallowedWord ( ma .getAnArgument ( ) ) and
150+ checkedExpr = getVisualQualifier ( ma ) .getUnderlyingExpr ( ) and
151+ branch = false
152+ )
135153 }
136154
137- override Expr getCheckedExpr ( ) { result = getVisualQualifier ( this ) .getUnderlyingExpr ( ) }
155+ override Expr getCheckedExpr ( ) { result = checkedExpr }
156+
157+ boolean getBranch ( ) { result = branch }
138158}
139159
140160/**
@@ -143,7 +163,7 @@ private class BlockListGuard extends PathGuard instanceof MethodCall {
143163 * or a sanitizer (`PathNormalizeSanitizer`), to ensure any internal `..` components are removed from the path.
144164 */
145165private predicate blockListGuard ( Guard g , Expr e , boolean branch ) {
146- branch = false and
166+ branch = g . ( BlockListGuard ) . getBranch ( ) and
147167 // Local taint-flow is used here to handle cases where the validated expression comes from the
148168 // expression reaching the sink, but it's not the same one, e.g.:
149169 // File file = source();
@@ -202,8 +222,10 @@ private predicate isStringPartialMatch(MethodCall ma) {
202222 exists ( RefType t | t = ma .getMethod ( ) .getDeclaringType ( ) |
203223 t instanceof TypeString or t instanceof StringsKt
204224 ) and
205- getSourceMethod ( ma .getMethod ( ) )
206- .hasName ( [ "contains" , "matches" , "regionMatches" , "indexOf" , "lastIndexOf" ] )
225+ getSourceMethod ( ma .getMethod ( ) ) .hasName ( [ "matches" , "regionMatches" , "indexOf" , "lastIndexOf" ] )
226+ or
227+ ma .getMethod ( ) .getDeclaringType ( ) instanceof StringsKt and
228+ getSourceMethod ( ma .getMethod ( ) ) .hasName ( "contains" )
207229}
208230
209231/**
@@ -223,8 +245,17 @@ private predicate isDisallowedWord(ConstantOrRegex word) {
223245/** A complementary guard that protects against path traversal, by looking for the literal `..`. */
224246private class PathTraversalGuard extends PathGuard {
225247 Expr checkedExpr ;
248+ boolean branch ;
226249
227250 PathTraversalGuard ( ) {
251+ exists ( StringSubstringMatch match , boolean matchingBranch |
252+ this = match and
253+ checkedExpr = match .getInputString ( ) and
254+ match .getMatchExpression ( ) .( CompileTimeConstantExpr ) .getStringValue ( ) = ".." and
255+ match .hasSubstringOnBranch ( matchingBranch ) and
256+ branch = matchingBranch .booleanNot ( )
257+ )
258+ or
228259 exists ( MethodCall ma , Method m , RefType t |
229260 m = ma .getMethod ( ) and
230261 t = m .getDeclaringType ( ) and
@@ -233,24 +264,23 @@ private class PathTraversalGuard extends PathGuard {
233264 ma .getAnArgument ( ) .( CompileTimeConstantExpr ) .getStringValue ( ) = ".."
234265 |
235266 this = ma and
236- getSourceMethod ( m ) .hasName ( "contains" )
267+ t instanceof StringsKt and
268+ getSourceMethod ( m ) .hasName ( "contains" ) and
269+ branch = false
237270 or
238271 exists ( EqualityTest eq |
239272 this = eq and
240273 getSourceMethod ( m ) .hasName ( [ "indexOf" , "lastIndexOf" ] ) and
241274 eq .getAnOperand ( ) = ma and
242- eq .getAnOperand ( ) .( CompileTimeConstantExpr ) .getIntValue ( ) = - 1
275+ eq .getAnOperand ( ) .( CompileTimeConstantExpr ) .getIntValue ( ) = - 1 and
276+ branch = eq .polarity ( )
243277 )
244278 )
245279 }
246280
247281 override Expr getCheckedExpr ( ) { result = checkedExpr }
248282
249- boolean getBranch ( ) {
250- this instanceof MethodCall and result = false
251- or
252- result = this .( EqualityTest ) .polarity ( )
253- }
283+ boolean getBranch ( ) { result = branch }
254284}
255285
256286/** A complementary sanitizer that protects against path traversal using path normalization. */
@@ -347,20 +377,32 @@ private class FileConstructorChildArgumentStep extends AdditionalTaintStep {
347377/** Gets a character used for replacement. */
348378private string getAReplacementChar ( ) { result = [ "" , "_" , "-" ] }
349379
380+ /** Holds if `replacement` is a safe replacement value. */
381+ private predicate isSafeReplacement ( Expr replacement ) {
382+ replacement .( CompileTimeConstantExpr ) .getStringOrCharValue ( ) = getAReplacementChar ( )
383+ }
384+
350385/** Gets a directory character represented as regex. */
351386private string getADirRegexChar ( ) { result = [ "\\." , "/" , "\\\\" ] }
352387
353388/** Gets a directory character represented as a char. */
354389private string getADirChar ( ) { result = [ "." , "/" , "\\" ] }
355390
391+ /** Holds if `value` contains every directory character. */
392+ bindingset [ value]
393+ private predicate containsAllDirectoryCharacters ( string value ) {
394+ forall ( string directoryCharacter | directoryCharacter = getADirChar ( ) |
395+ exists ( value .indexOf ( directoryCharacter ) )
396+ )
397+ }
398+
356399/** Holds if `replacement` replaces all directory characters. */
357400private predicate replacesDirectoryCharactersWithSingleReplaceAll ( StringReplacement replacement ) {
358401 exists ( CompileTimeConstantExpr target , string targetValue |
359402 replacement .matchExpressionIsRegex ( ) and
360403 target = replacement .getMatchExpression ( ) and
361404 target .getStringValue ( ) = targetValue and
362- replacement .getReplacement ( ) .( CompileTimeConstantExpr ) .getStringOrCharValue ( ) =
363- getAReplacementChar ( )
405+ isSafeReplacement ( replacement .getReplacement ( ) )
364406 |
365407 not targetValue .matches ( "%[^%]%" ) and
366408 targetValue .matches ( "[%.%]" ) and
@@ -375,6 +417,19 @@ private predicate replacesDirectoryCharactersWithSingleReplaceAll(StringReplacem
375417 )
376418}
377419
420+ /**
421+ * Holds if a character-set replacement replaces all directory characters.
422+ */
423+ private predicate replacesDirectoryCharactersWithCharacterSet ( StringReplacement replacement ) {
424+ exists ( CompileTimeConstantExpr target , string targetValue |
425+ replacement .matchExpressionIsCharacterSet ( ) and
426+ target = replacement .getMatchExpression ( ) and
427+ target .getStringValue ( ) = targetValue and
428+ containsAllDirectoryCharacters ( targetValue ) and
429+ isSafeReplacement ( replacement .getReplacement ( ) )
430+ )
431+ }
432+
378433/**
379434 * Holds if there are two chained replacement calls, `rc1` and `rc2`, that replace
380435 * '.' and one of '/' or '\'.
@@ -403,8 +458,8 @@ private predicate replacesDirectoryCharactersWithDoubleReplaceOrReplaceAll(Strin
403458 rc2 .getInputString ( ) = rc1 and
404459 target1 .getStringOrCharValue ( ) = targetValue1 and
405460 target2 .getStringOrCharValue ( ) = targetValue2 and
406- rc1 .getReplacement ( ) . ( CompileTimeConstantExpr ) . getStringOrCharValue ( ) = getAReplacementChar ( ) and
407- rc2 .getReplacement ( ) . ( CompileTimeConstantExpr ) . getStringOrCharValue ( ) = getAReplacementChar ( ) and
461+ isSafeReplacement ( rc1 .getReplacement ( ) ) and
462+ isSafeReplacement ( rc2 .getReplacement ( ) ) and
408463 // make sure the calls replace different characters
409464 targetValue2 != targetValue1 and
410465 // make sure one of the calls replaces '.'
@@ -420,10 +475,26 @@ private predicate replacesDirectoryCharactersWithDoubleReplaceOrReplaceAll(Strin
420475private class ReplaceDirectoryCharactersSanitizer extends StringReplacement {
421476 ReplaceDirectoryCharactersSanitizer ( ) {
422477 replacesDirectoryCharactersWithSingleReplaceAll ( this ) or
478+ replacesDirectoryCharactersWithCharacterSet ( this ) or
423479 replacesDirectoryCharactersWithDoubleReplaceOrReplaceAll ( this )
424480 }
425481}
426482
483+ /**
484+ * A sanitizer that protects against path injection vulnerabilities by removing
485+ * every directory character.
486+ */
487+ private class RemoveDirectoryCharactersSanitizer extends StringRemoval {
488+ RemoveDirectoryCharactersSanitizer ( ) {
489+ exists ( CompileTimeConstantExpr target , string targetValue |
490+ this .matchExpressionIsCharacterSet ( ) and
491+ target = this .getMatchExpression ( ) and
492+ target .getStringValue ( ) = targetValue and
493+ containsAllDirectoryCharacters ( targetValue )
494+ )
495+ }
496+ }
497+
427498/**
428499 * Holds if `matchesCall` confirms that `checkedExpr` does not contain any directory characters
429500 * on the given `branch`.
@@ -459,6 +530,23 @@ private predicate isMatchesCall(RegexMatch regexMatch, Expr checkedExpr, boolean
459530 )
460531}
461532
533+ /**
534+ * Holds if `match` confirms that `checkedExpr` does not contain any directory
535+ * characters on `branch`.
536+ */
537+ private predicate isCharacterSetMatch (
538+ StringCharacterSetMatch match , Expr checkedExpr , boolean branch
539+ ) {
540+ exists ( CompileTimeConstantExpr target , string targetValue , boolean matchingBranch |
541+ target = match .getMatchExpression ( ) and
542+ target .getStringValue ( ) = targetValue and
543+ containsAllDirectoryCharacters ( targetValue ) and
544+ checkedExpr = match .getInputString ( ) and
545+ match .hasMatchingCharacterOnBranch ( matchingBranch ) and
546+ branch = matchingBranch .booleanNot ( )
547+ )
548+ }
549+
462550/**
463551 * A guard that protects against path traversal by looking for patterns
464552 * that exclude directory characters: `..`, '/', and '\'.
@@ -467,7 +555,10 @@ private class DirectoryCharactersGuard extends PathGuard {
467555 Expr checkedExpr ;
468556 boolean branch ;
469557
470- DirectoryCharactersGuard ( ) { isMatchesCall ( this , checkedExpr , branch ) }
558+ DirectoryCharactersGuard ( ) {
559+ isMatchesCall ( this , checkedExpr , branch ) or
560+ isCharacterSetMatch ( this , checkedExpr , branch )
561+ }
471562
472563 override Expr getCheckedExpr ( ) { result = checkedExpr }
473564
@@ -492,6 +583,8 @@ private class DirectoryCharactersSanitizer extends PathInjectionSanitizer {
492583 DirectoryCharactersSanitizer ( ) {
493584 this .asExpr ( ) instanceof ReplaceDirectoryCharactersSanitizer
494585 or
586+ this .asExpr ( ) instanceof RemoveDirectoryCharactersSanitizer
587+ or
495588 this = DataFlow:: BarrierGuard< directoryCharactersGuard / 3 > :: getABarrierNode ( )
496589 }
497590}
0 commit comments