Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ba727af
Java: CWE-378: Temp Directory Hijacking Race Condition Vulnerability
JLLeitschuh Oct 14, 2020
aea0170
Better temp file deletion and file creation tracking
JLLeitschuh Oct 14, 2020
545eb2c
Fix inverted predicate logic and add additional test cases
JLLeitschuh Oct 15, 2020
253c96f
Improve TempDir hijacking detection with Guard
JLLeitschuh Jan 19, 2022
d9d5b67
Improve warning in TempDirHijackingVulnerability
JLLeitschuh Jan 19, 2022
1fc7629
Add documentation and additional test cases
JLLeitschuh Jan 20, 2022
b42ff13
Improve TempDirHijackingVulnerability message
JLLeitschuh Jan 20, 2022
20bd05b
Add predicate handling `isDirectory` case
JLLeitschuh Mar 9, 2022
442ef83
Add deleteOnExit as safe usage
JLLeitschuh Mar 10, 2022
fbecfdd
Start taint hijacking tracking with `java.io.tmpdir`
JLLeitschuh Mar 15, 2022
8a7d64d
Refactor common logic into TempFileLib
JLLeitschuh Mar 15, 2022
884db9e
Refactor more logic to TempFileLib
JLLeitschuh Mar 15, 2022
6f4ed4b
Apply suggestions from code review
JLLeitschuh Mar 16, 2022
03983f1
Refactor TempDirHijacking to show complete path
JLLeitschuh Mar 16, 2022
37b1e1d
Update to use new getSystemProperty predicate
JLLeitschuh Mar 18, 2022
ac8e1cc
Add additional test cases
JLLeitschuh Mar 18, 2022
84003c1
Fix some false positive paths with FlowState
JLLeitschuh Mar 18, 2022
4b6d1a4
Finalize and document FlowState usage
JLLeitschuh Mar 18, 2022
325d0e1
Add `NullLiteral` flow check for `File.createTempFile`
JLLeitschuh Mar 18, 2022
71f5fc5
Add additional tests and better tracking of 'unsafe use'
JLLeitschuh Mar 29, 2022
140c66e
Add additional tests cases for mkdir wrapper method checking
JLLeitschuh Mar 30, 2022
21bef99
Add release notes
JLLeitschuh Mar 30, 2022
407dd05
Rename localExprFlowPlusInitializers to localExprOrInitializerFlow
JLLeitschuh Mar 30, 2022
0f5a1e7
Expand isDeleteFileExpr to include delete method wrappers
JLLeitschuh Mar 31, 2022
e7f016e
Apply suggestions from code review
JLLeitschuh Apr 25, 2022
3a50253
Fix implicit 'this' use in TempFileLib
JLLeitschuh Apr 28, 2022
cd3662c
Cleanup after rebase on `main`
JLLeitschuh May 3, 2022
a2a7c73
Clean up function naming, documentation, and to some degree code with…
smowton May 9, 2022
b412c7f
Merge pull request #8 from smowton/feat/JLL/java/CWE-378
JLLeitschuh Jun 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add additional tests and better tracking of 'unsafe use'
  • Loading branch information
JLLeitschuh committed May 3, 2022
commit 71f5fc550c2a80e73656fa467c56a649489302d2
183 changes: 161 additions & 22 deletions java/ql/src/Security/CWE/CWE-378/TempDirHijackingVulnerability.ql
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,93 @@ private class FromDeleteFileFlowState extends DataFlow::FlowState {
FromDeleteFileFlowState() { this = "FromDeleteFileFlowState" }
}

/**
* The flow state after the `File::mkdir(s)` method has been called on tracked taint.
* This is the final state, and is only used when computing the 'unsafe' call site.
*/
private class FromMkdirsFileFlowState extends DataFlow::FlowState {
FromMkdirsFileFlowState() { this = "FromMkdirsFileFlowState" }
}
Comment on lines +27 to +59
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️ Flow States! These are awesome!


