|
Maybe I should use CWE-379 instead? https://cwe.mitre.org/data/definitions/379.html |
Sorry, something went wrong.
|
Would it make sense to cover java.nio.file.Files.createDirectories(...) as well? That method does not throw an exception when the directories already exists. Edit: Though I guess the use case you had in mind was the creation of a temporary directory using java.io.File. However, java.nio.file.Files provides createTempDir so I guess that pattern of deleting a temp file and then creating a directory with its path is unlikely to be used when the java.nio.file package is used. |
Sorry, something went wrong.
| override predicate isSink(DataFlow::Node sink) { isDeleteFileExpr(sink.asExpr()) } | ||
| } | ||
|
|
||
| private class TempDirHijackingFromDeleteConfig extends DataFlow2::Configuration { |
There was a problem hiding this comment.
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.
Sorry, something went wrong.
There was a problem hiding this comment.
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.
Sorry, something went wrong.
| 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.
Note you might get use within an &&, ||, == true and so on, does this work for those cases?
Sorry, something went wrong.
There was a problem hiding this comment.
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
Sorry, something went wrong.
There was a problem hiding this comment.
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)
Sorry, something went wrong.
There was a problem hiding this comment.
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:
Sorry, something went wrong.
There was a problem hiding this comment.
Btw. "controls" and "guards" are synonymous verbs in this context.
Sorry, something went wrong.
There was a problem hiding this comment.
I had to make some modifications to your proposal above but this ended up working:
However, this false-positives on the following two:
Is Guard not handling AssignAndExpr correctly for controls?
Sorry, something went wrong.
There was a problem hiding this comment.
Is Guard not handling AssignAndExpr correctly for controls?
It is now: #7698
Sorry, something went wrong.
|
Edit: Though I guess the use case you had in mind was the creation of a temporary directory using java.io.File. However, java.nio.file.Files provides createTempDir so I guess that pattern of deleting a temp file and then creating a directory with its path is unlikely to be used when the java.nio.file package is used. This is accurate. I'm happy to add that use case, but I don't think that it will occur all that often. |
Sorry, something went wrong.
|
When you are adding the query help, could you then direct the user to java.nio.file.Files.createTempDirectory(...) as alternative to their unsafe code? That would probably be the easiest and least error-prone solution. |
Sorry, something went wrong.
|
There are some outstanding disclosures and CVES from this query. Hence why development is kinda frozen. I'm still leveraging it to report vulns. |
Sorry, something went wrong.
| 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.
Files.createDirectories should also be considered here.
Files.createDirectory is safe, but Files.createDirectories is not.
Sorry, something went wrong.
There was a problem hiding this comment.
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
Sorry, something went wrong.
|
FYI, Spring boot was vulnerable to this bug and it had been fixed just 1 day before you created this pull request. |
Sorry, something went wrong.
|
Hey @trungPa, |
Sorry, something went wrong.
|
GH Security Advisory created here to track the above. @trungPa you should have access! Thanks! |
Sorry, something went wrong.
|
@JLLeitschuh I just happened to see the commit last year but didn't give it much thought before I see your PR. |
Sorry, something went wrong.
|
This PR is waiting on some stuff from #8032 but a review at this point would be appreciated |
Sorry, something went wrong.
There was a problem hiding this comment.
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.
Sorry, something went wrong.
| <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.
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.
Sorry, something went wrong.
There was a problem hiding this comment.
It is safe when the file attribute is explicitly passed IIRC
Sorry, something went wrong.
| * 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.
Might be necessary to change these QLDoc comments to "A ... method ..." to comply with the style guide.
Sorry, something went wrong.
| 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.
Might be good to explain why check for = 2, that is, to match the method creating a file in the default OS temp directory.
Sorry, something went wrong.
There was a problem hiding this comment.
Done!
Sorry, something went wrong.
| 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.
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?)
Sorry, something went wrong.
|
#8032 is running tests + a performance check right now, so will wait til that lands and this is rebased |
Sorry, something went wrong.
| 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; | ||
| } |
Sorry, something went wrong.
There was a problem hiding this comment.
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.
Sorry, something went wrong.
| 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.
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.
Sorry, something went wrong.
|
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. |
Sorry, something went wrong.
|
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 -> delete() -> mkdir[s]() -- like it so far, this is a clear indication of the pattern we're looking for, and is an ideal global dataflow problem. 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 mkdir'd and just flag the pattern as hazardous. 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 throwsIf family of predicates, the safeUse concept and the constrainedByIfCheck test, and instead just check for those conditions -- is the mkdir result discarded, or is it in a dangerous-looking local control-flow relationship with exists or isDirectory in either direction? 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? |
Sorry, something went wrong.
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.