-
Notifications
You must be signed in to change notification settings - Fork 1.7k
JS: Taint analysis for win paths #7968
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
Conversation
javascript/ql/lib/semmle/javascript/security/dataflow/TaintedPathCustomizations.qll
Outdated
Show resolved
Hide resolved
|
@erik-krogh has asked me to split the first commit into 3:
|
javascript/ql/lib/change-notes/2022-03-04-tainted-windows-paths.md
Outdated
Show resolved
Hide resolved
javascript/ql/lib/semmle/javascript/security/dataflow/TaintedPathCustomizations.qll
Outdated
Show resolved
Hide resolved
javascript/ql/lib/semmle/javascript/security/dataflow/TaintedPathCustomizations.qll
Outdated
Show resolved
Hide resolved
javascript/ql/lib/semmle/javascript/security/dataflow/TaintedPathCustomizations.qll
Outdated
Show resolved
Hide resolved
javascript/ql/lib/semmle/javascript/security/dataflow/TaintedPathCustomizations.qll
Outdated
Show resolved
Hide resolved
| /** | ||
| * A call that preserves taint without changing the flow label. | ||
| */ | ||
| class PreservingPathCall extends DataFlow::CallNode { | ||
| DataFlow::Node input; | ||
| DataFlow::Node output; | ||
| /** A call to the path.join method. */ | ||
| class JoinPathCall extends PathTransformationNode { | ||
| JoinPathCall() { | ||
| this = NodeJSLib::Path::moduleMember("join").getACall() and | ||
| input = this.getAnArgument() and | ||
| output = this | ||
| } | ||
|
|
||
| PreservingPathCall() { | ||
| this = | ||
| NodeJSLib::Path::moduleMember(["dirname", "toNamespacedPath", "parse", "format"]).getACall() and |
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'm confused by what you've done with this class.
It seems to me that you've changed the behavior of the first case (pasted below) in this "refactoring" commit.
NodeJSLib::Path::moduleMember(["dirname", "toNamespacedPath", "parse", "format"]).getACall() and
input = this.getAnArgument() and
output = thisThis class was used in the isPosixPathStep predicate to just forward a step while preserving the flow-labels.
It seems to me that you've changed it.
The "dirname" and "format" case is in the PreservingPathCall class, with the same behavior.
The "toNamespacedPath" case is in ToNamespacedPathCall, with the same behavior.
But the "parse" case is in a new class ParseCall with different behavior.
You've added some property checks based on ["root", "dir"]
I don't think that check works, but that's another matter. (CallNode::getALocalUse() can never be a PropRead).
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.
Sorry this reply took so long, I got really bogged down with other work and then time off.
What I tried to do here was to track through parse more precisely (it gives you an object which contains the root, dir, extension, baseName of a path). In principle, we may add more precise tracking of those individual properties — but that's neither a refactoring, nor what this PR really needs to be adding.
As the PR big enough on its own, I've amend the refactoring PR to remove the class ParseCall altogether (https://github.com/github/codeql/compare/7193fd691a391fb2f27db708b7877c648f797589..8b817fe555a0fdee7caed60ba44be1fc11ac39b7) and moved parse back to its friends dirname and format.
Thank you 👍
…rder to be able to track windows paths
Co-authored-by: Erik Krogh Kristensen <erik-krogh@github.com>
| exists(RegExpSequence seq | seq.getNumChild() = 2 and literal.getRoot() = seq | | ||
| seq.getChild(0) instanceof RegExpCaret and | ||
| seq.getChild(1) = term | ||
| class ToNamespacedPathCall extends PathTransformationNode { |
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 should be part of PreservingPathCall (like it was before).
| output = this.getCallback(1).getParameter(1) | ||
| module PathTransformations { | ||
| /** An abstract description of what kind of paths a call may return. */ | ||
| abstract class PathTransformationNode extends DataFlow::CallNode { |
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.
| abstract class PathTransformationNode extends DataFlow::CallNode { | |
| abstract class PathTransformationCall extends DataFlow::CallNode { |
?
| predicate isPathStep( | ||
| DataFlow::Node src, DataFlow::Node dst, Path::PathLabel srclabel, Path::PathLabel dstlabel | ||
| ) { | ||
| exists(PathTransformationNode ptn | ptn.step(srclabel, src, dstlabel, dst)) | ||
| } |
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 think you should just inline this predicate into the place it's used.
With your refactoring the predicate has become a predicate for taint-steps that originate from some call, which is not reflected in the name/qldoc.
| bindingset[s] | ||
| predicate isDrivePath(string s) { s.regexpMatch("^[a-zA-Z]:\\\\*") } | ||
|
|
||
| /** |
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.
| /** | |
| /** DEPRECATED: Use `PathLabel`. */ | |
| abstract deprecated class PosixLabel extends PathLabel { | |
| PosixLabel() { platform = platformPosix() } | |
| } | |
| /** |
| @@ -33,7 +36,38 @@ module TaintedPath { | |||
| */ | |||
| abstract class BarrierGuardNode extends DataFlow::LabeledBarrierGuardNode { } | |||
|
|
|||
| module Label { | |||
| /** | |||
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.
| /** | |
| /** DEPRECATED: Use `Path`. */ | |
| deprecated module Label = Path; | |
| /** |
| @@ -80,499 +132,596 @@ module TaintedPath { | |||
| predicate isAbsolute() { relativeness = "absolute" } | |||
|
|
|||
| /** Gets the path label with normalized flag set to true. */ | |||
| PosixPath toNormalized() { | |||
| PathLabel toNormalized() { | |||
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 think all of these utility predicates should preserve the platform
(add this.getPlatform() = result.getPlatform() to toNormalized, toNonNormalized, toAbsolute, toRelative`).
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 think your NormalizingPathCall doesn't preserve the platform due to you not preserving the platform in these utility predicates.
(The rest look OK).
| import javascript | ||
| import semmle.javascript.security.dataflow.TaintedPathQuery | ||
| import semmle.javascript.security.dataflow.TaintedPathCustomizations | ||
| import DataFlow::PathGraph | ||
| import semmle.javascript.frameworks.NodeJSLib |
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.
| import javascript | |
| import semmle.javascript.security.dataflow.TaintedPathQuery | |
| import semmle.javascript.security.dataflow.TaintedPathCustomizations | |
| import DataFlow::PathGraph | |
| import semmle.javascript.frameworks.NodeJSLib | |
| import javascript | |
| import semmle.javascript.security.dataflow.TaintedPathQuery | |
| import DataFlow::PathGraph |
Note to self: add this as an example to the ql/redundant-import ticket.
|
|
||
| class Win32Platform extends Path::Platform { |
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.
| class Win32Platform extends Path::Platform { | |
| /** Materializes the Win32 platform, which enables the Win32 tainted-path steps */ | |
| class Win32Platform extends Path::Platform { |
| @@ -0,0 +1 @@ | |||
| Security/CWE-022/TaintedWindowsPath.ql | |||
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 would like a test of which taint-flows are only an issue on Windows.
I've written it for you here:
import javascript
import semmle.javascript.security.dataflow.TaintedPathQuery
// TODO: Move this to TaintedWindowsPathQuery.qll?
class Win32Platform extends Path::Platform {
Win32Platform() { this = Path::platformWin32() }
override string getSep() { result = "\\" }
}
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where
cfg.hasFlowPath(source, sink) and
sink.getFlowLabel().(Path::PathLabel).getPlatform() = Path::platformWin32() and
not sink.getFlowLabel().(Path::PathLabel).getPlatform() = Path::platformPosix()
select source, sinkIt detects that express.js:27 is only an issue on windows (as expected).
But it doesn't flag express.js:35. I haven't looked into why.
|
|
||
| app.get("/some/path/2", function (req, res) { | ||
| const p = path.join('.', req.query.filename); | ||
| if (!p.startsWith('..')) { | ||
| req.files.foo.mv(p); // OK (but not on windows) | ||
| res.status(200); | ||
| } else { | ||
| res.status(400); | ||
| } | ||
| }); |
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.
Could you move this test to the Windows-path test folder?
With the test I suggested we can actually test that req.files.foo.mv(p) is only flagged by the windows query.
|
Talked this over with Calum; I'll drop this for the time being. |


This contributes support for tracking win32 paths in tainted path queries.
The main difference is, that you can now extend query labels by extending the
Platformclass (as inTaintedWindowsPaths.ql) and path will then be tracked using 8 labels, instead of four.By default, there's the four labels as before:
normalized-relative-posix-pathnormalized-absolute-posix-pathraw-relative-posix-pathraw-absolute-posix-pathWhen extending the
Platformclass by awin32variant, you'll additionally get:normalized-relative-win32-pathnormalized-absolute-win32-pathraw-relative-win32-pathraw-absolute-win32-path.I also refactored the
isAdditionalTaintSteppredicate that would have — IMHO — become too large after this change by pulling out much of the logic into an abstract class and extensions thereof.I ran an experiment, and looked at it with @esbena looking over my shoulder. It's not affecting the results of pre-existing queries. The results it finds for the new queries lgtm (I've looked at a subset of results from various projects).