Merge branch 'main' into amammad-js-CodeInjection_execa

This commit is contained in:
erik-krogh
2024-05-21 08:48:12 +02:00
8432 changed files with 829671 additions and 552232 deletions

View File

@@ -1,3 +1,75 @@
## 0.8.16
No user-facing changes.
## 0.8.15
### Minor Analysis Improvements
* The JavaScript extractor will on longer report syntax errors related to "strict mode".
Files containing such errors are now being fully analyzed along with other sources files.
This improves our support for source files that technically break the "strict mode" rules,
but where a build steps transforms the code such that it ends up working at runtime.
## 0.8.14
### Minor Analysis Improvements
* `API::Node#getInstance()` now includes instances of subclasses, include transitive subclasses.
The same changes applies to uses of the `Instance` token in data extensions.
## 0.8.13
### Query Metadata Changes
* The `@precision` of the `js/unsafe-external-link` has been reduced to `low` to reflect the fact that modern browsers do not expose the opening window for such links. This mitigates the potential security risk of having a link with `target="_blank"`.
### Minor Analysis Improvements
* The call graph has been improved, leading to more alerts for data flow based queries.
## 0.8.12
No user-facing changes.
## 0.8.11
No user-facing changes.
## 0.8.10
No user-facing changes.
## 0.8.9
### Bug Fixes
* The left operand of the `&&` operator no longer propagates data flow by default.
## 0.8.8
No user-facing changes.
## 0.8.7
### Minor Analysis Improvements
* Added support for [doT](https://github.com/olado/doT) templates.
## 0.8.6
No user-facing changes.
## 0.8.5
No user-facing changes.
## 0.8.4
### Minor Analysis Improvements
* Added django URLs to detected "safe" URL patterns in `js/unsafe-external-link`.
## 0.8.3
### Query Metadata Changes

View File

@@ -9,6 +9,14 @@ of the origin page using <code>window.opener</code> unless link type <code>noope
or <code>noreferrer</code> is specified. This is a potential security risk.
</p>
<p>
Note that only older browsers, where <code>target="_blank"</code> does not imply <code>rel="noopener"</code>,
are affected by this vulnerability. Modern browsers implicitly add <code>rel="noopener"</code> to
<code>target="_blank"</code> links.
Refer to the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#browser_compatibility">browser compatibility section
on the anchor element</a> for details on which browsers implicitly add <code>rel="noopener"</code> to <code>target="_blank"</code> links.
</p>
</overview>
<recommendation>

View File

@@ -10,7 +10,7 @@
* security
* external/cwe/cwe-200
* external/cwe/cwe-1022
* @precision very-high
* @precision low
*/
import javascript

View File

@@ -1,6 +1,6 @@
/**
* @name Successfully extracted files
* @description Lists all files in the source code directory that were extracted without encountering an error in the file.
* @name Extracted files
* @description Lists all files in the source code directory that were extracted.
* @kind diagnostic
* @id js/diagnostics/successfully-extracted-files
* @tags successfully-extracted-files
@@ -9,7 +9,5 @@
import javascript
from File f
where
not exists(Error e | e.isFatal() and e.getFile() = f) and
exists(f.getRelativePath())
where exists(f.getRelativePath())
select f, ""

View File

@@ -1,4 +0,0 @@
import codeql.typos.TypoDatabase as DB
/** DEPRECATED: Use the `codeql/typos` pack instead. */
deprecated predicate typos = DB::typos/2;

View File

@@ -12,7 +12,7 @@ import javascript
from Directive d
where
not d instanceof KnownDirective and
not d instanceof Directive::KnownDirective and
// ignore ":" pseudo-directive sometimes seen in dual-use shell/node.js scripts
not d.getExpr().getStringValue() = ":" and
// but exclude attribute top-levels: `<a href="javascript:'some-attribute-string'">`

View File

@@ -46,7 +46,8 @@ class SpuriousArguments extends Expr {
SpuriousArguments() {
this = invk.getArgument(maxArity(invk)).asExpr() and
not invk.isIncomplete()
not invk.isIncomplete() and
not invk.getAstNode() instanceof TaggedTemplateExpr
}
/**

View File

@@ -1,7 +0,0 @@
/**
* Provides predicates for reasoning about regular expressions
* that match URLs and hostname patterns.
*/
deprecated import semmle.javascript.security.regexp.HostnameRegexp as Dep
import Dep

View File

@@ -36,7 +36,7 @@
<p>
For JavaScript in the browser,
<code>RandomSource.getRandomValues</code> provides a cryptographically
<code>crypto.getRandomValues</code> provides a cryptographically
secure pseudo-random number generator.
</p>
@@ -69,7 +69,7 @@
<references>
<li>Wikipedia: <a href="http://en.wikipedia.org/wiki/Pseudorandom_number_generator">Pseudo-random number generator</a>.</li>
<li>Mozilla Developer Network: <a href="https://developer.mozilla.org/en-US/docs/Web/API/RandomSource/getRandomValues">RandomSource.getRandomValues</a>.</li>
<li>Mozilla Developer Network: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues">Crypto: getRandomValues()</a>.</li>
<li>NodeJS: <a href="https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback">crypto.randomBytes</a></li>
</references>
</qhelp>

View File

@@ -2,5 +2,7 @@ function securePassword() {
// GOOD: the random suffix is cryptographically secure
var suffix = window.crypto.getRandomValues(new Uint32Array(1))[0];
var password = "myPassword" + suffix;
return password;
// GOOD: if a random value between 0 and 1 is desired
var secret = window.crypto.getRandomValues(new Uint32Array(1))[0] * Math.pow(2,-32);
}

View File

@@ -19,6 +19,10 @@
If possible, store configuration files including credential data separately from the source code,
in a secure location with restricted access.
</p>
<p>
If the credentials are a placeholder value, make sure the value is obviously a placeholder by
using a name such as <code>"SampleToken"</code> or <code>"MyPassword"</code>.
</p>
</recommendation>
<example>

View File

@@ -30,7 +30,7 @@ where
// exclude dummy passwords and templates
not (
sink.getNode().(Sink).(DefaultCredentialsSink).getKind() =
["password", "credentials", "token"] and
["password", "credentials", "token", "key"] and
PasswordHeuristics::isDummyPassword(val)
or
sink.getNode().(Sink).getKind() = "authorization header" and

View File

@@ -6,6 +6,7 @@
* @kind metric
* @tags summary
* lines-of-code
* debug
* @id js/summary/lines-of-user-code
*/

View File

@@ -0,0 +1,3 @@
## 0.8.10
No user-facing changes.

View File

@@ -0,0 +1,3 @@
## 0.8.11
No user-facing changes.

View File

@@ -0,0 +1,3 @@
## 0.8.12
No user-facing changes.

View File

@@ -0,0 +1,9 @@
## 0.8.13
### Query Metadata Changes
* The `@precision` of the `js/unsafe-external-link` has been reduced to `low` to reflect the fact that modern browsers do not expose the opening window for such links. This mitigates the potential security risk of having a link with `target="_blank"`.
### Minor Analysis Improvements
* The call graph has been improved, leading to more alerts for data flow based queries.

View File

@@ -0,0 +1,6 @@
## 0.8.14
### Minor Analysis Improvements
* `API::Node#getInstance()` now includes instances of subclasses, include transitive subclasses.
The same changes applies to uses of the `Instance` token in data extensions.

View File

@@ -0,0 +1,8 @@
## 0.8.15
### Minor Analysis Improvements
* The JavaScript extractor will on longer report syntax errors related to "strict mode".
Files containing such errors are now being fully analyzed along with other sources files.
This improves our support for source files that technically break the "strict mode" rules,
but where a build steps transforms the code such that it ends up working at runtime.

View File

@@ -0,0 +1,3 @@
## 0.8.16
No user-facing changes.

View File

@@ -1,4 +1,5 @@
---
category: minorAnalysis
---
## 0.8.4
### Minor Analysis Improvements
* Added django URLs to detected "safe" URL patterns in `js/unsafe-external-link`.

View File

@@ -0,0 +1,3 @@
## 0.8.5
No user-facing changes.

View File

@@ -0,0 +1,3 @@
## 0.8.6
No user-facing changes.

View File

@@ -0,0 +1,5 @@
## 0.8.7
### Minor Analysis Improvements
* Added support for [doT](https://github.com/olado/doT) templates.

View File

@@ -0,0 +1,3 @@
## 0.8.8
No user-facing changes.

View File

@@ -0,0 +1,5 @@
## 0.8.9
### Bug Fixes
* The left operand of the `&&` operator no longer propagates data flow by default.

View File

@@ -1,2 +1,2 @@
---
lastReleaseVersion: 0.8.3
lastReleaseVersion: 0.8.16

View File

@@ -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>

View File

@@ -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"

View File

@@ -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()
)
)
}
}
}

View File

@@ -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);
});
});
}

View File

@@ -0,0 +1,8 @@
const tar = require("tar");
tar.x({
file: tarFileName,
strip: 1,
C: 'some-dir',
maxReadSize: 16 * 1024 * 1024 // 16 MB
})

View File

@@ -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);

View 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)
)
}
}
}

View File

@@ -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")
)
}

View File

@@ -9,7 +9,6 @@
*/
import javascript
import meta.MetaMetrics
private import Expressions.ExprHasNoEffect
import meta.internal.TaintMetrics

View File

@@ -1,5 +1,5 @@
name: codeql/javascript-queries
version: 0.8.4-dev
version: 0.8.17-dev
groups:
- javascript
- queries