/**
* A guard that is checking if a directory exists, throwing if it does not exist.
*/
private Guard directoryExistsGuard(ThrowStmt s) {
result =
any(MethodAccess existanceCheck |
(
existanceCheck.getMethod() instanceof MethodFileExists
or
existanceCheck.getMethod() instanceof MethodFileIsDirectory
)
) and
result.directlyControls(s.getEnclosingStmt(), false)
}

/**
* A guard that is verifying that the directory is sucsessfully created, throwing when it is not created.
*/
private Guard directoryCreationGuard(ThrowStmt s) {
result = any(MethodAccess creationCheck |
creationCheck.getMethod() instanceof MethodFileCreatesDirs
) and
result.directlyControls(s.getEnclosingStmt(), false)
}

/**
* A guard that is safely verfying that a directory is created.
*
* 'Safe' means that the directory is guarnteed to have been created, and the directory did not already exist.
*/
private Guard safeDirectoryCreationGuard(ThrowStmt s) {
result = directoryCreationGuard(s) and
// This guard is not 'safe' if a `directoryExistsGuard` is also guarding this throw statement
not exists(directoryExistsGuard(s))
}
Comment on lines +80 to +100
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few additional cases that I want to test this predicate for, in particular, something like:

void createIfNotPresent(File file) throws IOException {
    if (file.exists()) return;
    if (!file.mkdirs()) throw new IOException("Failed to make directory");
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. This predicate does cover that case correctly.


/**
* An expression that will create a directory without throwing an exception if a file/directory already exists.
*/
private predicate isNonThrowingDirectoryCreationExpression(Expr qualifier, MethodAccess creationCall) {
private predicate isNonThrowingDirectoryCreationExpression(
Expr fileInstanceExpr, MethodAccess creationCall
) {
creationCall.getMethod() instanceof MethodFileCreatesDirs and
creationCall.getQualifier() = qualifier
creationCall.getQualifier() = fileInstanceExpr and
// Check for 'throwing creations' and ensure that this call is not used in a throwing case.
if creationCall.(Guard).directlyControls(_, _) then // The creation call is used in a guard
not creationCall = safeDirectoryCreationGuard(_) // Ensure that the guard is insufficient.
else any() // The creation call is not used in a guard. Thus is not a 'throwing creation'.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if there's a better way to write this. Suggestions welcome.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

creationCall.(Guard).directlyControls(_, _) implies not creationCall = safeDirectoryCreationGuard(_)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You know, I was trying to figure out how to use implies but I still have difficult wrapping my head around it. I'll give this a shot, thanks.

or
// Recursively look for methods that encapsulate the above.
// Thus, the use of 'helper directory creation methods' are still considered
// when assessing if an `unsafeUse` is present or not.
exists(Method m, int argumentIndex, Expr arg |
arg.(VarAccess).getVariable() = m.getParameter(argumentIndex) and
isNonThrowingDirectoryCreationExpression(any(Expr e2 | DataFlow::localExprFlow(arg, e2)),
creationCall)
|
m.getAReference().getArgument(argumentIndex) = fileInstanceExpr
)
}
Comment thread
JLLeitschuh marked this conversation as resolved.

private class MethodFileDelete extends Method {
MethodFileDelete() {
getDeclaringType() instanceof TypeFile and
hasName("delete")
this.getDeclaringType() instanceof TypeFile and
this.hasName("delete")
}
}

private class MethodFileIsDirectory extends Method {
MethodFileIsDirectory() {
this.getDeclaringType() instanceof TypeFile and
this.hasName("isDirectory")
}
}

private class MethodFileExists extends Method {
MethodFileExists() {
this.getDeclaringType() instanceof TypeFile and
this.hasName("exists")
}
}

Expand Down Expand Up @@ -143,7 +218,13 @@ private class TempDirHijackingFromDirectoryCreateToUnsafeUseConfig extends DataF
}

private class DirectoryCreationBarrierGuard extends DataFlow::BarrierGuard {
DirectoryCreationBarrierGuard() { isNonThrowingDirectoryCreationExpression(_, this) }
Expr fileInstanceExpr;

DirectoryCreationBarrierGuard() {
isNonThrowingDirectoryCreationExpression(fileInstanceExpr, this)
}

Expr getFileInstanceExpr() { result = fileInstanceExpr }

override predicate checks(Expr e, boolean branch) { this.controls(e, branch) }
}
Expand All @@ -153,13 +234,18 @@ private class DirectoryCreationBarrierGuard extends DataFlow::BarrierGuard {
* is not guarded by a check of the return value of `File::mkdir(s)`.
*/
predicate isUnsafeUseUnconstrainedByIfCheck(DataFlow::Node sink, Expr unsafeUse) {
exists(Guard g, MethodAccess ma |
exists(DirectoryCreationBarrierGuard g, MethodAccess ma |
any(TempDirHijackingFromDeleteConfig c).isSink(sink) and // Sink is a call to `mkdir` or `mkdirs`
sink.asExpr() = ma.getQualifier() and // The method access is on the same object as the sink
g = ma and // The guard is the method access
any(TempDirHijackingFromDirectoryCreateToUnsafeUseConfig c)
.hasFlow(sink, DataFlow::exprNode(unsafeUse)) and // There is some flow from the sink to an unsafe use of the File
( // TODO: Consider replacing this entire bit of logic with `TempDirHijackingFullPathInlcudingUnsafeUse`.
any(TempDirHijackingFromDirectoryCreateToUnsafeUseConfig c)
.hasFlow(sink, DataFlow::exprNode(unsafeUse)) // There is some flow from the sink to an unsafe use of the File
or
DataFlow::localExprFlow(g.getFileInstanceExpr(), unsafeUse) // There is some local flow from the passed file instance to an unsafe use
) and
unsafeUse != sink.asExpr() and // The unsafe use is not the sink itself
not g.getFileInstanceExpr() = unsafeUse and // The unsafe use is not the file instance
not safeUse(unsafeUse) and // The unsafe use is not a safe use
not g.controls(unsafeUse.getBasicBlock(), true) and
not booleanVarAccessGuardsGuard(g) // The guard is not guarded by a boolean variable access guard
Expand Down Expand Up @@ -242,29 +328,39 @@ private predicate safeUse(Expr e) {
not DataFlow::localExprFlow(getStringAccessOnFile(e), any(Expr sink | not safeUse(sink)))
}

private predicate isSourceUnified(DataFlow::Node source, DataFlow::FlowState state) {
exists(DirHijackingTaintSource taintSource |
source = taintSource and state = taintSource.getFlowState()
)
}

private predicate isAdditionalTaintStepUnified(
DataFlow::Node node1, DataFlow::FlowState state1, DataFlow::Node node2, DataFlow::FlowState state2
) {
// From a temporary directory system property access to `File.createTempFile(_, _, flow)` call
isTaintStepFileCreateTempFile(node1, node2) and
state1 instanceof FromSystemPropertyFlowState and
state2 instanceof FromFileCreateTempFileFlowState
or
// From `File.createTempFile(_, _, flow)` to a call to `File::delete`
node1 = node2 and
isDeleteFileExpr(node1.asExpr()) and
state1 instanceof FromFileCreateTempFileFlowState and
state2 instanceof FromDeleteFileFlowState
}

private class TempDirHijackingFullPath extends TaintTracking::Configuration {
TempDirHijackingFullPath() { this = "TempDirHijackingFullPath" }

override predicate isSource(DataFlow::Node source, DataFlow::FlowState state) {
exists(DirHijackingTaintSource taintSource |
source = taintSource and state = taintSource.getFlowState()
)
isSourceUnified(source, state)
}

override predicate isAdditionalTaintStep(
DataFlow::Node node1, DataFlow::FlowState state1, DataFlow::Node node2,
DataFlow::FlowState state2
) {
// From a temporary directory system property access to `File.createTempFile(_, _, flow)` call
isTaintStepFileCreateTempFile(node1, node2) and
state1 instanceof FromSystemPropertyFlowState and
state2 instanceof FromFileCreateTempFileFlowState
or
// From `File.createTempFile(_, _, flow)` to a call to `File::delete`
node1 = node2 and
isDeleteFileExpr(node1.asExpr()) and
state1 instanceof FromFileCreateTempFileFlowState and
state2 instanceof FromDeleteFileFlowState
isAdditionalTaintStepUnified(node1, state1, node2, state2)
}

override predicate isSink(DataFlow::Node sink, DataFlow::FlowState state) {
Expand All @@ -274,14 +370,57 @@ private class TempDirHijackingFullPath extends TaintTracking::Configuration {
}
}

/**
* A configuration that tracks the full path of the temporary directory taint all the way to an
* `unsafeUse` of the potentially hijacked temporary directory.
*
* This is intentionally not used in the the generation of the displayed path;
* this is because there may not exist an explicit path from the call to `File::mkdir(s)` call to the the `unsafeUse`.
*/
class TempDirHijackingFullPathInlcudingUnsafeUse extends TaintTracking2::Configuration {
TempDirHijackingFullPathInlcudingUnsafeUse() {
this = "TempDirHijackingFullPathInlcudingUnsafeUse"
}

override predicate isSource(DataFlow::Node source, DataFlow::FlowState state) {
isSourceUnified(source, state)
}

override predicate isAdditionalTaintStep(
DataFlow::Node node1, DataFlow::FlowState state1, DataFlow::Node node2,
DataFlow::FlowState state2
) {
isAdditionalTaintStepUnified(node1, state1, node2, state2)
or // `File::mkdir(s)` is not an end-state when looking for an unsafe use
isNonThrowingDirectoryCreationExpression(node1.asExpr(), _) and
node1.asExpr() = node2.asExpr() and
Comment thread
JLLeitschuh marked this conversation as resolved.
Outdated
state1 instanceof FromDeleteFileFlowState and
state2 instanceof FromMkdirsFileFlowState
}

override predicate isSink(DataFlow::Node sink, DataFlow::FlowState state) {
// From a `File::delete` call, to a call to `File::mkdir(s)`
isUnsafeUseUnconstrainedByIfCheck(_, sink.asExpr()) and
state instanceof FromMkdirsFileFlowState
}
}

from
DataFlow::PathNode pathSource, DataFlow::PathNode pathSink, DataFlow::Node deleteCheckpoint,
MethodAccess creationCall, Expr unsafe
where
exists(TempDirHijackingFullPath c | c.hasFlowPath(pathSource, pathSink)) and
// Generate the full path to display to the user
any(TempDirHijackingFullPath c).hasFlowPath(pathSource, pathSink) and
// Find the delete checkpoint to display to the user
any(TempDirHijackingToDeleteConfig c).hasFlow(pathSource.getNode(), deleteCheckpoint) and
// Find the delete checkpoint to display to the user
any(TempDirHijackingFromDeleteConfig c).hasFlow(deleteCheckpoint, pathSink.getNode()) and
// Find a full path where an `unsafe` use is present
any(TempDirHijackingFullPathInlcudingUnsafeUse c)
.hasFlow(pathSource.getNode(), DataFlow::exprNode(unsafe)) and
Comment on lines +412 to +493
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it fine that I'm using this many taint tracking and data flow modules in one query? I'm concerned about the performance impact of this potentially.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing inherently terrible about it. We'll do perf analysis prior to committing anything to check.

// Verify the unsafe use is not constrained by an if check
isUnsafeUseUnconstrainedByIfCheck(pathSink.getNode(), unsafe) and
// Verfy the creation call expression is the expected sink
isNonThrowingDirectoryCreationExpression(pathSink.getNode().asExpr(), creationCall)
select pathSink, pathSource, pathSink,
"Local temporary directory hijacking race condition between $@ and this directory creation call. As such, the directory usage $@ may have been hijacked by another local user.",
Expand Down
Loading