-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Java: CWE-378: Temp Directory Hijacking Race Condition Vulnerability #4473
base: main
Are you sure you want to change the base?
Conversation
Maybe I should use CWE-379 instead? https://cwe.mitre.org/data/definitions/379.html |
Would it make sense to cover Edit: Though I guess the use case you had in mind was the creation of a temporary directory using |
override predicate isSink(DataFlow::Node sink) { isDeleteFileExpr(sink.asExpr()) } | ||
} | ||
|
||
private class TempDirHijackingFromDeleteConfig extends DataFlow2::Configuration { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: because the two configs are independent (no methods in this class depend on the flows found in TempDirHijackingToDeleteConfig
), this can legally use DataFlow::Configuration
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds like playing with fire? One mistake and I end up with the two configurations artificially restricting each other.
Historically, I've ended up with some really wacky & hard to debug issues when extending DataFlow::Configuration
multiple times.
predicate isSinkConstrainedByIfCheck(DataFlow2::Node sink) { | ||
exists(MethodAccess ma, IfStmt ifStmt | | ||
sink.asExpr() = ma.getQualifier() and | ||
// Data flow from the return value from `mkdirs` into an 'if' check |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note you might get use within an &&
, ||
, == true
and so on, does this work for those cases?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does not, I'd like to expand this so that it uses Guard
. I was chatting with a few people about this in the slack channel.
https://ghsecuritylab.slack.com/archives/CQJU6RN49/p1602785245063800
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be a good example to work from: https://github.com/github/codeql/blob/main/java/ql/src/Security/CWE/CWE-022/TaintedPath.ql#L21
Alternatively you could quite closely approximate by just checking the return value has any use -- it's probably a small minority of users that call mkdir
, store its return value, but then don't properly check it (compared to simply throwing it away altogether)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To follow up on how to use Guard
for this: There is in general not a single recipe for this as there are many ways to mix control flow and data flow in a query.
To cover the linked TaintedPath
example first: That's using a BarrierGuard
, which is actually just a convenience wrapper that defines a number of data-flow nodes as barriers/sanitizers (which means that data flow is prohibited from using those nodes); it does this by selecting all those data-flow nodes that are expressions whose corresponding control-flow nodes are guarded by the condition evaluating to false ("guarded" is a technical term with the definition: C evaluates to false guards N if and only if every control-flow path reaching N must have passed through the false-edge leaving C).
BarrierGuards are nice, but doesn't fit the use-case in this query, as the flow path stops at the mkdir
qualifier. They are however implemented in terms of Guard
, which allows us to reason about such "guarding" relationships between control-flow nodes.
So what we want is to see whether there's a (problematic) use of temp
that isn't guarded by temp.mkdir()
evaluating to true. We can approximate problematic uses by, for instance, all subsequent local uses that are not safe and approximate "safe" by saying that the safe uses are the ones that are used in string concatenations (e.g. for error reporting).
So I'd write this predicate something like this:
predicate isSinkConstrainedByIfCheck(DataFlow::Node sink) {
exists(Guard g, MethodAccess ma, Expr unsafeUse |
any(TempDirHijackingFromDeleteConfig c).isSink(sink) and
sink.asExpr() = ma.getQualifier() and
g = ma and
DataFlow::localExprFlow(sink.asExpr(), unsafeUse) and
unsafeUse != sink.asExpr() and
not safeUse(unsafeUse) and
not g.controls(unsafeUse.getBasicBlock(), true)
)
}
predicate safeUse(Expr e) {
exists(AddExpr concat |
concat.getType() instanceof TypeString and
concat.getAnOperand() = e
)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw. "controls" and "guards" are synonymous verbs in this context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had to make some modifications to your proposal above but this ended up working:
predicate isUnsafeUseUnconstrainedByIfCheck(DataFlow::Node sink, Expr unsafeUse) {
exists(Guard g, MethodAccess ma |
any(TempDirHijackingFromDeleteConfig c).isSink(sink) and // Sink is a call to delete
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
DataFlow::localExprFlow(sink.asExpr(), unsafeUse) and // There is some flow from the sink to an unsafe use of the File
unsafeUse != sink.asExpr() and // The unsafe use is not the sink itself
not safeUse(unsafeUse) and // The unsafe use is not a safe use
not g.controls(unsafeUse.getBasicBlock(), true)
)
}
private predicate safeUse(Expr e) {
exists(AndLogicalExpr andExp |
andExp.getType() instanceof BooleanType and andExp.getAnOperand() = e
)
or
exists(AssignAndExpr assignAndExp |
assignAndExp.getType() instanceof BooleanType and assignAndExp.getSource() = e
)
}
However, this false-positives on the following two:
static File safe4() throws IOException {
boolean success = true;
File temp = File.createTempFile("test", "directory");
success &= temp.delete();
success &= temp.mkdir();
if (!success) {
throw new RuntimeException("Failed to create directory");
}
return temp;
}
static File safe5() throws IOException {
boolean success = true;
File temp = File.createTempFile("test", "directory");
success &= temp.delete();
success &= temp.mkdir();
if (success) {
return temp;
} else {
throw new RuntimeException("Failed to create directory");
}
}
Is Guard
not handling AssignAndExpr
correctly for controls
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is Guard not handling AssignAndExpr correctly for controls?
It is now: #7698
This is accurate. I'm happy to add that use case, but I don't think that it will occur all that often. |
When you are adding the query help, could you then direct the user to |
There are some outstanding disclosures and CVES from this query. Hence why development is kinda frozen. I'm still leveraging it to report vulns. |
override predicate isSink(DataFlow::Node sink) { | ||
exists(MethodAccess ma | | ||
ma.getMethod() instanceof MethodFileMkdir and | ||
ma.getQualifier() = sink.asExpr() | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Files.createDirectories
should also be considered here.
Files.createDirectory
is safe, but Files.createDirectories
is not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I decided not to do this, because this would be taint flow, instead of data. flow. I don't think the added overhead of taint flow is worth adding these two methods that are probably not commonly leveraged
d153907
to
f5dfc81
Compare
FYI, Spring boot was vulnerable to this bug and it had been fixed just 1 day before you created this pull request. |
Hey @trungPa, |
GH Security Advisory created here to track the above. @trungPa you should have access! Thanks! |
@JLLeitschuh I just happened to see the commit last year but didn't give it much thought before I see your PR. |
d4739a2
to
fee9c41
Compare
This PR is waiting on some stuff from #8032 but a review at this point would be appreciated |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hopefully these mostly documentation related comments are useful.
Feel free to consider them only as suggestions since I am not a member of this project.
java/ql/src/Security/CWE/CWE-378/TempDirHijackingVulnerability.qhelp
Outdated
Show resolved
Hide resolved
<li><a href="https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html#createTempDirectory-java.nio.file.Path-java.lang.String-java.nio.file.attribute.FileAttribute...-">java.nio.file.Files.createTempDirectory</a></li> | ||
<li><a href="https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html#createTempFile-java.nio.file.Path-java.lang.String-java.lang.String-java.nio.file.attribute.FileAttribute...-">java.nio.file.Files.createTempFile</a></li> | ||
</ul> | ||
<p>Otherwise, create the file/directory by manually specifying the expected posix file permissions. | ||
For example: <code>PosixFilePermissions.asFileAttribute(EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE))</code></p> | ||
<ul> | ||
<li><a href="https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html#createFile-java.nio.file.Path-java.nio.file.attribute.FileAttribute...-">java.nio.file.Files.createFile</a></li> | ||
<li><a href="https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html#createDirectory-java.nio.file.Path-java.nio.file.attribute.FileAttribute...-">java.nio.file.Files.createDirectory</a></li> | ||
<li><a href="https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html#createDirectories-java.nio.file.Path-java.nio.file.attribute.FileAttribute...-">java.nio.file.Files.createDirectories</a></li> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should createDirectories
really be recommended (especially in the context of custom file permissions)? As mentioned in this comment that method does not fail when the directories already exist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is safe when the file attribute is explicitly passed IIRC
java/ql/src/Security/CWE/CWE-378/TempDirHijackingVulnerability.qhelp
Outdated
Show resolved
Hide resolved
* All `java.io.File::createTempFile` methods. | ||
*/ | ||
class MethodFileCreateTempFile extends Method { | ||
MethodFileCreateTempFile() { | ||
this.getDeclaringType() instanceof TypeFile and | ||
this.hasName("createTempFile") | ||
} | ||
} | ||
|
||
/** | ||
* All methods on the class `java.io.File` that create directories. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be necessary to change these QLDoc comments to "A ... method ..." to comply with the style guide.
java/ql/src/Security/CWE/CWE-378/TempDirHijackingVulnerability.qhelp
Outdated
Show resolved
Hide resolved
override predicate isSource(DataFlow::Node source) { | ||
source.asExpr() = | ||
any(MethodAccess ma | | ||
ma.getMethod() instanceof MethodFileCreateTempFile and ma.getNumArgument() = 2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be good to explain why check for = 2
, that is, to match the method creating a file in the default OS temp directory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) { | ||
node2.asExpr() = | ||
any(MethodAccess ma | | ||
ma.getMethod() instanceof MethodFileCreateTempFile and ma.getArgument(2) = node1.asExpr() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this taint step correct in case the source is already a MethodFileCreateTempFile
?
It looks like this taint step is only intended for the case where File.createTempFile
is called with java.io.tmpdir
as parent directory.
(Though apparently the tests cover this and pass?)
java/ql/src/Security/CWE/CWE-378/TempDirHijackingVulnerability.ql
Outdated
Show resolved
Hide resolved
java/ql/src/Security/CWE/CWE-378/TempDirHijackingVulnerability.ql
Outdated
Show resolved
Hide resolved
#8032 is running tests + a performance check right now, so will wait til that lands and this is rebased |
5a9ed32
to
a6d7a2a
Compare
static File safe11() throws IOException { | ||
File temp = null; | ||
if (temp == null) { | ||
while (true) { | ||
temp = File.createTempFile("test", "directory"); | ||
if (temp.delete() && temp.mkdir()) { | ||
break; | ||
} | ||
} | ||
} | ||
return temp; | ||
} | ||
|
||
File safe12temp; | ||
File safe12() throws IOException { | ||
if (safe12temp == null) { | ||
while (true) { | ||
safe12temp = File.createTempFile("test", "directory"); | ||
if (safe12temp.delete() && safe12temp.mkdir()) { | ||
break; | ||
} | ||
} | ||
} | ||
return safe12temp; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned in #8490 safe11 is an uninteresting case since it's practically certainly a bug when a local variable is constant and controlling a condition. Safe12 is not safe unless this is the only possible write to that field, which we can only know if the field is private and the assignment on line 170 is the only possible write.
File vulnerable4() throws IOException { | ||
File temp = new File(System.getProperty("java.io.tmpdir")); | ||
ensureDirectory(temp); | ||
File workDir = File.createTempFile("test", "directory", temp); | ||
if (!workDir.delete()) { | ||
throw new IOException("Could not delete temp file: " + workDir.getAbsolutePath()); | ||
} | ||
ensureDirectory(workDir); | ||
return temp; | ||
} | ||
|
||
private static void ensureDirectory(File dir) throws IOException { | ||
if (!dir.mkdirs() && !dir.isDirectory()) { | ||
throw new IOException("Mkdirs failed to create " + dir.toString()); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Before, when I wasn't using using FlowState
, this actually flagged two vulnerabilities instead of one. The first one for the call to ensureDirectory(temp);
and the second for the call to ensureDirectory(workDir);
. Because I'm now using flow labels, it correctly, identifies the single path that is actually a vulnerability here.
java/ql/src/Security/CWE/CWE-378/TempDirHijackingVulnerability.ql
Outdated
Show resolved
Hide resolved
Co-authored-by: Marcono1234 <[email protected]>
Co-authored-by: Chris Smowton <[email protected]>
e0ef00b
to
cd3662c
Compare
…out changing behaviour
I've made a bunch of cleanups at JLLeitschuh#8, but the key points that absolutely must be attended to are:
Two examples of those heuristics:
I suspect for some of these heuristics we should accept slightly lower accuracy in exchange for a more predictable, explicable query. |
So I looked over your test cases, and based on them would like to suggest a simpler structure for this: Bits to keep: file created in global temp dir -> Then flag:
Then we could discard all the logic relating to throw statements (there's no reason to prefer a throw over a boolean return as an error signal) and safe/unsafe uses of the file that is In terms of the current implementation, that would mean keeping the dataflow configs that look for the temp dir -> delete -> recreate pattern, but removing the What do you think, would that achieve decent results? Could you test such a simpler query against your test suite and see if it produces acceptable results? |
On Unix-like systems, the temporary directory is shared with all users on the system. As such, improperly writing to the system temporary directory can allow attackers to hijack temporary directory resources.