Skip to content

Commit 76433a3

Browse files
Jami CogswellJami Cogswell
Jami Cogswell
authored and
Jami Cogswell
committed
Java: generalize sanitizer and add tests
1 parent ab3690f commit 76433a3

File tree

4 files changed

+366
-76
lines changed

4 files changed

+366
-76
lines changed

java/ql/lib/semmle/code/java/JDK.qll

+30
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,36 @@ class StringContainsMethod extends Method {
4545
}
4646
}
4747

48+
/** A call to the `java.lang.String.matches` method. */
49+
class StringMatchesCall extends MethodCall {
50+
StringMatchesCall() {
51+
exists(Method m | m = this.getMethod() |
52+
m.getDeclaringType() instanceof TypeString and
53+
m.hasName("matches")
54+
)
55+
}
56+
}
57+
58+
/** A call to the `java.lang.String.replaceAll` method. */
59+
class StringReplaceAllCall extends MethodCall {
60+
StringReplaceAllCall() {
61+
exists(Method m | m = this.getMethod() |
62+
m.getDeclaringType() instanceof TypeString and
63+
m.hasName("replaceAll")
64+
)
65+
}
66+
}
67+
68+
/** A call to the `java.lang.String.replace` method. */
69+
class StringReplaceCall extends MethodCall {
70+
StringReplaceCall() {
71+
exists(Method m | m = this.getMethod() |
72+
m.getDeclaringType() instanceof TypeString and
73+
m.hasName("replace")
74+
)
75+
}
76+
}
77+
4878
/**
4979
* The methods on the class `java.lang.String` that are used to perform partial matches with a specified substring or char.
5080
*/

java/ql/lib/semmle/code/java/security/PathSanitizer.qll

+126-28
Original file line numberDiff line numberDiff line change
@@ -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 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+
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
*/
417522
private 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
*/
438537
private 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

Comments
 (0)