Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions javascript/ql/src/Security/CWE-200/FileAccessToHttp.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* @name File Access data flows to Http POST/PUT
* @description Writing data from file directly to http body or request header can be an indication to data exfiltration or unauthorized information disclosure.
* @kind problem
* @problem.severity warning
* @id js/file-access-to-http
* @tags security
* external/cwe/cwe-200
*/

import javascript
import semmle.javascript.security.dataflow.FileAccessToHttp

from FileAccessToHttpDataFlow::Configuration config, DataFlow::Node src, DataFlow::Node sink
where config.hasFlow (src, sink)
select src, "$@ flows directly to Http request body", sink, "File access"
16 changes: 16 additions & 0 deletions javascript/ql/src/Security/CWE-912/HttpToFileAccess.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* @name Http response data flows to File Access
* @description Writing data from an HTTP request directly to the file system allows arbitrary file upload and might indicate a backdoor.
* @kind problem
* @problem.severity warning
* @id js/http-to-file-access
* @tags security
* external/cwe/cwe-912
*/

import javascript
import semmle.javascript.security.dataflow.HttpToFileAccess

from HttpToFileAccessFlow::Configuration configuration, DataFlow::Node src, DataFlow::Node sink
where configuration.hasFlow(src, sink)
select sink, "$@ flows to file system", src, "Untrusted data received from Http response"
16 changes: 15 additions & 1 deletion javascript/ql/src/semmle/javascript/Concepts.qll
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,27 @@ abstract class SystemCommandExecution extends DataFlow::Node {
}

/**
* A data flow node that performs a file system access.
* A data flow node that performs a file system access (read, write, copy, permissions, stats, etc).
*/
abstract class FileSystemAccess extends DataFlow::Node {

/** Gets an argument to this file system access that is interpreted as a path. */
abstract DataFlow::Node getAPathArgument();

/** Gets a node that represents file system access data, such as buffer the data is copied to. */
abstract DataFlow::Node getDataNode();
}

/**
* A data flow node that performs read file system access.
*/
abstract class FileSystemReadAccess extends FileSystemAccess { }

/**
* A data flow node that performs write file system access.
*/
abstract class FileSystemWriteAccess extends FileSystemAccess { }

/**
* A data flow node that contains a file name or an array of file names from the local file system.
*/
Expand Down
4 changes: 4 additions & 0 deletions javascript/ql/src/semmle/javascript/frameworks/Express.qll
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,10 @@ module Express {
asExpr().(MethodCallExpr).calls(any(ResponseExpr res), name))
}

override DataFlow::Node getDataNode() {
result = DataFlow::valueNode(astNode)
}

override DataFlow::Node getAPathArgument() {
result = DataFlow::valueNode(astNode.getArgument(0))
}
Expand Down
5 changes: 5 additions & 0 deletions javascript/ql/src/semmle/javascript/frameworks/HTTP.qll
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ module HTTP {
result = "http" or result = "https"
}

/**
* An expression whose value is sent as (part of) the body of an HTTP request (POST, PUT).
*/
abstract class RequestBody extends DataFlow::Node {}

/**
* An expression whose value is sent as (part of) the body of an HTTP response.
*/
Expand Down
180 changes: 174 additions & 6 deletions javascript/ql/src/semmle/javascript/frameworks/NodeJSLib.qll
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,23 @@ module NodeJSLib {
)
}

/**
* Holds if the `i`th parameter of method `methodName` of the Node.js
* `fs` module might represent a data parameter or buffer or a callback
* that receives the data.
*
* We determine this by looking for an externs declaration for
* `fs.methodName` where the `i`th parameter's name is `data` or
* `buffer` or a 'callback'.
*/
private predicate fsDataParam(string methodName, int i, string n) {
exists (ExternalMemberDecl decl, Function f, JSDocParamTag p |
decl.hasQualifiedName("fs", methodName) and f = decl.getInit() and
p.getDocumentedParameter() = f.getParameter(i).getAVariable() and
n = p.getName().toLowerCase() |
n = "data" or n = "buffer" or n = "callback"
)
}
/**
* A member `member` from module `fs` or its drop-in replacements `graceful-fs` or `fs-extra`.
*/
Expand All @@ -384,21 +401,161 @@ module NodeJSLib {
)
}


