Merge branch 'aegilops/js/insecure-helmet-middleware' of https://github.com/aegilops/codeql into aegilops/js/insecure-helmet-middleware

This commit is contained in:
aegilops
2024-06-07 15:50:15 +01:00
1359 changed files with 35767 additions and 22584 deletions

View File

@@ -1,6 +1,5 @@
load("@rules_pkg//pkg:mappings.bzl", "pkg_files")
load("@semmle_code//:dist.bzl", "dist")
load("@semmle_code//buildutils-internal:zipmerge.bzl", "zipmerge")
load("//misc/bazel:pkg.bzl", "codeql_pack")
package(default_visibility = ["//visibility:public"])
@@ -23,26 +22,25 @@ pkg_files(
strip_prefix = None,
)
dist(
name = "javascript-extractor-pack",
# We have to use a zip of the typescript parser wrapper, as it's generated by a genrule
# and we don't know a list of its output files.
codeql_pack(
name = "javascript",
srcs = [
":dbscheme-group",
"//javascript/downgrades",
"//javascript/externs",
"//javascript/extractor:tools-extractor",
"@semmle_code//language-packs/javascript:resources",
"//javascript/resources",
],
prefix = "javascript",
visibility = ["//visibility:public"],
zips = {"//javascript/extractor/lib/typescript": "tools"},
)
# We have to zipmerge in the typescript parser wrapper, as it's generated by a genrule
# and we don't know a list of its output files. Therefore, we sidestep the
# rules_pkg tooling here, and generate the zip for the language pack manually.
zipmerge(
name = "javascript",
srcs = [
":javascript-extractor-pack.zip",
"//javascript/extractor/lib/typescript",
],
out = "javascript.zip",
# TODO copy for internal repository backward compatibility
genrule(
name = "javascript.zip",
srcs = [":javascript-generic-zip"],
outs = ["javascript.zip"],
cmd = "cp $< $@",
)

View File

@@ -1,11 +1,12 @@
load("@semmle_code//:dist.bzl", "pack_zip")
load("//misc/bazel:pkg.bzl", "codeql_pkg_files", "strip_prefix")
pack_zip(
codeql_pkg_files(
name = "downgrades",
srcs = glob(
["**/*"],
exclude = ["BUILD.bazel"],
),
prefix = "downgrades",
visibility = ["//visibility:public"],
strip_prefix = strip_prefix.from_pkg(),
visibility = ["//javascript:__pkg__"],
)

View File

@@ -1,11 +1,12 @@
load("@semmle_code//:dist.bzl", "pack_zip")
load("//misc/bazel:pkg.bzl", "codeql_pkg_files", "strip_prefix")
pack_zip(
codeql_pkg_files(
name = "externs",
srcs = glob(
["**/*"],
exclude = ["BUILD.bazel"],
),
prefix = "tools/data/externs",
visibility = ["//visibility:public"],
strip_prefix = strip_prefix.from_pkg(),
visibility = ["//javascript:__pkg__"],
)

View File

@@ -1,5 +1,3 @@
load("@semmle_code//:common.bzl", "on_windows")
# Builds a zip file of the compiled typscript-parser-wrapper and its dependencies.
genrule(
name = "typescript",
@@ -33,19 +31,16 @@ genrule(
# Install again with only runtime deps
"$$NPM install --prod",
"mv node_modules build/",
"mkdir -p javascript/tools/typescript-parser-wrapper",
"mv build/* javascript/tools/typescript-parser-wrapper",
"mkdir -p typescript-parser-wrapper",
"mv build/* typescript-parser-wrapper",
"OUT=$$BAZEL_ROOT/$@",
"case $$OSTYPE in",
" cygwin|msys|win32) OUT=$$(cygpath -w $$OUT);;",
"esac",
"",
]) + on_windows(
" && ".join([
"$$BAZEL_ROOT/$(execpath @bazel_tools//tools/zip:zipper) cC $$(cygpath -w $$BAZEL_ROOT/$@) $$(find javascript -name '*' -print)",
"rm -rf $$TEMP",
]),
" && ".join([
"$$BAZEL_ROOT/$(execpath @bazel_tools//tools/zip:zipper) cC $$BAZEL_ROOT/$@ $$(find javascript -name '*' -print)",
"rm -rf $$TEMP",
]),
),
"$$BAZEL_ROOT/$(execpath @bazel_tools//tools/zip:zipper) cC $$OUT $$(find typescript-parser-wrapper -name '*' -print)",
"rm -rf $$TEMP",
]),
tools = [
"@bazel_tools//tools/zip:zipper",
"@nodejs//:node_bin",

View File

@@ -159,6 +159,7 @@ import com.semmle.util.trap.TrapWriter;
* <li>Files with base name "package.json" or "tsconfig.json", and files whose base name
* is of the form "codeql-javascript-*.json".
* <li>JavaScript, JSON or YAML files whose base name starts with ".eslintrc".
* <li>JSON files whose base name is ".xsaccess".
* <li>All extension-less files.
* </ul>
*
@@ -393,9 +394,10 @@ public class AutoBuild {
for (FileType filetype : defaultExtract)
for (String extension : filetype.getExtensions()) patterns.add("**/*" + extension);
// include .eslintrc files, package.json files, tsconfig.json files, and
// codeql-javascript-*.json files
// include .eslintrc files, .xsaccess files, package.json files,
// tsconfig.json files, and codeql-javascript-*.json files
patterns.add("**/.eslintrc*");
patterns.add("**/.xsaccess");
patterns.add("**/package.json");
patterns.add("**/tsconfig*.json");
patterns.add("**/codeql-javascript-*.json");
@@ -735,6 +737,7 @@ public class AutoBuild {
.collect(Collectors.toList());
filesToExtract = filesToExtract.stream()
.filter(p -> !isFileTooLarge(p))
.sorted(PATH_ORDERING)
.collect(Collectors.toCollection(() -> new LinkedHashSet<>()));
@@ -1010,6 +1013,15 @@ protected DependencyInstallationResult preparePackagesAndDependencies(Set<Path>
return config;
}
private boolean isFileTooLarge(Path f) {
long fileSize = f.toFile().length();
if (fileSize > 1_000_000L * this.maximumFileSizeInMegabytes) {
warn("Skipping " + f + " because it is too large (" + StringUtil.printFloat(fileSize / 1_000_000.0) + " MB). The limit is " + this.maximumFileSizeInMegabytes + " MB.");
return true;
}
return false;
}
private Set<Path> extractTypeScript(
Set<Path> files,
Set<Path> extractedFiles,
@@ -1051,9 +1063,10 @@ protected DependencyInstallationResult preparePackagesAndDependencies(Set<Path>
// compiler can parse them for us.
continue;
}
if (!extractedFiles.contains(sourcePath)) {
typeScriptFiles.add(sourcePath);
if (extractedFiles.contains(sourcePath)) {
continue;
}
typeScriptFiles.add(sourcePath);
}
typeScriptFiles.sort(PATH_ORDERING);
extractTypeScriptFiles(typeScriptFiles, extractedFiles, extractors);
@@ -1236,11 +1249,6 @@ protected DependencyInstallationResult preparePackagesAndDependencies(Set<Path>
warn("Skipping " + file + ", which does not exist.");
return;
}
long fileSize = f.length();
if (fileSize > 1_000_000L * this.maximumFileSizeInMegabytes) {
warn("Skipping " + file + " because it is too large (" + StringUtil.printFloat(fileSize / 1_000_000.0) + " MB). The limit is " + this.maximumFileSizeInMegabytes + " MB.");
return;
}
try {
long start = logBeginProcess("Extracting " + file);

View File

@@ -184,8 +184,8 @@ public class FileExtractor {
if (super.contains(f, lcExt, config)) return true;
// detect JSON-encoded configuration files whose name starts with `.` and ends with `rc`
// (e.g., `.eslintrc` or `.babelrc`)
if (f.isFile() && f.getName().matches("\\..*rc")) {
// (e.g., `.eslintrc` or `.babelrc`) as well as `.xsaccess` files
if (f.isFile() && f.getName().matches("\\..*rc|\\.xsaccess")) {
try (BufferedReader br = new BufferedReader(new FileReader(f))) {
// check whether the first two non-empty lines look like the start of a JSON object
// (two lines because the opening brace is usually on a line by itself)

View File

@@ -50,7 +50,7 @@ public class AllTests {
entry = zis.getNextEntry();
}
}
Path tsWrapper = tempDir.resolve("javascript/tools/typescript-parser-wrapper/main.js");
Path tsWrapper = tempDir.resolve("typescript-parser-wrapper/main.js");
if (!Files.exists(tsWrapper)) {
throw new RuntimeException("Could not find ts-wrapper at " + tsWrapper);
}

View File

@@ -0,0 +1,3 @@
{
"exposed": true // Expose data via http
}

View File

@@ -0,0 +1,22 @@
#10000=@"/.xsaccess;sourcefile"
files(#10000,"/.xsaccess")
#10001=@"/;folder"
folders(#10001,"/")
containerparent(#10001,#10000)
#10002=@"loc,{#10000},0,0,0,0"
locations_default(#10002,#10000,0,0,0,0)
hasLocation(#10000,#10002)
#20000=*
json(#20000,5,#10000,0,"{\n ""ex ... http\n}")
#20001=@"loc,{#10000},1,1,3,1"
locations_default(#20001,#10000,1,1,3,1)
json_locations(#20000,#20001)
#20002=*
json(#20002,1,#20000,0,"true")
#20003=@"loc,{#10000},2,14,2,17"
locations_default(#20003,#10000,2,14,2,17)
json_locations(#20002,#20003)
json_literals("true","true",#20002)
json_properties(#20000,"exposed",#20002)
numlines(#10000,3,0,0)
filetype(#10000,"json")

View File

@@ -0,0 +1 @@
These tests are still run with the legacy test runner

View File

@@ -1,3 +1,17 @@
## 1.0.0
### Breaking Changes
* CodeQL package management is now generally available, and all GitHub-produced CodeQL packages have had their version numbers increased to 1.0.0.
### Minor Analysis Improvements
* Additional heuristics for a new sensitive data classification for private information (e.g. credit card numbers) have been added to the shared `SensitiveDataHeuristics.qll` library. This may result in additional results for queries that use sensitive data such as `js/clear-text-storage-sensitive-data` and `js/clear-text-logging`.
### Bug Fixes
* Fixed a bug where very large TypeScript files would cause database creation to crash. Large files over 10MB were already excluded from analysis, but the file size check was not applied to TypeScript files.
## 0.9.1
No user-facing changes.

View File

@@ -1,4 +0,0 @@
---
category: minorAnalysis
---
* Additional heuristics for a new sensitive data classification for private information (e.g. credit card numbers) have been added to the shared `SensitiveDataHeuristics.qll` library. This may result in additional results for queries that use sensitive data such as `js/clear-text-storage-sensitive-data` and `js/clear-text-logging`.

View File

@@ -0,0 +1,13 @@
## 1.0.0
### Breaking Changes
* CodeQL package management is now generally available, and all GitHub-produced CodeQL packages have had their version numbers increased to 1.0.0.
### Minor Analysis Improvements
* Additional heuristics for a new sensitive data classification for private information (e.g. credit card numbers) have been added to the shared `SensitiveDataHeuristics.qll` library. This may result in additional results for queries that use sensitive data such as `js/clear-text-storage-sensitive-data` and `js/clear-text-logging`.
### Bug Fixes
* Fixed a bug where very large TypeScript files would cause database creation to crash. Large files over 10MB were already excluded from analysis, but the file size check was not applied to TypeScript files.

View File

@@ -1,2 +1,2 @@
---
lastReleaseVersion: 0.9.1
lastReleaseVersion: 1.0.0

View File

@@ -1,5 +1,5 @@
name: codeql/javascript-all
version: 0.9.2-dev
version: 1.0.1-dev
groups: javascript
dbscheme: semmlecode.javascript.dbscheme
extractor: javascript

View File

@@ -33,4 +33,10 @@ newtype TNode =
TExceptionalInvocationReturnNode(InvokeExpr e) or
TGlobalAccessPathRoot() or
TTemplatePlaceholderTag(Templating::TemplatePlaceholderTag tag) or
TReflectiveParametersNode(Function f)
TReflectiveParametersNode(Function f) or
TForbiddenRecursionGuard() {
none() and
// We want to prune irrelevant models before materialising data flow nodes, so types contributed
// directly from CodeQL must expose their pruning info without depending on data flow nodes.
(any(ModelInput::TypeModel tm).isTypeUsed("") implies any())
}

View File

@@ -168,9 +168,20 @@ module ModelInput {
* A unit class for adding additional type model rows from CodeQL models.
*/
class TypeModel extends Unit {
/**
* Holds if any of the other predicates in this class might have a result
* for the given `type`.
*
* The implementation of this predicate should not depend on `DataFlow::Node`.
*/
bindingset[type]
predicate isTypeUsed(string type) { none() }
/**
* Gets a data-flow node that is a source of the given `type`.
*
* Note that `type` should also be included in `isTypeUsed`.
*
* This must not depend on API graphs, but ensures that an API node is generated for
* the source.
*/
@@ -180,6 +191,8 @@ module ModelInput {
* Gets a data-flow node that is a sink of the given `type`,
* usually because it is an argument passed to a parameter of that type.
*
* Note that `type` should also be included in `isTypeUsed`.
*
* This must not depend on API graphs, but ensures that an API node is generated for
* the sink.
*/
@@ -188,6 +201,8 @@ module ModelInput {
/**
* Gets an API node that is a source or sink of the given `type`.
*
* Note that `type` should also be included in `isTypeUsed`.
*
* Unlike `getASource` and `getASink`, this may depend on API graphs.
*/
API::Node getAnApiNode(string type) { none() }
@@ -354,6 +369,28 @@ private predicate typeVariableModel(string name, string path) {
Extensions::typeVariableModel(name, path)
}
/**
* Holds if the given extension tuple `madId` should pretty-print as `model`.
*
* This predicate should only be used in tests.
*/
predicate interpretModelForTest(QlBuiltins::ExtensionId madId, string model) {
exists(string type, string path, string kind |
Extensions::sourceModel(type, path, kind, madId) and
model = "Source: " + type + "; " + path + "; " + kind
)
or
exists(string type, string path, string kind |
Extensions::sinkModel(type, path, kind, madId) and
model = "Sink: " + type + "; " + path + "; " + kind
)
or
exists(string type, string path, string input, string output, string kind |
Extensions::summaryModel(type, path, input, output, kind, madId) and
model = "Summary: " + type + "; " + path + "; " + input + "; " + output + "; " + kind
)
}
/**
* Holds if rows involving `type` might be relevant for the analysis of this database.
*/
@@ -367,6 +404,8 @@ predicate isRelevantType(string type) {
(
Specific::isTypeUsed(type)
or
any(TypeModel model).isTypeUsed(type)
or
exists(TestAllModels t)
)
or

View File

@@ -1,3 +1,9 @@
## 1.0.0
### Breaking Changes
* CodeQL package management is now generally available, and all GitHub-produced CodeQL packages have had their version numbers increased to 1.0.0.
## 0.8.16
No user-facing changes.

View File

@@ -0,0 +1,5 @@
## 1.0.0
### Breaking Changes
* CodeQL package management is now generally available, and all GitHub-produced CodeQL packages have had their version numbers increased to 1.0.0.

View File

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

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

View File

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

View File

@@ -0,0 +1,22 @@
passingPositiveTests
| PASSED | CommandInjection | tests.js:11:46:11:70 | // test ... jection |
| PASSED | CommandInjection | tests.js:12:43:12:67 | // test ... jection |
| PASSED | CommandInjection | tests.js:13:63:13:87 | // test ... jection |
| PASSED | CommandInjection | tests.js:14:62:14:86 | // test ... jection |
| PASSED | CommandInjection | tests.js:15:60:15:84 | // test ... jection |
| PASSED | CommandInjection | tests.js:17:45:17:69 | // test ... jection |
| PASSED | CommandInjection | tests.js:18:42:18:66 | // test ... jection |
| PASSED | CommandInjection | tests.js:19:62:19:86 | // test ... jection |
| PASSED | CommandInjection | tests.js:20:63:20:87 | // test ... jection |
| PASSED | CommandInjection | tests.js:21:60:21:84 | // test ... jection |
| PASSED | CommandInjection | tests.js:23:43:23:67 | // test ... jection |
| PASSED | CommandInjection | tests.js:24:40:24:64 | // test ... jection |
| PASSED | CommandInjection | tests.js:25:40:25:64 | // test ... jection |
| PASSED | CommandInjection | tests.js:26:60:26:84 | // test ... jection |
| PASSED | CommandInjection | tests.js:28:41:28:65 | // test ... jection |
| PASSED | CommandInjection | tests.js:29:58:29:82 | // test ... jection |
| PASSED | CommandInjection | tests.js:31:51:31:75 | // test ... jection |
| PASSED | CommandInjection | tests.js:32:68:32:92 | // test ... jection |
| PASSED | CommandInjection | tests.js:34:49:34:73 | // test ... jection |
| PASSED | CommandInjection | tests.js:35:66:35:90 | // test ... jection |
failingPositiveTests

View File

@@ -0,0 +1,36 @@
import { execa, execaSync, execaCommand, execaCommandSync, $ } from 'execa';
import http from 'node:http'
import url from 'url'
http.createServer(async function (req, res) {
let cmd = url.parse(req.url, true).query["cmd"][0];
let arg1 = url.parse(req.url, true).query["arg1"];
let arg2 = url.parse(req.url, true).query["arg2"];
let arg3 = url.parse(req.url, true).query["arg3"];
await $`${cmd} ${arg1} ${arg2} ${arg3}`; // test: CommandInjection
await $`ssh ${arg1} ${arg2} ${arg3}`; // test: CommandInjection
$({ shell: false }).sync`${cmd} ${arg1} ${arg2} ${arg3}`; // test: CommandInjection
$({ shell: true }).sync`${cmd} ${arg1} ${arg2} ${arg3}`; // test: CommandInjection
$({ shell: false }).sync`ssh ${arg1} ${arg2} ${arg3}`; // test: CommandInjection
$.sync`${cmd} ${arg1} ${arg2} ${arg3}`; // test: CommandInjection
$.sync`ssh ${arg1} ${arg2} ${arg3}`; // test: CommandInjection
await $({ shell: true })`${cmd} ${arg1} ${arg2} ${arg3}` // test: CommandInjection
await $({ shell: false })`${cmd} ${arg1} ${arg2} ${arg3}` // test: CommandInjection
await $({ shell: false })`ssh ${arg1} ${arg2} ${arg3}` // test: CommandInjection
await execa(cmd, [arg1, arg2, arg3]); // test: CommandInjection
await execa(cmd, { shell: true }); // test: CommandInjection
await execa(cmd, { shell: true }); // test: CommandInjection
await execa(cmd, [arg1, arg2, arg3], { shell: true }); // test: CommandInjection
execaSync(cmd, [arg1, arg2, arg3]); // test: CommandInjection
execaSync(cmd, [arg1, arg2, arg3], { shell: true }); // test: CommandInjection
await execaCommand(cmd + arg1 + arg2 + arg3); // test: CommandInjection
await execaCommand(cmd + arg1 + arg2 + arg3, { shell: true }); // test: CommandInjection
execaCommandSync(cmd + arg1 + arg2 + arg3); // test: CommandInjection
execaCommandSync(cmd + arg1 + arg2 + arg3, { shell: true }); // test: CommandInjection
});

View File

@@ -0,0 +1,38 @@
import javascript
class InlineTest extends LineComment {
string tests;
InlineTest() { tests = this.getText().regexpCapture("\\s*test:(.*)", 1) }
string getPositiveTest() {
result = tests.trim().splitAt(",").trim() and not result.matches("!%")
}
predicate hasPositiveTest(string test) { test = this.getPositiveTest() }
predicate inNode(DataFlow::Node n) {
this.getLocation().getFile() = n.getFile() and
this.getLocation().getStartLine() = n.getStartLine()
}
}
import experimental.semmle.javascript.Execa
query predicate passingPositiveTests(string res, string expectation, InlineTest t) {
res = "PASSED" and
t.hasPositiveTest(expectation) and
expectation = "CommandInjection" and
exists(SystemCommandExecution n |
t.inNode(n.getArgumentList()) or t.inNode(n.getACommandArgument())
)
}
query predicate failingPositiveTests(string res, string expectation, InlineTest t) {
res = "FAILED" and
t.hasPositiveTest(expectation) and
expectation = "CommandInjection" and
not exists(SystemCommandExecution n |
t.inNode(n.getArgumentList()) or t.inNode(n.getACommandArgument())
)
}

View File

@@ -0,0 +1,6 @@
passingPositiveTests
| PASSED | PathInjection | tests.js:9:43:9:64 | // test ... jection |
| PASSED | PathInjection | tests.js:12:50:12:71 | // test ... jection |
| PASSED | PathInjection | tests.js:15:61:15:82 | // test ... jection |
| PASSED | PathInjection | tests.js:18:73:18:94 | // test ... jection |
failingPositiveTests

View File

@@ -0,0 +1,19 @@
import { execa, $ } from 'execa';
import http from 'node:http'
import url from 'url'
http.createServer(async function (req, res) {
let filePath = url.parse(req.url, true).query["filePath"][0];
// Piping to stdin from a file
await $({ inputFile: filePath })`cat` // test: PathInjection
// Piping to stdin from a file
await execa('cat', { inputFile: filePath }); // test: PathInjection
// Piping Stdout to file
await execa('echo', ['example3']).pipeStdout(filePath); // test: PathInjection
// Piping all of command output to file
await execa('echo', ['example4'], { all: true }).pipeAll(filePath); // test: PathInjection
});

View File

@@ -0,0 +1,34 @@
import javascript
class InlineTest extends LineComment {
string tests;
InlineTest() { tests = this.getText().regexpCapture("\\s*test:(.*)", 1) }
string getPositiveTest() {
result = tests.trim().splitAt(",").trim() and not result.matches("!%")
}
predicate hasPositiveTest(string test) { test = this.getPositiveTest() }
predicate inNode(DataFlow::Node n) {
this.getLocation().getFile() = n.getFile() and
this.getLocation().getStartLine() = n.getStartLine()
}
}
import experimental.semmle.javascript.Execa
query predicate passingPositiveTests(string res, string expectation, InlineTest t) {
res = "PASSED" and
t.hasPositiveTest(expectation) and
expectation = "PathInjection" and
exists(FileSystemReadAccess n | t.inNode(n.getAPathArgument()))
}
query predicate failingPositiveTests(string res, string expectation, InlineTest t) {
res = "FAILED" and
t.hasPositiveTest(expectation) and
expectation = "PathInjection" and
not exists(FileSystemReadAccess n | t.inNode(n.getAPathArgument()))
}

View File

@@ -79,6 +79,7 @@ taintFlow
| test.js:269:10:269:31 | this.ba ... ource() | test.js:269:10:269:31 | this.ba ... ource() |
| test.js:272:6:272:40 | new MyS ... ource() | test.js:272:6:272:40 | new MyS ... ource() |
| test.js:274:6:274:39 | testlib ... eName() | test.js:274:6:274:39 | testlib ... eName() |
| test.js:277:8:277:31 | "danger ... .danger | test.js:277:8:277:31 | "danger ... .danger |
isSink
| test.js:54:18:54:25 | source() | test-sink |
| test.js:55:22:55:29 | source() | test-sink |

View File

@@ -11,6 +11,7 @@ extensions:
- ['testlib', 'Member[ParamDecoratorSource].DecoratedParameter', 'test-source']
- ['testlib', 'Member[getSource].ReturnValue', 'test-source']
- ['(testlib)', 'Member[parenthesizedPackageName].ReturnValue', 'test-source']
- ['danger-constant', 'Member[danger]', 'test-source']
- addsTo:
pack: codeql/javascript-all

View File

@@ -272,3 +272,9 @@ class MySubclass2 extends MySubclass {
sink(new MySubclass2().baseclassSource()); // NOT OK
sink(testlib.parenthesizedPackageName()); // NOT OK
function dangerConstant() {
sink("danger-constant".danger); // NOT OK
sink("danger-constant".safe); // OK
sink("danger-constant"); // OK
}

View File

@@ -2,6 +2,15 @@ import javascript
import testUtilities.ConsistencyChecking
import semmle.javascript.frameworks.data.internal.ApiGraphModels as ApiGraphModels
class TypeModelFromCodeQL extends ModelInput::TypeModel {
override predicate isTypeUsed(string type) { type = "danger-constant" }
override DataFlow::Node getASource(string type) {
type = "danger-constant" and
result.getStringValue() = "danger-constant"
}
}
class BasicTaintTracking extends TaintTracking::Configuration {
BasicTaintTracking() { this = "BasicTaintTracking" }

View File

@@ -0,0 +1,15 @@
load("//misc/bazel:pkg.bzl", "codeql_pkg_files")
codeql_pkg_files(
name = "resources",
srcs = glob(
["**/*"],
exclude = [
"tools/*.sh",
"BUILD.bazel",
],
),
exes = glob(["tools/*.sh"]),
strip_prefix = "",
visibility = ["//javascript:__pkg__"],
)

View File

@@ -0,0 +1,94 @@
name: "javascript"
aliases:
- javascript-typescript
- typescript
display_name: "JavaScript/TypeScript"
version: 1.22.1
column_kind: "utf16"
unicode_newlines: true
build_modes:
- none
file_coverage_languages:
- name: javascript
display_name: JavaScript
scc_languages:
- JavaScript
- name: typescript
display_name: TypeScript
scc_languages:
- TypeScript
- TypeScript Typings
github_api_languages:
- JavaScript
- TypeScript
scc_languages:
- JavaScript
- TypeScript
- TypeScript Typings
file_types:
- name: javascript
display_name: JavaScript
extensions:
- .js
- .jsx
- name: ecmascript
display_name: ECMAScript
extensions:
- .es
- .es6
- .mjs
- name: typescript
display_name: TypeScript
extensions:
- .ts
- .tsx
- name: html
display_name: HTML
extensions:
- .html
- .htm
- .xhtm
- .xhtml
- name: vue
display_name: Vue.js component
extensions:
- .vue
- name: data
display_name: Data or configuration files
extensions:
- .json
- .yml
- .yaml
- .raml
legacy_qltest_extraction: true
options:
trap:
title: TRAP options
description: Options about how the extractor handles TRAP files
type: object
visibility: 3
properties:
cache:
title: TRAP cache options
description: Options about how the extractor handles its TRAP cache
type: object
properties:
dir:
title: TRAP cache directory
description: The directory of the TRAP cache to use
type: string
bound:
title: TRAP cache bound
description: A soft limit (in MB) on the size of the TRAP cache
type: string
pattern: "[0-9]+"
write:
title: TRAP cache writeable
description: Whether to write to the TRAP cache as well as reading it
type: string
pattern: "(true|TRUE|false|FALSE)"
skip_types:
title: Skip type extraction for TypeScript
description: Whether to skip the extraction of types in a TypeScript application
type: string
pattern: "^(false|true)$"

View File

@@ -0,0 +1,30 @@
@echo off
SETLOCAL EnableDelayedExpansion
set jvm_args=-Xss16m
rem If CODEQL_RAM is set, use half for Java and half for TS.
if NOT [%CODEQL_RAM%] == [] (
set /a "half_ram=CODEQL_RAM/2"
set LGTM_TYPESCRIPT_RAM=%half_ram%
set jvm_args=!jvm_args! -Xmx!half_ram!m
)
rem If CODEQL_THREADS is set, propagate via LGTM_THREADS.
if NOT [%CODEQL_THREADS%] == [] (
set LGTM_THREADS=%CODEQL_THREADS%
)
rem The JS autobuilder expects to find typescript modules under SEMMLE_DIST/tools.
rem They are included in the pack, but we need to set SEMMLE_DIST appropriately.
set SEMMLE_DIST=%CODEQL_EXTRACTOR_JAVASCRIPT_ROOT%
rem The JS autobuilder expects LGTM_SRC to be set to the source root.
set LGTM_SRC=%CD%
type NUL && "%CODEQL_JAVA_HOME%\bin\java.exe" %jvm_args% ^
-cp "%CODEQL_EXTRACTOR_JAVASCRIPT_ROOT%\tools\extractor-javascript.jar" ^
com.semmle.js.extractor.AutoBuild
exit /b %ERRORLEVEL%
ENDLOCAL

View File

@@ -0,0 +1,29 @@
#!/bin/sh
set -eu
jvm_args=-Xss16m
# If CODEQL_RAM is set, use half for Java and half for TS.
if [ -n "${CODEQL_RAM:-}" ] ; then
half_ram="$(( CODEQL_RAM / 2 ))"
LGTM_TYPESCRIPT_RAM="$half_ram"
export LGTM_TYPESCRIPT_RAM
jvm_args="$jvm_args -Xmx${half_ram}m"
fi
# If CODEQL_THREADS is set, propagate via LGTM_THREADS.
if [ -n "${CODEQL_THREADS:-}" ] ; then
LGTM_THREADS="$CODEQL_THREADS"
export LGTM_THREADS
fi
# The JS autobuilder expects to find typescript modules under SEMMLE_DIST/tools.
# They are included in the pack, but we need to set SEMMLE_DIST appropriately.
# We want to word-split $jvm_args, so disable the shellcheck warning.
# shellcheck disable=SC2086
env SEMMLE_DIST="$CODEQL_EXTRACTOR_JAVASCRIPT_ROOT" \
LGTM_SRC="$(pwd)" \
"${CODEQL_JAVA_HOME}/bin/java" $jvm_args \
-cp "$CODEQL_EXTRACTOR_JAVASCRIPT_ROOT/tools/extractor-javascript.jar" \
com.semmle.js.extractor.AutoBuild

View File

@@ -0,0 +1,8 @@
{
"paths-ignore": [
"**/node_modules/**",
"**/bower_components/**",
"**/*.min.js",
"**/*-min.js"
]
}

View File

@@ -0,0 +1,2 @@
@echo off
type "%CODEQL_EXTRACTOR_JAVASCRIPT_ROOT%\tools\baseline-config.json"

View File

@@ -0,0 +1,3 @@
#!/bin/sh
cat "$CODEQL_EXTRACTOR_JAVASCRIPT_ROOT/tools/baseline-config.json"

View File

@@ -0,0 +1,4 @@
#!/bin/sh
echo "Not implemented." 1>&2
exit 1