mirror of
https://github.com/github/codeql.git
synced 2026-04-30 03:05:15 +02:00
Merge branch 'github:main' into amammad-js-CodeInjection_dynamic_import
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>Extracting Compressed files with any compression algorithm like gzip can cause to denial of service attacks.</p>
|
||||
<p>Attackers can compress a huge file which created by repeated similiar byte and convert it to a small compressed file.</p>
|
||||
|
||||
</overview>
|
||||
<recommendation>
|
||||
|
||||
<p>When you want to decompress a user-provided compressed file you must be careful about the decompression ratio or read these files within a loop byte by byte to be able to manage the decompressed size in each cycle of the loop.</p>
|
||||
|
||||
</recommendation>
|
||||
<example>
|
||||
|
||||
<p>
|
||||
JsZip: check uncompressedSize Object Field before extraction.
|
||||
</p>
|
||||
<sample src="jszip_good.js"/>
|
||||
|
||||
<p>
|
||||
nodejs Zlib: use <a href="https://nodejs.org/dist/latest-v18.x/docs/api/zlib.html#class-options">maxOutputLength option</a> which it'll limit the buffer read size
|
||||
</p>
|
||||
<sample src="zlib_good.js" />
|
||||
|
||||
<p>
|
||||
node-tar: use <a href="https://github.com/isaacs/node-tar/blob/8c5af15e43a769fd24aa7f1c84d93e54824d19d2/lib/list.js#L90">maxReadSize option</a> which it'll limit the buffer read size
|
||||
</p>
|
||||
<sample src="node-tar_good.js" />
|
||||
|
||||
</example>
|
||||
<references>
|
||||
|
||||
<li>
|
||||
<a href="https://github.com/advisories/GHSA-8225-6cvr-8pqp">CVE-2017-16129</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.bamsoftware.com/hacks/zipbomb/">A great research to gain more impact by this kind of attacks</a>
|
||||
</li>
|
||||
|
||||
</references>
|
||||
</qhelp>
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @name User-controlled file decompression
|
||||
* @description User-controlled data that flows into decompression library APIs without checking the compression rate is dangerous
|
||||
* @kind path-problem
|
||||
* @problem.severity error
|
||||
* @security-severity 7.8
|
||||
* @precision high
|
||||
* @id js/user-controlled-data-decompression
|
||||
* @tags security
|
||||
* experimental
|
||||
* external/cwe/cwe-522
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import DataFlow::PathGraph
|
||||
import DecompressionBombs
|
||||
|
||||
class BombConfiguration extends TaintTracking::Configuration {
|
||||
BombConfiguration() { this = "DecompressionBombs" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) { sink instanceof DecompressionBomb::Sink }
|
||||
|
||||
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(DecompressionBomb::AdditionalTaintStep addstep |
|
||||
addstep.isAdditionalTaintStep(pred, succ)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
from BombConfiguration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||
where cfg.hasFlowPath(source, sink)
|
||||
select sink.getNode(), source, sink, "This Decompression depends on a $@.", source.getNode(),
|
||||
"potentially untrusted source"
|
||||
@@ -0,0 +1,432 @@
|
||||
import javascript
|
||||
import experimental.semmle.javascript.FormParsers
|
||||
import experimental.semmle.javascript.ReadableStream
|
||||
import DataFlow::PathGraph
|
||||
|
||||
module DecompressionBomb {
|
||||
/**
|
||||
* The Sinks of uncontrolled data decompression
|
||||
*/
|
||||
class Sink extends DataFlow::Node {
|
||||
Sink() { this = any(Range r).sink() }
|
||||
}
|
||||
|
||||
/**
|
||||
* The additional taint steps that need for creating taint tracking or dataflow.
|
||||
*/
|
||||
abstract class AdditionalTaintStep extends string {
|
||||
AdditionalTaintStep() { this = "AdditionalTaintStep" }
|
||||
|
||||
/**
|
||||
* Holds if there is a additional taint step between pred and succ.
|
||||
*/
|
||||
abstract predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ);
|
||||
}
|
||||
|
||||
/**
|
||||
* A abstract class responsible for extending new decompression sinks
|
||||
*/
|
||||
abstract class Range extends API::Node {
|
||||
/**
|
||||
* Gets the sink of responsible for decompression node
|
||||
*
|
||||
* it can be a path, stream of compressed data,
|
||||
* or a call to function that use pipe
|
||||
*/
|
||||
abstract DataFlow::Node sink();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides additional taint steps for Readable Stream object
|
||||
*/
|
||||
module ReadableStream {
|
||||
class ReadableStreamAdditionalTaintStep extends DecompressionBomb::AdditionalTaintStep {
|
||||
ReadableStreamAdditionalTaintStep() { this = "AdditionalTaintStep" }
|
||||
|
||||
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
(
|
||||
readablePipeAdditionalTaintStep(pred, succ)
|
||||
or
|
||||
streamPipelineAdditionalTaintStep(pred, succ)
|
||||
or
|
||||
promisesFileHandlePipeAdditionalTaintStep(pred, succ)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides additional taint steps for File system access functions
|
||||
*/
|
||||
module FileSystemAccessAdditionalTaintStep {
|
||||
class ReadableStreamAdditionalTaintStep extends DecompressionBomb::AdditionalTaintStep {
|
||||
ReadableStreamAdditionalTaintStep() { this = "AdditionalTaintStep" }
|
||||
|
||||
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
// additional taint step for fs.readFile(pred)
|
||||
// It can be global additional step too
|
||||
exists(DataFlow::CallNode n | n = DataFlow::moduleMember("fs", "readFile").getACall() |
|
||||
pred = n.getArgument(0) and succ = n.getABoundCallbackParameter(1, 1)
|
||||
)
|
||||
or
|
||||
exists(FileSystemReadAccess cn |
|
||||
pred = cn.getAPathArgument() and
|
||||
succ = cn.getADataNode()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides Models for [jszip](https://www.npmjs.com/package/jszip) package
|
||||
*/
|
||||
module JsZip {
|
||||
/**
|
||||
* The decompression bomb sinks
|
||||
*/
|
||||
class DecompressionBomb extends DecompressionBomb::Range {
|
||||
DecompressionBomb() { this = API::moduleImport("jszip").getMember("loadAsync") }
|
||||
|
||||
override DataFlow::Node sink() {
|
||||
result = this.getParameter(0).asSink() and not this.sanitizer(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a jszip `loadAsync` instance
|
||||
* and Holds if member of name `uncompressedSize` exists
|
||||
*/
|
||||
predicate sanitizer(API::Node loadAsync) {
|
||||
exists(loadAsync.getASuccessor*().getMember("_data").getMember("uncompressedSize"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides Models for [node-tar](https://www.npmjs.com/package/tar) package
|
||||
*/
|
||||
module NodeTar {
|
||||
/**
|
||||
* The decompression bomb sinks
|
||||
*/
|
||||
class DecompressionBomb extends DecompressionBomb::Range {
|
||||
DecompressionBomb() { this = API::moduleImport("tar").getMember(["x", "extract"]) }
|
||||
|
||||
override DataFlow::Node sink() {
|
||||
(
|
||||
// piping tar.x()
|
||||
result = this.getACall()
|
||||
or
|
||||
// tar.x({file: filename})
|
||||
result = this.getParameter(0).getMember("file").asSink()
|
||||
) and
|
||||
// and there shouldn't be a "maxReadSize: ANum" option
|
||||
not this.sanitizer(this.getParameter(0))
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a options parameter that belong to a `tar` instance
|
||||
* and Holds if "maxReadSize: ANumber" option exists
|
||||
*/
|
||||
predicate sanitizer(API::Node tarExtract) { exists(tarExtract.getMember("maxReadSize")) }
|
||||
}
|
||||
|
||||
/**
|
||||
* The decompression Additional Taint Steps
|
||||
*/
|
||||
class DecompressionAdditionalSteps extends DecompressionBomb::AdditionalTaintStep {
|
||||
DecompressionAdditionalSteps() { this = "AdditionalTaintStep" }
|
||||
|
||||
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(API::Node n | n = API::moduleImport("tar") |
|
||||
pred = n.asSource() and
|
||||
(
|
||||
succ = n.getMember("x").getACall() or
|
||||
succ = n.getMember("x").getACall().getArgument(0)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides Models for `node:zlib` package
|
||||
*/
|
||||
module Zlib {
|
||||
/**
|
||||
* The decompression sinks of `node:zlib`
|
||||
*/
|
||||
class DecompressionBomb extends DecompressionBomb::Range {
|
||||
boolean isSynk;
|
||||
|
||||
DecompressionBomb() {
|
||||
this =
|
||||
API::moduleImport("zlib")
|
||||
.getMember([
|
||||
"gunzip", "gunzipSync", "unzip", "unzipSync", "brotliDecompress",
|
||||
"brotliDecompressSync", "inflateSync", "inflateRawSync", "inflate", "inflateRaw"
|
||||
]) and
|
||||
isSynk = true
|
||||
or
|
||||
this =
|
||||
API::moduleImport("zlib")
|
||||
.getMember([
|
||||
"createGunzip", "createBrotliDecompress", "createUnzip", "createInflate",
|
||||
"createInflateRaw"
|
||||
]) and
|
||||
isSynk = false
|
||||
}
|
||||
|
||||
override DataFlow::Node sink() {
|
||||
result = this.getACall() and
|
||||
not this.sanitizer(this.getParameter(0)) and
|
||||
isSynk = false
|
||||
or
|
||||
result = this.getACall().getArgument(0) and
|
||||
not this.sanitizer(this.getParameter(1)) and
|
||||
isSynk = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a options parameter that belong to a zlib instance
|
||||
* and Holds if "maxOutputLength: ANumber" option exists
|
||||
*/
|
||||
predicate sanitizer(API::Node zlib) { exists(zlib.getMember("maxOutputLength")) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides Models for [pako](https://www.npmjs.com/package/pako) package
|
||||
*/
|
||||
module Pako {
|
||||
/**
|
||||
* The decompression bomb sinks
|
||||
*/
|
||||
class DecompressionBomb extends DecompressionBomb::Range {
|
||||
DecompressionBomb() {
|
||||
this = API::moduleImport("pako").getMember(["inflate", "inflateRaw", "ungzip"])
|
||||
}
|
||||
|
||||
override DataFlow::Node sink() { result = this.getParameter(0).asSink() }
|
||||
}
|
||||
|
||||
/**
|
||||
* The decompression Additional Taint Steps
|
||||
*/
|
||||
class DecompressionAdditionalSteps extends DecompressionBomb::AdditionalTaintStep {
|
||||
DecompressionAdditionalSteps() { this = "AdditionalTaintStep" }
|
||||
|
||||
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
// succ = new Uint8Array(pred)
|
||||
exists(DataFlow::Node n, NewExpr ne | ne = n.asExpr() |
|
||||
pred.asExpr() = ne.getArgument(0) and
|
||||
succ.asExpr() = ne and
|
||||
ne.getCalleeName() = "Uint8Array"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides Models for [adm-zip](https://www.npmjs.com/package/adm-zip) package
|
||||
*/
|
||||
module AdmZip {
|
||||
/**
|
||||
* The decompression bomb sinks
|
||||
*/
|
||||
class DecompressionBomb extends DecompressionBomb::Range {
|
||||
DecompressionBomb() { this = API::moduleImport("adm-zip").getInstance() }
|
||||
|
||||
override DataFlow::Node sink() {
|
||||
result =
|
||||
this.getMember(["extractAllTo", "extractEntryTo", "readAsText"]).getReturn().asSource()
|
||||
or
|
||||
result = this.getASuccessor*().getMember("getData").getReturn().asSource()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The decompression Additional Taint Steps
|
||||
*/
|
||||
class DecompressionAdditionalSteps extends DecompressionBomb::AdditionalTaintStep {
|
||||
DecompressionAdditionalSteps() { this = "AdditionalTaintStep" }
|
||||
|
||||
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(API::Node n | n = API::moduleImport("adm-zip") |
|
||||
pred = n.getParameter(0).asSink() and
|
||||
(
|
||||
succ =
|
||||
n.getInstance()
|
||||
.getMember(["extractAllTo", "extractEntryTo", "readAsText"])
|
||||
.getReturn()
|
||||
.asSource()
|
||||
or
|
||||
succ =
|
||||
n.getInstance()
|
||||
.getMember("getEntries")
|
||||
.getASuccessor*()
|
||||
.getMember("getData")
|
||||
.getReturn()
|
||||
.asSource()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides Models for [decompress](https://www.npmjs.com/package/decompress) package
|
||||
*/
|
||||
module Decompress {
|
||||
/**
|
||||
* The decompression bomb sinks
|
||||
*/
|
||||
class DecompressionBomb extends DecompressionBomb::Range {
|
||||
DecompressionBomb() { this = API::moduleImport("decompress") }
|
||||
|
||||
override DataFlow::Node sink() { result = this.getACall().getArgument(0) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides Models for [gunzip-maybe][https://www.npmjs.com/package/gunzip-maybe] package
|
||||
*/
|
||||
module GunzipMaybe {
|
||||
/**
|
||||
* The decompression bomb sinks
|
||||
*/
|
||||
class DecompressionBomb extends DecompressionBomb::Range {
|
||||
DecompressionBomb() { this = API::moduleImport("gunzip-maybe") }
|
||||
|
||||
override DataFlow::Node sink() { result = this.getACall() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides Models for [unbzip2-stream](https://www.npmjs.com/package/unbzip2-stream) package
|
||||
*/
|
||||
module Unbzip2Stream {
|
||||
/**
|
||||
* The decompression bomb sinks
|
||||
*/
|
||||
class DecompressionBomb extends DecompressionBomb::Range {
|
||||
DecompressionBomb() { this = API::moduleImport("unbzip2-stream") }
|
||||
|
||||
override DataFlow::Node sink() { result = this.getACall() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides Models for [unzipper](https://www.npmjs.com/package/unzipper) package
|
||||
*/
|
||||
module Unzipper {
|
||||
/**
|
||||
* The decompression bomb sinks
|
||||
*/
|
||||
class DecompressionBomb extends DecompressionBomb::Range {
|
||||
string funcName;
|
||||
|
||||
DecompressionBomb() {
|
||||
this = API::moduleImport("unzipper").getMember(["Extract", "Parse", "ParseOne"]) and
|
||||
funcName = ["Extract", "Parse", "ParseOne"]
|
||||
or
|
||||
this = API::moduleImport("unzipper").getMember("Open") and
|
||||
// open has some functions which will be specified in sink predicate
|
||||
funcName = "Open"
|
||||
}
|
||||
|
||||
override DataFlow::Node sink() {
|
||||
result = this.getMember(["buffer", "file", "url", "file"]).getACall().getArgument(0) and
|
||||
funcName = "Open"
|
||||
or
|
||||
result = this.getACall() and
|
||||
funcName = ["Extract", "Parse", "ParseOne"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a
|
||||
* and Holds if unzipper instance has a member `uncompressedSize`
|
||||
*
|
||||
* it is really difficult to implement this sanitizer,
|
||||
* so i'm going to check if there is a member like `vars.uncompressedSize` in whole DB or not!
|
||||
*/
|
||||
predicate sanitizer() {
|
||||
exists(this.getASuccessor*().getMember("vars").getMember("uncompressedSize")) and
|
||||
funcName = ["Extract", "Parse", "ParseOne"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides Models for [yauzl](https://www.npmjs.com/package/yauzl) package
|
||||
*/
|
||||
module Yauzl {
|
||||
API::Node test() { result = API::moduleImport("yauzl").getASuccessor*() }
|
||||
|
||||
/**
|
||||
* The decompression bomb sinks
|
||||
*/
|
||||
class DecompressionBomb extends DecompressionBomb::Range {
|
||||
// open function has a sanitizer
|
||||
string methodName;
|
||||
|
||||
DecompressionBomb() {
|
||||
this =
|
||||
API::moduleImport("yauzl").getMember(["fromFd", "fromBuffer", "fromRandomAccessReader"]) and
|
||||
methodName = "from"
|
||||
or
|
||||
this = API::moduleImport("yauzl").getMember("open") and
|
||||
methodName = "open"
|
||||
}
|
||||
|
||||
override DataFlow::Node sink() {
|
||||
(
|
||||
result = this.getParameter(2).getParameter(1).getMember("readEntry").getACall() or
|
||||
result =
|
||||
this.getParameter(2)
|
||||
.getParameter(1)
|
||||
.getMember("openReadStream")
|
||||
.getParameter(1)
|
||||
.getParameter(1)
|
||||
.asSource()
|
||||
) and
|
||||
not this.sanitizer() and
|
||||
methodName = "open"
|
||||
or
|
||||
result = this.getParameter(0).asSink() and
|
||||
methodName = "from"
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a
|
||||
* and Holds if yauzl `open` instance has a member `uncompressedSize`
|
||||
*/
|
||||
predicate sanitizer() {
|
||||
exists(this.getASuccessor*().getMember("uncompressedSize")) and
|
||||
methodName = ["readStream", "open"]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The decompression Additional Taint Steps
|
||||
*/
|
||||
class DecompressionAdditionalSteps extends DecompressionBomb::AdditionalTaintStep {
|
||||
DecompressionAdditionalSteps() { this = "AdditionalTaintStep" }
|
||||
|
||||
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(API::Node open | open = API::moduleImport("yauzl").getMember("open") |
|
||||
pred = open.getParameter(0).asSink() and
|
||||
(
|
||||
succ = open.getParameter(2).getParameter(1).getMember("readEntry").getACall() or
|
||||
succ =
|
||||
open.getParameter(2)
|
||||
.getParameter(1)
|
||||
.getMember("openReadStream")
|
||||
.getParameter(1)
|
||||
.getParameter(1)
|
||||
.asSource()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
const jszipp = require("jszip");
|
||||
function zipBombSafe(zipFile) {
|
||||
jszipp.loadAsync(zipFile.data).then(function (zip) {
|
||||
if (zip.file("10GB")["_data"]["uncompressedSize"] > 1024 * 1024 * 8) {
|
||||
console.log("error")
|
||||
}
|
||||
zip.file("10GB").async("uint8array").then(function (u8) {
|
||||
console.log(u8);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
const tar = require("tar");
|
||||
|
||||
tar.x({
|
||||
file: tarFileName,
|
||||
strip: 1,
|
||||
C: 'some-dir',
|
||||
maxReadSize: 16 * 1024 * 1024 // 16 MB
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
const zlib = require("zlib");
|
||||
|
||||
zlib.gunzip(
|
||||
inputZipFile.data,
|
||||
{ maxOutputLength: 1024 * 1024 * 5 },
|
||||
(err, buffer) => {
|
||||
doSomeThingWithData(buffer);
|
||||
});
|
||||
zlib.gunzipSync(inputZipFile.data, { maxOutputLength: 1024 * 1024 * 5 });
|
||||
|
||||
inputZipFile.pipe(zlib.createGunzip({ maxOutputLength: 1024 * 1024 * 5 })).pipe(outputFile);
|
||||
@@ -1,4 +0,0 @@
|
||||
/** DEPRECATED: Use `semmle.javascript.Actions` instead. */
|
||||
deprecated module Actions {
|
||||
import semmle.javascript.Actions::Actions
|
||||
}
|
||||
211
javascript/ql/src/experimental/semmle/javascript/Execa.qll
Normal file
211
javascript/ql/src/experimental/semmle/javascript/Execa.qll
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Models the `execa` library in terms of `FileSystemAccess` and `SystemCommandExecution`.
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
/**
|
||||
* Provide model for [Execa](https://github.com/sindresorhus/execa) package
|
||||
*/
|
||||
module Execa {
|
||||
/**
|
||||
* The Execa input file read and output file write
|
||||
*/
|
||||
class ExecaFileSystemAccess extends FileSystemReadAccess, DataFlow::Node {
|
||||
API::Node execaArg;
|
||||
boolean isPipedToFile;
|
||||
|
||||
ExecaFileSystemAccess() {
|
||||
(
|
||||
execaArg = API::moduleImport("execa").getMember("$").getParameter(0) and
|
||||
isPipedToFile = false
|
||||
or
|
||||
execaArg =
|
||||
API::moduleImport("execa")
|
||||
.getMember(["execa", "execaCommand", "execaCommandSync", "execaSync"])
|
||||
.getParameter([0, 1, 2]) and
|
||||
isPipedToFile = false
|
||||
or
|
||||
execaArg =
|
||||
API::moduleImport("execa")
|
||||
.getMember(["execa", "execaCommand", "execaCommandSync", "execaSync"])
|
||||
.getReturn()
|
||||
.getMember(["pipeStdout", "pipeAll", "pipeStderr"])
|
||||
.getParameter(0) and
|
||||
isPipedToFile = true
|
||||
) and
|
||||
this = execaArg.asSink()
|
||||
}
|
||||
|
||||
override DataFlow::Node getADataNode() { none() }
|
||||
|
||||
override DataFlow::Node getAPathArgument() {
|
||||
result = execaArg.getMember("inputFile").asSink() and isPipedToFile = false
|
||||
or
|
||||
result = execaArg.asSink() and isPipedToFile = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `execa.execa` or `execa.execaSync`
|
||||
*/
|
||||
class ExecaCall extends API::CallNode {
|
||||
boolean isSync;
|
||||
|
||||
ExecaCall() {
|
||||
this = API::moduleImport("execa").getMember("execa").getACall() and
|
||||
isSync = false
|
||||
or
|
||||
this = API::moduleImport("execa").getMember("execaSync").getACall() and
|
||||
isSync = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The system command execution nodes for `execa.execa` or `execa.execaSync` functions
|
||||
*/
|
||||
class ExecaExec extends SystemCommandExecution, ExecaCall {
|
||||
ExecaExec() { isSync = [false, true] }
|
||||
|
||||
override DataFlow::Node getACommandArgument() { result = this.getArgument(0) }
|
||||
|
||||
override predicate isShellInterpreted(DataFlow::Node arg) {
|
||||
// if shell: true then first and second args are sinks
|
||||
// options can be third argument
|
||||
arg = [this.getArgument(0), this.getParameter(1).getUnknownMember().asSink()] and
|
||||
isExecaShellEnable(this.getParameter(2))
|
||||
or
|
||||
// options can be second argument
|
||||
arg = this.getArgument(0) and
|
||||
isExecaShellEnable(this.getParameter(1))
|
||||
}
|
||||
|
||||
override DataFlow::Node getArgumentList() {
|
||||
// execa(cmd, [arg]);
|
||||
exists(DataFlow::Node arg | arg = this.getArgument(1) |
|
||||
// if it is a object then it is a option argument not command argument
|
||||
result = arg and not arg.asExpr() instanceof ObjectExpr
|
||||
)
|
||||
}
|
||||
|
||||
override predicate isSync() { isSync = true }
|
||||
|
||||
override DataFlow::Node getOptionsArg() {
|
||||
result = this.getLastArgument() and result.asExpr() instanceof ObjectExpr
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `execa.$` or `execa.$.sync` or `execa.$({})` or `execa.$.sync({})` tag functions
|
||||
*/
|
||||
private class ExecaScriptCall extends API::CallNode {
|
||||
boolean isSync;
|
||||
|
||||
ExecaScriptCall() {
|
||||
exists(API::Node script |
|
||||
script =
|
||||
[
|
||||
API::moduleImport("execa").getMember("$"),
|
||||
API::moduleImport("execa").getMember("$").getReturn()
|
||||
]
|
||||
|
|
||||
this = script.getACall() and
|
||||
isSync = false
|
||||
or
|
||||
this = script.getMember("sync").getACall() and
|
||||
isSync = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The system command execution nodes for `execa.$` or `execa.$.sync` tag functions
|
||||
*/
|
||||
class ExecaScript extends SystemCommandExecution, ExecaScriptCall {
|
||||
ExecaScript() { isSync = [false, true] }
|
||||
|
||||
override DataFlow::Node getACommandArgument() {
|
||||
result = this.getParameter(1).asSink() and
|
||||
not isTaggedTemplateFirstChildAnElement(this.getParameter(1).asSink().asExpr().getParent())
|
||||
}
|
||||
|
||||
override predicate isShellInterpreted(DataFlow::Node arg) {
|
||||
isExecaShellEnable(this.getParameter(0)) and
|
||||
arg = this.getAParameter().asSink()
|
||||
}
|
||||
|
||||
override DataFlow::Node getArgumentList() {
|
||||
result = this.getParameter(any(int i | i >= 1)).asSink() and
|
||||
isTaggedTemplateFirstChildAnElement(this.getParameter(1).asSink().asExpr().getParent())
|
||||
or
|
||||
result = this.getParameter(any(int i | i >= 2)).asSink() and
|
||||
not isTaggedTemplateFirstChildAnElement(this.getParameter(1).asSink().asExpr().getParent())
|
||||
}
|
||||
|
||||
override DataFlow::Node getOptionsArg() { result = this.getParameter(0).asSink() }
|
||||
|
||||
override predicate isSync() { isSync = true }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `execa.execaCommandSync` or `execa.execaCommand`
|
||||
*/
|
||||
private class ExecaCommandCall extends API::CallNode {
|
||||
boolean isSync;
|
||||
|
||||
ExecaCommandCall() {
|
||||
this = API::moduleImport("execa").getMember("execaCommandSync").getACall() and
|
||||
isSync = true
|
||||
or
|
||||
this = API::moduleImport("execa").getMember("execaCommand").getACall() and
|
||||
isSync = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The system command execution nodes for `execa.execaCommand` or `execa.execaCommandSync` functions
|
||||
*/
|
||||
class ExecaCommandExec extends SystemCommandExecution, ExecaCommandCall {
|
||||
ExecaCommandExec() { isSync = [false, true] }
|
||||
|
||||
override DataFlow::Node getACommandArgument() {
|
||||
result = this.(DataFlow::CallNode).getArgument(0)
|
||||
}
|
||||
|
||||
override DataFlow::Node getArgumentList() {
|
||||
// execaCommand(`${cmd} ${arg}`);
|
||||
result.asExpr() = this.getParameter(0).asSink().asExpr().getAChildExpr() and
|
||||
not result.asExpr() = this.getArgument(0).asExpr().getChildExpr(0)
|
||||
}
|
||||
|
||||
override predicate isShellInterpreted(DataFlow::Node arg) {
|
||||
// execaCommandSync(`${cmd} ${arg}`, {shell: true})
|
||||
arg.asExpr() = this.getArgument(0).asExpr().getAChildExpr+() and
|
||||
isExecaShellEnable(this.getParameter(1))
|
||||
or
|
||||
// there is only one argument that is constructed in previous nodes,
|
||||
// it makes sanitizing really hard to select whether it is vulnerable to argument injection or not
|
||||
arg = this.getParameter(0).asSink() and
|
||||
not exists(this.getArgument(0).asExpr().getChildExpr(1))
|
||||
}
|
||||
|
||||
override predicate isSync() { isSync = true }
|
||||
|
||||
override DataFlow::Node getOptionsArg() {
|
||||
result = this.getLastArgument() and result.asExpr() instanceof ObjectExpr
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets a TemplateLiteral and check if first child is a template element */
|
||||
private predicate isTaggedTemplateFirstChildAnElement(TemplateLiteral templateLit) {
|
||||
exists(templateLit.getChildExpr(0).(TemplateElement))
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds whether Execa has shell enabled options or not, get Parameter responsible for options
|
||||
*/
|
||||
pragma[inline]
|
||||
private predicate isExecaShellEnable(API::Node n) {
|
||||
n.getMember("shell").asSink().asExpr().(BooleanLiteral).getValue() = "true"
|
||||
}
|
||||
}
|
||||
179
javascript/ql/src/experimental/semmle/javascript/FormParsers.qll
Normal file
179
javascript/ql/src/experimental/semmle/javascript/FormParsers.qll
Normal file
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* Provides classes for modeling the server-side form/file parsing libraries.
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import experimental.semmle.javascript.ReadableStream
|
||||
|
||||
/**
|
||||
* A module for modeling [busboy](https://www.npmjs.com/package/busboy) package
|
||||
*/
|
||||
module BusBoy {
|
||||
/**
|
||||
* A source of remote flow from the `Busboy` library.
|
||||
*/
|
||||
private class BusBoyRemoteFlow extends RemoteFlowSource {
|
||||
BusBoyRemoteFlow() {
|
||||
exists(API::Node busboyOnEvent |
|
||||
busboyOnEvent = API::moduleImport("busboy").getReturn().getMember("on")
|
||||
|
|
||||
// Files
|
||||
busboyOnEvent.getParameter(0).asSink().mayHaveStringValue("file") and
|
||||
// second param of 'file' event is a Readable stream
|
||||
this = readableStreamDataNode(busboyOnEvent.getParameter(1).getParameter(1))
|
||||
or
|
||||
// Fields
|
||||
busboyOnEvent.getParameter(0).asSink().mayHaveStringValue(["file", "field"]) and
|
||||
this =
|
||||
API::moduleImport("busboy")
|
||||
.getReturn()
|
||||
.getMember("on")
|
||||
.getParameter(1)
|
||||
.getAParameter()
|
||||
.asSource()
|
||||
)
|
||||
}
|
||||
|
||||
override string getSourceType() { result = "parsed user value from Busbuy" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A busboy file data step according to a Readable Stream type
|
||||
*/
|
||||
private class AdditionalTaintStep extends TaintTracking::SharedTaintStep {
|
||||
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(API::Node busboyOnEvent |
|
||||
busboyOnEvent = API::moduleImport("busboy").getReturn().getMember("on")
|
||||
|
|
||||
busboyOnEvent.getParameter(0).asSink().mayHaveStringValue("file") and
|
||||
customStreamPipeAdditionalTaintStep(busboyOnEvent.getParameter(1).getParameter(1), pred,
|
||||
succ)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A module for modeling [formidable](https://www.npmjs.com/package/formidable) package
|
||||
*/
|
||||
module Formidable {
|
||||
/**
|
||||
* A source of remote flow from the `Formidable` library parsing a HTTP request.
|
||||
*/
|
||||
private class FormidableRemoteFlow extends RemoteFlowSource {
|
||||
FormidableRemoteFlow() {
|
||||
exists(API::Node formidable |
|
||||
formidable = API::moduleImport("formidable").getReturn()
|
||||
or
|
||||
formidable = API::moduleImport("formidable").getMember("formidable").getReturn()
|
||||
or
|
||||
formidable =
|
||||
API::moduleImport("formidable").getMember(["IncomingForm", "Formidable"]).getInstance()
|
||||
|
|
||||
this =
|
||||
formidable.getMember("parse").getACall().getABoundCallbackParameter(1, any(int i | i > 0))
|
||||
or
|
||||
// if callback is not provide a promise will be returned,
|
||||
// return values contains [fields,files] members
|
||||
exists(API::Node parseMethod |
|
||||
parseMethod = formidable.getMember("parse") and parseMethod.getNumParameter() = 1
|
||||
|
|
||||
this = parseMethod.getReturn().asSource()
|
||||
)
|
||||
or
|
||||
// event handler
|
||||
this = formidable.getMember("on").getParameter(1).getAParameter().asSource()
|
||||
)
|
||||
}
|
||||
|
||||
override string getSourceType() { result = "parsed user value from Formidable" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A module for modeling [multiparty](https://www.npmjs.com/package/multiparty) package
|
||||
*/
|
||||
module Multiparty {
|
||||
/**
|
||||
* A source of remote flow from the `Multiparty` library.
|
||||
*/
|
||||
private class MultipartyRemoteFlow extends RemoteFlowSource {
|
||||
MultipartyRemoteFlow() {
|
||||
exists(API::Node form |
|
||||
form = API::moduleImport("multiparty").getMember("Form").getInstance()
|
||||
|
|
||||
exists(API::CallNode parse | parse = form.getMember("parse").getACall() |
|
||||
this = parse.getParameter(1).getParameter([1, 2]).asSource()
|
||||
)
|
||||
or
|
||||
exists(API::Node on | on = form.getMember("on") |
|
||||
(
|
||||
on.getParameter(0).asSink().mayHaveStringValue(["file", "field"]) and
|
||||
this = on.getParameter(1).getParameter([0, 1]).asSource()
|
||||
or
|
||||
on.getParameter(0).asSink().mayHaveStringValue("part") and
|
||||
this = readableStreamDataNode(on.getParameter(1).getParameter(0))
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override string getSourceType() { result = "parsed user value from Multiparty" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A multiparty part data step according to a Readable Stream type
|
||||
*/
|
||||
private class AdditionalTaintStep extends TaintTracking::SharedTaintStep {
|
||||
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(API::Node multipartyOnEvent |
|
||||
multipartyOnEvent =
|
||||
API::moduleImport("multiparty").getMember("Form").getInstance().getMember("on")
|
||||
|
|
||||
multipartyOnEvent.getParameter(0).asSink().mayHaveStringValue("part") and
|
||||
customStreamPipeAdditionalTaintStep(multipartyOnEvent.getParameter(1).getParameter(0), pred,
|
||||
succ)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A module for modeling [dicer](https://www.npmjs.com/package/dicer) package
|
||||
*/
|
||||
module Dicer {
|
||||
/**
|
||||
* A source of remote flow from the `dicer` library.
|
||||
*/
|
||||
private class DicerRemoteFlow extends RemoteFlowSource {
|
||||
DicerRemoteFlow() {
|
||||
exists(API::Node dicer | dicer = API::moduleImport("dicer").getInstance() |
|
||||
exists(API::Node on | on = dicer.getMember("on") |
|
||||
on.getParameter(0).asSink().mayHaveStringValue("part") and
|
||||
this = readableStreamDataNode(on.getParameter(1).getParameter(0))
|
||||
or
|
||||
exists(API::Node onPart | onPart = on.getParameter(1).getParameter(0).getMember("on") |
|
||||
onPart.getParameter(0).asSink().mayHaveStringValue("header") and
|
||||
this = onPart.getParameter(1).getParameter(0).asSource()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override string getSourceType() { result = "parsed user value from Dicer" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A dicer part data step according to a Readable Stream type
|
||||
*/
|
||||
private class AdditionalTaintStep extends TaintTracking::SharedTaintStep {
|
||||
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(API::Node onEvent |
|
||||
onEvent = API::moduleImport("dicer").getInstance().getMember("on")
|
||||
|
|
||||
onEvent.getParameter(0).asSink().mayHaveStringValue("part") and
|
||||
customStreamPipeAdditionalTaintStep(onEvent.getParameter(1).getParameter(0), pred, succ)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Provides helper predicates to work with any Readable Stream in dataflow queries
|
||||
*
|
||||
* main predicate in which you can use by passing a Readable Stream is `customStreamPipeAdditionalTaintStep`
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
/**
|
||||
* Holds if there is a step between `fs.createReadStream` and `stream.Readable.from` first parameters to all other piped parameters
|
||||
*
|
||||
* It can be global additional step too
|
||||
*/
|
||||
predicate readablePipeAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(API::Node receiver |
|
||||
receiver =
|
||||
[
|
||||
API::moduleImport("fs").getMember("createReadStream"),
|
||||
API::moduleImport("stream").getMember("Readable").getMember("from")
|
||||
]
|
||||
|
|
||||
customStreamPipeAdditionalTaintStep(receiver, pred, succ)
|
||||
or
|
||||
pred = receiver.getParameter(0).asSink() and
|
||||
succ = receiver.getReturn().asSource()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* additional taint steps for piped stream from `createReadStream` method of `fs/promises.open`
|
||||
*
|
||||
* It can be global additional step too
|
||||
*/
|
||||
predicate promisesFileHandlePipeAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(API::Node receiver | receiver = nodeJsPromisesFileSystem().getMember("open") |
|
||||
customStreamPipeAdditionalTaintStep(receiver, pred, succ)
|
||||
or
|
||||
pred = receiver.getParameter(0).asSink() and
|
||||
succ = receiver.getReturn().asSource()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets nodejs `fs` Promises API
|
||||
*/
|
||||
API::Node nodeJsPromisesFileSystem() {
|
||||
result = [API::moduleImport("fs").getMember("promises"), API::moduleImport("fs/promises")]
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if
|
||||
* or `receiver.pipe(pred).pipe(sth).pipe(succ)`
|
||||
*
|
||||
* or `receiver.pipe(sth).pipe(pred).pipe(succ)`
|
||||
*
|
||||
* or `receiver.pipe(succ)` and receiver is pred
|
||||
*
|
||||
* Receiver is a Readable Stream object
|
||||
*/
|
||||
predicate customStreamPipeAdditionalTaintStep(
|
||||
API::Node receiver, DataFlow::Node pred, DataFlow::Node succ
|
||||
) {
|
||||
// following connect the first pipe parameter to the last pipe parameter
|
||||
exists(API::Node firstPipe | firstPipe = receiver.getMember("pipe") |
|
||||
pred = firstPipe.getParameter(0).asSink() and
|
||||
succ = firstPipe.getASuccessor*().getMember("pipe").getParameter(0).asSink()
|
||||
)
|
||||
or
|
||||
// following connect a pipe parameter to the next pipe parameter
|
||||
exists(API::Node cn | cn = receiver.getASuccessor+() |
|
||||
pred = cn.getParameter(0).asSink() and
|
||||
succ = cn.getReturn().getMember("pipe").getParameter(0).asSink()
|
||||
)
|
||||
or
|
||||
// it is a function that its return value is a Readable stream object
|
||||
pred = receiver.getReturn().asSource() and
|
||||
succ = receiver.getReturn().getMember("pipe").getParameter(0).asSink()
|
||||
or
|
||||
// it is a Readable stream object
|
||||
pred = receiver.asSource() and
|
||||
succ = receiver.getMember("pipe").getParameter(0).asSink()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if
|
||||
*
|
||||
* ```js
|
||||
* await pipeline(
|
||||
* pred,
|
||||
* succ_or_pred,
|
||||
* succ
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
* It can be global additional step too
|
||||
*/
|
||||
predicate streamPipelineAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
// this step connect the a pipeline parameter to the next pipeline parameter
|
||||
exists(API::CallNode cn, int i |
|
||||
// we assume that there are maximum 10 pipes mostly or maybe less
|
||||
i in [0 .. 10] and
|
||||
cn = nodeJsStream().getMember("pipeline").getACall()
|
||||
|
|
||||
pred = cn.getParameter(i).asSink() and
|
||||
succ = cn.getParameter(i + 1).asSink()
|
||||
)
|
||||
or
|
||||
// this step connect the first pipeline parameter to the next parameters
|
||||
exists(API::CallNode cn, int i |
|
||||
// we assume that there are maximum 10 pipes mostly or maybe less
|
||||
i in [1 .. 10] and
|
||||
cn = nodeJsStream().getMember("pipeline").getACall()
|
||||
|
|
||||
pred = cn.getParameter(0).asSink() and
|
||||
succ = cn.getParameter(i).asSink()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets `stream` Promises API
|
||||
*/
|
||||
API::Node nodeJsStream() {
|
||||
result = [API::moduleImport("stream/promises"), API::moduleImport("stream").getMember("promises")]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a Readable stream object,
|
||||
* and returns all nodes responsible for a data read of this Readable stream
|
||||
*/
|
||||
DataFlow::Node readableStreamDataNode(API::Node stream) {
|
||||
result = stream.asSource()
|
||||
or
|
||||
// 'data' event
|
||||
exists(API::CallNode onEvent | onEvent = stream.getMember("on").getACall() |
|
||||
result = onEvent.getParameter(1).getParameter(0).asSource() and
|
||||
onEvent.getParameter(0).asSink().mayHaveStringValue("data")
|
||||
)
|
||||
or
|
||||
// 'Readable' event
|
||||
exists(API::CallNode onEvent | onEvent = stream.getMember("on").getACall() |
|
||||
(
|
||||
result = onEvent.getParameter(1).getReceiver().getMember("read").getReturn().asSource() or
|
||||
result = stream.getMember("read").getReturn().asSource()
|
||||
) and
|
||||
onEvent.getParameter(0).asSink().mayHaveStringValue("readable")
|
||||
)
|
||||
}
|
||||
157
javascript/ql/src/experimental/semmle/javascript/SQL.qll
Normal file
157
javascript/ql/src/experimental/semmle/javascript/SQL.qll
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Provides classes for working with SQL connectors.
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
module ExperimentalSql {
|
||||
/**
|
||||
* Provides SQL injection Sinks for the [TypeORM](https://www.npmjs.com/package/typeorm) package
|
||||
*/
|
||||
private module TypeOrm {
|
||||
/**
|
||||
* Gets a `DataSource` instance
|
||||
*
|
||||
* `DataSource` is a pre-defined connection configuration to a specific database.
|
||||
*/
|
||||
API::Node dataSource() {
|
||||
result = API::moduleImport("typeorm").getMember("DataSource").getInstance()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a `QueryRunner` instance
|
||||
*/
|
||||
API::Node queryRunner() { result = dataSource().getMember("createQueryRunner").getReturn() }
|
||||
|
||||
/**
|
||||
* Gets a `*QueryBuilder` instance
|
||||
*/
|
||||
API::Node queryBuilderInstance() {
|
||||
// a `*QueryBuilder` instance of a Data Mapper based Entity
|
||||
result =
|
||||
[
|
||||
// Using DataSource
|
||||
dataSource(),
|
||||
// Using repository
|
||||
dataSource().getMember("getRepository").getReturn(),
|
||||
// Using entity manager
|
||||
dataSource().getMember("manager"), queryRunner().getMember("manager")
|
||||
].getMember("createQueryBuilder").getReturn()
|
||||
or
|
||||
// A `*QueryBuilder` instance of an Active record based Entity
|
||||
result =
|
||||
API::moduleImport("typeorm")
|
||||
.getMember("Entity")
|
||||
.getReturn()
|
||||
.getADecoratedClass()
|
||||
.getMember("createQueryBuilder")
|
||||
.getReturn()
|
||||
or
|
||||
// A WhereExpressionBuilder can be used in complex WHERE expression
|
||||
result =
|
||||
API::moduleImport("typeorm")
|
||||
.getMember(["Brackets", "NotBrackets"])
|
||||
.getParameter(0)
|
||||
.getParameter(0)
|
||||
or
|
||||
// In case of custom query builders
|
||||
result =
|
||||
API::moduleImport("typeorm")
|
||||
.getMember([
|
||||
"SelectQueryBuilder", "InsertQueryBuilder", "RelationQueryBuilder",
|
||||
"UpdateQueryBuilder", "WhereExpressionBuilder"
|
||||
])
|
||||
.getInstance()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets function names which create any type of `QueryBuilder` like `WhereExpressionBuilder` or `InsertQueryBuilder`
|
||||
*/
|
||||
string queryBuilderMethods() {
|
||||
result =
|
||||
[
|
||||
"select", "addSelect", "where", "andWhere", "orWhere", "having", "orHaving", "andHaving",
|
||||
"orderBy", "addOrderBy", "distinctOn", "groupBy", "addCommonTableExpression",
|
||||
"leftJoinAndSelect", "innerJoinAndSelect", "leftJoin", "innerJoin", "leftJoinAndMapOne",
|
||||
"innerJoinAndMapOne", "leftJoinAndMapMany", "innerJoinAndMapMany", "orUpdate", "orIgnore",
|
||||
"values", "set"
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets function names that the return values of these functions can be the results of a database query run
|
||||
*/
|
||||
string queryBuilderResult() {
|
||||
result = ["getOne", "getOneOrFail", "getMany", "getRawOne", "getRawMany", "stream"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a QueryBuilder instance that has a query builder function
|
||||
*/
|
||||
API::Node getASuccessorOfBuilderInstance(string queryBuilderMethod) {
|
||||
result.getMember(queryBuilderMethod) = queryBuilderInstance().getASuccessor*()
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to some successor functions of TypeORM `createQueryBuilder` function which are dangerous
|
||||
*/
|
||||
private class QueryBuilderCall extends DatabaseAccess, DataFlow::Node {
|
||||
API::Node queryBuilder;
|
||||
|
||||
QueryBuilderCall() {
|
||||
queryBuilder = getASuccessorOfBuilderInstance(queryBuilderMethods()) and
|
||||
this = queryBuilder.asSource()
|
||||
}
|
||||
|
||||
override DataFlow::Node getAResult() {
|
||||
result = queryBuilder.getMember(queryBuilderResult()).getReturn().asSource()
|
||||
}
|
||||
|
||||
override DataFlow::Node getAQueryArgument() {
|
||||
exists(string memberName | memberName = queryBuilderMethods() |
|
||||
memberName = ["leftJoinAndSelect", "innerJoinAndSelect", "leftJoin", "innerJoin"] and
|
||||
result = queryBuilder.getMember(memberName).getParameter(2).asSink()
|
||||
or
|
||||
memberName =
|
||||
["leftJoinAndMapOne", "innerJoinAndMapOne", "leftJoinAndMapMany", "innerJoinAndMapMany"] and
|
||||
result = queryBuilder.getMember(memberName).getParameter(3).asSink()
|
||||
or
|
||||
memberName =
|
||||
[
|
||||
"select", "addSelect", "where", "andWhere", "orWhere", "having", "orHaving",
|
||||
"andHaving", "orderBy", "addOrderBy", "distinctOn", "groupBy",
|
||||
"addCommonTableExpression"
|
||||
] and
|
||||
result = queryBuilder.getMember(memberName).getParameter(0).asSink()
|
||||
or
|
||||
memberName = ["orIgnore", "orUpdate"] and
|
||||
result = queryBuilder.getMember(memberName).getParameter([0, 1]).asSink()
|
||||
or
|
||||
// following functions if use a function as their input fields,called function parameters which are vulnerable
|
||||
memberName = ["values", "set"] and
|
||||
result =
|
||||
queryBuilder.getMember(memberName).getParameter(0).getAMember().getReturn().asSink()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to the TypeORM `query` function of a `QueryRunner`
|
||||
*/
|
||||
private class QueryRunner extends DatabaseAccess, API::CallNode {
|
||||
QueryRunner() { queryRunner().getMember("query").getACall() = this }
|
||||
|
||||
override DataFlow::Node getAResult() { result = this }
|
||||
|
||||
override DataFlow::Node getAQueryArgument() { result = this.getArgument(0) }
|
||||
}
|
||||
|
||||
/** An expression that is passed to the `query` function and hence interpreted as SQL. */
|
||||
class QueryString extends SQL::SqlString {
|
||||
QueryString() {
|
||||
this = any(QueryRunner qr).getAQueryArgument() or
|
||||
this = any(QueryBuilderCall qb).getAQueryArgument()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user