/**
* A call to a method from module `fs`, `graceful-fs` or `fs-extra`.
*/
private class NodeJSFileSystemAccess extends FileSystemAccess, DataFlow::CallNode {
private class NodeJSFileSystemAccessCall extends FileSystemAccess, DataFlow::CallNode {
string methodName;

NodeJSFileSystemAccess() {
NodeJSFileSystemAccessCall() {
this = fsModuleMember(methodName).getACall()
}

string getMethodName() {
result = methodName
}

override DataFlow::Node getDataNode() {
(
methodName = "readFileSync" and
result = this
)
or
exists (int i, string paramName | fsDataParam(methodName, i, paramName) |
(
paramName = "callback" and
exists (DataFlow::ParameterNode p, string n |
p = getCallback(i).getAParameter() and
n = p.getName().toLowerCase() and
result = p |
n = "data" or n = "buffer" or n = "string"
)
)
or
result = getArgument(i))
}

override DataFlow::Node getAPathArgument() {
exists (int i | fsFileParam(methodName, i) |
result = getArgument(i)
exists (int i | fsFileParam(methodName, i) |
result = getArgument(i))
}
}

/** Only NodeJSSystemFileAccessCalls that write data to 'fs' */
private class NodeJSFileSystemAccessWriteCall extends FileSystemWriteAccess, NodeJSFileSystemAccessCall {
NodeJSFileSystemAccessWriteCall () {
this.getMethodName() = "appendFile" or
this.getMethodName() = "appendFileSync" or
this.getMethodName() = "write" or
this.getMethodName() = "writeFile" or
this.getMethodName() = "writeFileSync" or
this.getMethodName() = "writeSync"
}
}

/** Only NodeJSSystemFileAccessCalls that read data from 'fs' */
private class NodeJSFileSystemAccessReadCall extends FileSystemReadAccess, NodeJSFileSystemAccessCall {
NodeJSFileSystemAccessReadCall () {
this.getMethodName() = "read" or
this.getMethodName() = "readSync" or
this.getMethodName() = "readFile" or
this.getMethodName() = "readFileSync"
}
}

/**
* A call to write corresponds to a pattern where file stream is open first with 'createWriteStream', followed by 'write' or 'end' call
*/
private class NodeJSFileSystemWrite extends FileSystemWriteAccess, DataFlow::CallNode {

NodeJSFileSystemAccessCall init;

NodeJSFileSystemWrite() {
exists (NodeJSFileSystemAccessCall n |
n.getCalleeName() = "createWriteStream" and init = n |
this = n.getAMemberCall("write") or
this = n.getAMemberCall("end")
)
}

override DataFlow::Node getDataNode() {
result = this.getArgument(0)
}

override DataFlow::Node getAPathArgument() {
result = init.getAPathArgument()
}
}

/**
* A call to read corresponds to a pattern where file stream is open first with createReadStream, followed by 'read' call
*/
private class NodeJSFileSystemRead extends FileSystemReadAccess, DataFlow::CallNode {

NodeJSFileSystemAccessCall init;

NodeJSFileSystemRead() {
exists (NodeJSFileSystemAccessCall n |
n.getCalleeName() = "createReadStream" and init = n |
this = n.getAMemberCall("read")
)
}

override DataFlow::Node getDataNode() {
result = this
}

override DataFlow::Node getAPathArgument() {
result = init.getAPathArgument()
}
}

/**
* A call to read corresponds to a pattern where file stream is open first with createReadStream, followed by 'pipe' call
*/
private class NodeJSFileSystemPipe extends FileSystemReadAccess, DataFlow::CallNode {

NodeJSFileSystemAccessCall init;

NodeJSFileSystemPipe() {
exists (NodeJSFileSystemAccessCall n |
n.getCalleeName() = "createReadStream" and init = n |
this = n.getAMemberCall("pipe")
)
}

override DataFlow::Node getDataNode() {
result = this.getArgument(0)
}

override DataFlow::Node getAPathArgument() {
result = init.getAPathArgument()
}
}

/**
* An 'on' event where data comes in as a parameter (usage: readstream.on('data', chunk))
*/
private class NodeJSFileSystemReadDataEvent extends FileSystemReadAccess, DataFlow::CallNode {

NodeJSFileSystemAccessCall init;

NodeJSFileSystemReadDataEvent() {
exists(NodeJSFileSystemAccessCall n |
n.getCalleeName() = "createReadStream" and init = n |
this = n.getAMethodCall("on") and
this.getArgument(0).mayHaveStringValue("data")
)
}

override DataFlow::Node getDataNode() {
result = this.getCallback(1).getParameter(0)
}

override DataFlow::Node getAPathArgument() {
result = init.getAPathArgument()
}
}

/**
Expand Down Expand Up @@ -637,9 +794,20 @@ module NodeJSLib {
result = "http.request data parameter"
}
}



/**
* An argument to client request.write () method, can be used to write body to a HTTP or HTTPS POST/PUT request,
* or request option (like headers, cookies, even url)
*/
class HttpRequestWriteArgument extends HTTP::RequestBody, DataFlow::Node {
HttpRequestWriteArgument () {
exists(CustomClientRequest req |
this = req.getAMethodCall("write").getArgument(0) or
this = req.getArgument(0))
}
}

/**
* A data flow node that is registered as a callback for an HTTP or HTTPS request made by a Node.js process, for example the function `handler` in `http.request(url).on(message, handler)`.
*/
class ClientRequestHandler extends DataFlow::FunctionNode {
Expand Down
8 changes: 8 additions & 0 deletions javascript/ql/src/semmle/javascript/frameworks/Request.qll
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,13 @@ module Request {
}

}

// using 'request' library to make http 'POST' and 'PUT' requests with message body.
private class RequestPostBody extends HTTP::RequestBody {
RequestPostBody () {
this = DataFlow::moduleMember("request", "post").getACall().getArgument(1) or
this = DataFlow::moduleImport("request").getAnInvocation().getArgument(0)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Provides Taint tracking configuration for reasoning about file access taint flow to http post body
*/
import javascript
import semmle.javascript.frameworks.HTTP

module FileAccessToHttpDataFlow {
/**
* A data flow source for reasoning about file access to http post body flow vulnerabilities.
*/
abstract class Source extends DataFlow::Node { }

/**
* A data flow sink for reasoning about file access to http post body flow vulnerabilities.
*/
abstract class Sink extends DataFlow::Node { }

/**
* A sanitizer for reasoning about file access to http post body flow vulnerabilities.
*/
abstract class Sanitizer extends DataFlow::Node { }

/**
* A taint-tracking configuration for reasoning about file access to http post body flow vulnerabilities.
*/
class Configuration extends TaintTracking::Configuration {
Configuration() { this = "FileAccessToHttpDataFlow" }

override predicate isSource(DataFlow::Node source) {
source instanceof Source
}

override predicate isSink(DataFlow::Node sink) {
sink instanceof Sink
}

override predicate isSanitizer(DataFlow::Node node) {
super.isSanitizer(node) or
node instanceof Sanitizer
}

/** additional taint step that taints an object wrapping a source */
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
(
pred = DataFlow::valueNode(_) or
pred = DataFlow::parameterNode(_) or
pred instanceof DataFlow::PropRead
) and
exists (DataFlow::PropWrite pwr |
succ = pwr.getBase() and
pred = pwr.getRhs()
)
}
}

/** A source is a file access parameter, as in readFromFile(buffer). */
private class FileAccessArgumentAsSource extends Source {
FileAccessArgumentAsSource() {
exists(FileSystemReadAccess src |
this = src.getDataNode().getALocalSource()
)
}
}

/** Sink is any parameter or argument that evaluates to a parameter ot a function or call that sets Http Body on a request */
private class HttpRequestBodyAsSink extends Sink {
HttpRequestBodyAsSink () {
this instanceof HTTP::RequestBody
}
}
}
Loading