mirror of
https://github.com/github/codeql.git
synced 2026-05-20 14:17:11 +02:00
Merge remote-tracking branch 'origin/master' into HEAD
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
+ semmlecode-javascript-queries/Security/CWE-094/CodeInjection.ql: /Security/CWE/CWE-094
|
||||
+ semmlecode-javascript-queries/Security/CWE-094/UnsafeDynamicMethodAccess.ql: /Security/CWE/CWE-094
|
||||
+ semmlecode-javascript-queries/Security/CWE-116/IncompleteSanitization.ql: /Security/CWE/CWE-116
|
||||
+ semmlecode-javascript-queries/Security/CWE-116/IncompleteHtmlAttributeSanitization.ql: /Security/CWE/CWE-116
|
||||
+ semmlecode-javascript-queries/Security/CWE-116/DoubleEscaping.ql: /Security/CWE/CWE-116
|
||||
+ semmlecode-javascript-queries/Security/CWE-134/TaintedFormatString.ql: /Security/CWE/CWE-134
|
||||
+ semmlecode-javascript-queries/Security/CWE-201/PostMessageStar.ql: /Security/CWE/CWE-201
|
||||
|
||||
@@ -5,7 +5,7 @@ Overview
|
||||
--------
|
||||
|
||||
This document presents an approach for running information flow analyses (such as the standard
|
||||
Semmle security queries) on an application that depends on one or more npm packages. Instead of
|
||||
security queries) on an application that depends on one or more npm packages. Instead of
|
||||
installing the npm packages during the snapshot build and analyzing them together with application
|
||||
code, we analyze each package in isolation and compute *flow summaries* that record information
|
||||
about any sources, sinks and flow steps contributed by the package's API. These flow summaries
|
||||
@@ -41,7 +41,7 @@ If the value of ``p`` can be controlled by an untrusted user, this would allow t
|
||||
folders, which may not be desirable.
|
||||
|
||||
By analyzing the application code base together with the source code for the ``mkdirp`` package,
|
||||
Semmle's default path injection analysis would be able to track taint through the call to ``mkdirp`` into its
|
||||
the default path injection analysis would be able to track taint through the call to ``mkdirp`` into its
|
||||
implementation, which ultimately uses built-in Node.js file system APIs to create the folder. Since
|
||||
the path injection analysis has built-in models of these APIs it would then be able to spot and flag this
|
||||
vulnerability.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "typescript-parser-wrapper",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"typescript": "3.8.2"
|
||||
"typescript": "3.9.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.json",
|
||||
|
||||
@@ -4,31 +4,31 @@
|
||||
|
||||
"@types/node@12.7.11":
|
||||
version "12.7.11"
|
||||
resolved "node-12.7.11.tgz#be879b52031cfb5d295b047f5462d8ef1a716446"
|
||||
resolved node-12.7.11.tgz#be879b52031cfb5d295b047f5462d8ef1a716446
|
||||
|
||||
ansi-regex@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
|
||||
resolved ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df
|
||||
|
||||
ansi-styles@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
|
||||
resolved ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe
|
||||
|
||||
ansi-styles@^3.1.0:
|
||||
version "3.2.0"
|
||||
resolved "ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88"
|
||||
resolved ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88
|
||||
dependencies:
|
||||
color-convert "^1.9.0"
|
||||
|
||||
argparse@^1.0.7:
|
||||
version "1.0.9"
|
||||
resolved "argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86"
|
||||
resolved argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86
|
||||
dependencies:
|
||||
sprintf-js "~1.0.2"
|
||||
|
||||
babel-code-frame@^6.22.0:
|
||||
version "6.26.0"
|
||||
resolved "babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
|
||||
resolved babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b
|
||||
dependencies:
|
||||
chalk "^1.1.3"
|
||||
esutils "^2.0.2"
|
||||
@@ -36,22 +36,22 @@ babel-code-frame@^6.22.0:
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
resolved balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.8"
|
||||
resolved "brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
|
||||
resolved brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
builtin-modules@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
|
||||
resolved builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f
|
||||
|
||||
chalk@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
|
||||
resolved chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98
|
||||
dependencies:
|
||||
ansi-styles "^2.2.1"
|
||||
escape-string-regexp "^1.0.2"
|
||||
@@ -61,7 +61,7 @@ chalk@^1.1.3:
|
||||
|
||||
chalk@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba"
|
||||
resolved chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba
|
||||
dependencies:
|
||||
ansi-styles "^3.1.0"
|
||||
escape-string-regexp "^1.0.5"
|
||||
@@ -69,45 +69,45 @@ chalk@^2.3.0:
|
||||
|
||||
color-convert@^1.9.0:
|
||||
version "1.9.1"
|
||||
resolved "color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed"
|
||||
resolved color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed
|
||||
dependencies:
|
||||
color-name "^1.1.1"
|
||||
|
||||
color-name@^1.1.1:
|
||||
version "1.1.3"
|
||||
resolved "color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||
resolved color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25
|
||||
|
||||
commander@^2.12.1:
|
||||
version "2.13.0"
|
||||
resolved "commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
|
||||
resolved commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
resolved concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b
|
||||
|
||||
diff@^3.2.0:
|
||||
version "3.4.0"
|
||||
resolved "diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c"
|
||||
resolved diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c
|
||||
|
||||
escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
resolved escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4
|
||||
|
||||
esprima@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
|
||||
resolved esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804
|
||||
|
||||
esutils@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
|
||||
resolved esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b
|
||||
|
||||
fs.realpath@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
resolved fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f
|
||||
|
||||
glob@^7.1.1:
|
||||
version "7.1.2"
|
||||
resolved "glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
|
||||
resolved glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
@@ -118,93 +118,93 @@ glob@^7.1.1:
|
||||
|
||||
has-ansi@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
|
||||
resolved has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91
|
||||
dependencies:
|
||||
ansi-regex "^2.0.0"
|
||||
|
||||
has-flag@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
|
||||
resolved has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
resolved inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9
|
||||
dependencies:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2:
|
||||
version "2.0.3"
|
||||
resolved "inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||
resolved inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de
|
||||
|
||||
js-tokens@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
||||
resolved js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b
|
||||
|
||||
js-yaml@^3.7.0:
|
||||
version "3.10.0"
|
||||
resolved "js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc"
|
||||
resolved js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc
|
||||
dependencies:
|
||||
argparse "^1.0.7"
|
||||
esprima "^4.0.0"
|
||||
|
||||
minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
resolved minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
once@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
resolved once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
path-is-absolute@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
resolved path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f
|
||||
|
||||
path-parse@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
|
||||
resolved path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1
|
||||
|
||||
resolve@^1.3.2:
|
||||
version "1.5.0"
|
||||
resolved "resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36"
|
||||
resolved resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36
|
||||
dependencies:
|
||||
path-parse "^1.0.5"
|
||||
|
||||
semver@^5.3.0:
|
||||
version "5.5.0"
|
||||
resolved "semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
|
||||
resolved semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab
|
||||
|
||||
sprintf-js@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||
resolved sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c
|
||||
|
||||
strip-ansi@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
|
||||
resolved strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf
|
||||
dependencies:
|
||||
ansi-regex "^2.0.0"
|
||||
|
||||
supports-color@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
|
||||
resolved supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7
|
||||
|
||||
supports-color@^4.0.0:
|
||||
version "4.5.0"
|
||||
resolved "supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b"
|
||||
resolved supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b
|
||||
dependencies:
|
||||
has-flag "^2.0.0"
|
||||
|
||||
tslib@^1.8.0, tslib@^1.8.1:
|
||||
version "1.9.0"
|
||||
resolved "tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8"
|
||||
resolved tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8
|
||||
|
||||
tslint@^5.9.1:
|
||||
version "5.9.1"
|
||||
resolved "tslint-5.9.1.tgz#1255f87a3ff57eb0b0e1f0e610a8b4748046c9ae"
|
||||
resolved tslint-5.9.1.tgz#1255f87a3ff57eb0b0e1f0e610a8b4748046c9ae
|
||||
dependencies:
|
||||
babel-code-frame "^6.22.0"
|
||||
builtin-modules "^1.1.1"
|
||||
@@ -221,14 +221,14 @@ tslint@^5.9.1:
|
||||
|
||||
tsutils@^2.12.1:
|
||||
version "2.19.1"
|
||||
resolved "tsutils-2.19.1.tgz#76d7ebdea9d7a7bf4a05f50ead3701b0168708d7"
|
||||
resolved tsutils-2.19.1.tgz#76d7ebdea9d7a7bf4a05f50ead3701b0168708d7
|
||||
dependencies:
|
||||
tslib "^1.8.1"
|
||||
|
||||
typescript@3.8.2:
|
||||
version "3.8.2"
|
||||
resolved typescript-3.8.2.tgz#91d6868aaead7da74f493c553aeff76c0c0b1d5a
|
||||
typescript@3.9.2:
|
||||
version "3.9.2"
|
||||
resolved typescript-3.9.2.tgz#64e9c8e9be6ea583c54607677dd4680a1cf35db9
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
resolved wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f
|
||||
|
||||
@@ -77,8 +77,8 @@ import com.semmle.util.trap.TrapWriter;
|
||||
* <li><code>LGTM_INDEX_EXCLUDE</code>: a newline-separated list of paths to exclude
|
||||
* <li><code>LGTM_REPOSITORY_FOLDERS_CSV</code>: the path of a CSV file containing file
|
||||
* classifications
|
||||
* <li><code>LGTM_INDEX_FILTERS</code>: a newline-separated list of {@link ProjectLayout}-style
|
||||
* patterns that can be used to refine the list of files to include and exclude
|
||||
* <li><code>LGTM_INDEX_FILTERS</code>: a newline-separated list of strings of form "include:PATTERN"
|
||||
* or "exclude:PATTERN" that can be used to refine the list of files to include and exclude.
|
||||
* <li><code>LGTM_INDEX_TYPESCRIPT</code>: whether to extract TypeScript
|
||||
* <li><code>LGTM_INDEX_FILETYPES</code>: a newline-separated list of ".extension:filetype" pairs
|
||||
* specifying which {@link FileType} to use for the given extension; the additional file type
|
||||
|
||||
@@ -146,7 +146,7 @@ public class ExtractionMetrics {
|
||||
|
||||
public void stopPhase(
|
||||
ExtractionPhase
|
||||
event /* technically not needed, but useful for documentation and sanity checking */) {
|
||||
event /* technically not needed, but useful for documentation and consistency checking */) {
|
||||
if (stack.isEmpty()) {
|
||||
failTimings(
|
||||
String.format(
|
||||
|
||||
@@ -1588,7 +1588,16 @@ public class TypeScriptASTConverter {
|
||||
}
|
||||
|
||||
private Node convertLiteralType(JsonObject node, SourceLocation loc) throws ParseError {
|
||||
return convertChild(node, "literal");
|
||||
Node literal = convertChild(node, "literal");
|
||||
// Convert a negated literal to a negative number
|
||||
if (literal instanceof UnaryExpression) {
|
||||
UnaryExpression unary = (UnaryExpression) literal;
|
||||
if (unary.getOperator().equals("-") && unary.getArgument() instanceof Literal) {
|
||||
Literal arg = (Literal) unary.getArgument();
|
||||
literal = new Literal(loc, arg.getTokenType(), "-" + arg.getValue());
|
||||
}
|
||||
}
|
||||
return literal;
|
||||
}
|
||||
|
||||
private Node convertMappedType(JsonObject node, SourceLocation loc) throws ParseError {
|
||||
|
||||
@@ -86,6 +86,12 @@ public class TypeScriptParser {
|
||||
*/
|
||||
public static final String TYPESCRIPT_TIMEOUT_VAR = "SEMMLE_TYPESCRIPT_TIMEOUT";
|
||||
|
||||
/**
|
||||
* An environment variable that can be set to specify a number of retries when verifying
|
||||
* the TypeScript installation. Default is 3.
|
||||
*/
|
||||
public static final String TYPESCRIPT_RETRIES_VAR = "SEMMLE_TYPESCRIPT_RETRIES";
|
||||
|
||||
/**
|
||||
* An environment variable (without the <tt>SEMMLE_</tt> or <tt>LGTM_</tt> prefix), that can be
|
||||
* set to indicate the maximum heap space usable by the Node.js process, in addition to its
|
||||
@@ -179,9 +185,6 @@ public class TypeScriptParser {
|
||||
public String verifyNodeInstallation() {
|
||||
if (nodeJsVersionString != null) return nodeJsVersionString;
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ByteArrayOutputStream err = new ByteArrayOutputStream();
|
||||
|
||||
// Determine where to find the Node.js runtime.
|
||||
String explicitNodeJsRuntime = Env.systemEnv().get(TYPESCRIPT_NODE_RUNTIME_VAR);
|
||||
if (explicitNodeJsRuntime != null) {
|
||||
@@ -198,12 +201,41 @@ public class TypeScriptParser {
|
||||
nodeJsRuntimeExtraArgs = Arrays.asList(extraArgs.split("\\s+"));
|
||||
}
|
||||
|
||||
// Run 'node --version' with a timeout, and retry a few times if it times out.
|
||||
// If the Java process is suspended we may get a spurious timeout, and we want to
|
||||
// support long suspensions in cloud environments. Instead of setting a huge timeout,
|
||||
// retrying guarantees we can survive arbitrary suspensions as long as they don't happen
|
||||
// too many times in rapid succession.
|
||||
int timeout = Env.systemEnv().getInt(TYPESCRIPT_TIMEOUT_VAR, 10000);
|
||||
int numRetries = Env.systemEnv().getInt(TYPESCRIPT_RETRIES_VAR, 3);
|
||||
for (int i = 0; i < numRetries - 1; ++i) {
|
||||
try {
|
||||
return startNodeAndGetVersion(timeout);
|
||||
} catch (InterruptedError e) {
|
||||
Exceptions.ignore(e, "We will retry the call that caused this exception.");
|
||||
System.err.println("Starting Node.js seems to take a long time. Retrying.");
|
||||
}
|
||||
}
|
||||
try {
|
||||
return startNodeAndGetVersion(timeout);
|
||||
} catch (InterruptedError e) {
|
||||
Exceptions.ignore(e, "Exception details are not important.");
|
||||
throw new CatastrophicError(
|
||||
"Could not start Node.js (timed out after " + (timeout / 1000) + "s and " + numRetries + " attempts");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that Node.js is installed and can be run and returns its version string.
|
||||
*/
|
||||
private String startNodeAndGetVersion(int timeout) throws InterruptedError {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ByteArrayOutputStream err = new ByteArrayOutputStream();
|
||||
Builder b =
|
||||
new Builder(
|
||||
getNodeJsRuntimeInvocation("--version"), out, err, getParserWrapper().getParentFile());
|
||||
b.expectFailure(); // We want to do our own logging in case of an error.
|
||||
|
||||
int timeout = Env.systemEnv().getInt(TYPESCRIPT_TIMEOUT_VAR, 10000);
|
||||
try {
|
||||
int r = b.execute(timeout);
|
||||
String stdout = new String(out.toByteArray());
|
||||
@@ -213,10 +245,6 @@ public class TypeScriptParser {
|
||||
"Could not start Node.js. It is required for TypeScript extraction.\n" + stderr);
|
||||
}
|
||||
return nodeJsVersionString = stdout;
|
||||
} catch (InterruptedError e) {
|
||||
Exceptions.ignore(e, "Exception details are not important.");
|
||||
throw new CatastrophicError(
|
||||
"Could not start Node.js (timed out after " + (timeout / 1000) + "s).");
|
||||
} catch (ResourceError e) {
|
||||
// In case 'node' is not found, the process builder converts the IOException
|
||||
// into a ResourceError.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @description An AngularJS event listener that listens for a non-existent event has no effect.
|
||||
* @kind problem
|
||||
* @problem.severity warning
|
||||
* @precision medium
|
||||
* @precision low
|
||||
* @id js/angular/dead-event-listener
|
||||
* @tags correctness
|
||||
* frameworks/angularjs
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @description Unused dependencies are confusing, and should be removed.
|
||||
* @kind problem
|
||||
* @problem.severity recommendation
|
||||
* @precision high
|
||||
* @precision low
|
||||
* @id js/angular/unused-dependency
|
||||
* @tags maintainability
|
||||
* frameworks/angularjs
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* @tags maintainability
|
||||
* correctness
|
||||
* external/cwe/cwe-758
|
||||
* @precision medium
|
||||
* @precision low
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @tags maintainability
|
||||
* correctness
|
||||
* external/cwe/cwe-563
|
||||
* @precision medium
|
||||
* @precision low
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* @id js/too-many-parameters
|
||||
* @tags testability
|
||||
* readability
|
||||
* @precision high
|
||||
* @precision low
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
@@ -108,7 +108,7 @@ predicate signaturesMatch(MethodSignature method, MethodSignature other) {
|
||||
method.getBody().getThisTypeAnnotation().getType() =
|
||||
other.getBody().getThisTypeAnnotation().getType()
|
||||
) and
|
||||
// The types are compared in matchingCallSignature. This is sanity-check that the textual representation of the type-annotations are somewhat similar.
|
||||
// The types are compared in matchingCallSignature. This is a consistency check that the textual representation of the type-annotations are somewhat similar.
|
||||
forall(int i | i in [0 .. -1 + method.getBody().getNumParameter()] |
|
||||
getParameterTypeAnnotation(method, i) = getParameterTypeAnnotation(other, i)
|
||||
) and
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* @problem.severity recommendation
|
||||
* @id js/unused-property
|
||||
* @tags maintainability
|
||||
* @precision high
|
||||
* @precision low
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* @id js/bitwise-sign-check
|
||||
* @tags reliability
|
||||
* correctness
|
||||
* @precision medium
|
||||
* @precision low
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* convention
|
||||
* external/cwe/cwe-570
|
||||
* external/cwe/cwe-571
|
||||
* @precision medium
|
||||
* @precision low
|
||||
*/
|
||||
|
||||
import Clones
|
||||
|
||||
@@ -158,5 +158,11 @@ predicate hasNoEffect(Expr e) {
|
||||
// exclude block-level flow type annotations. For example: `(name: empty)`.
|
||||
not e.(ParExpr).getExpression().getLastToken().getNextToken().getValue() = ":" and
|
||||
// exclude the first statement of a try block
|
||||
not e = any(TryStmt stmt).getBody().getStmt(0).(ExprStmt).getExpr()
|
||||
not e = any(TryStmt stmt).getBody().getStmt(0).(ExprStmt).getExpr() and
|
||||
// exclude expressions that are alone in a file, and file doesn't contain a function.
|
||||
not exists(TopLevel top |
|
||||
top = e.getParent().(ExprStmt).getParent() and
|
||||
top.getNumChild() = 1 and
|
||||
not exists(Function fun | fun.getEnclosingContainer() = top)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -162,7 +162,26 @@ abstract class NullOrUndefinedConversion extends ImplicitConversion {
|
||||
class PlusConversion extends NullOrUndefinedConversion {
|
||||
PlusConversion() { parent instanceof AddExpr or parent instanceof AssignAddExpr }
|
||||
|
||||
override string getConversionTarget() { result = "number or string" }
|
||||
override string getConversionTarget() {
|
||||
result = getDefiniteSiblingType()
|
||||
or
|
||||
not exists(getDefiniteSiblingType()) and
|
||||
result = "number or string"
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sibling of this implicit conversion.
|
||||
* E.g. if this is `a` in the expression `a + b`, then the sibling is `b`.
|
||||
*/
|
||||
private Expr getSibling() { result = parent.getAChild() and not result = this.getEnclosingExpr() }
|
||||
|
||||
/**
|
||||
* Gets the unique type of the sibling expression, if that type is `string` or `number`.
|
||||
*/
|
||||
private string getDefiniteSiblingType() {
|
||||
result = unique(InferredType t | t = getSibling().flow().analyze().getAType()).getTypeofTag() and
|
||||
result = ["string", "number"]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* @id js/misspelled-identifier
|
||||
* @tags maintainability
|
||||
* readability
|
||||
* @precision high
|
||||
* @precision low
|
||||
*/
|
||||
|
||||
import Misspelling
|
||||
|
||||
@@ -14,6 +14,37 @@
|
||||
|
||||
import Misspelling
|
||||
|
||||
from GlobalVarAccess gva, VarDecl lvd
|
||||
where misspelledVariableName(gva, lvd)
|
||||
select gva, "'" + gva + "' may be a typo for variable $@.", lvd, lvd.getName()
|
||||
/**
|
||||
* Gets the number of times a local variable with name `name` occurs in the program.
|
||||
*/
|
||||
bindingset[name]
|
||||
int localAcceses(string name) {
|
||||
result = count(VarAccess acc | acc.getName() = name and not acc instanceof GlobalVarAccess)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of times a global variable with name `name` occurs in the program.
|
||||
*/
|
||||
bindingset[name]
|
||||
int globalAccesses(string name) { result = count(GlobalVarAccess acc | acc.getName() = name) }
|
||||
|
||||
/**
|
||||
* Holds if our heuristic says that the local variable `lvd` seems to be a misspelling of the global variable `gva`.
|
||||
* Otherwise the global variable is likely the misspelling.
|
||||
*/
|
||||
predicate globalIsLikelyCorrect(GlobalVarAccess gva, VarDecl lvd) {
|
||||
// If there are more occurrences of the global (by a margin of at least 2), and the local is missing one letter compared to the global.
|
||||
globalAccesses(gva.getName()) >= localAcceses(lvd.getName()) + 2 and
|
||||
lvd.getName().length() = gva.getName().length() - 1
|
||||
or
|
||||
// Or if there are many more of the global.
|
||||
globalAccesses(gva.getName()) > 2 * localAcceses(lvd.getName()) + 2
|
||||
}
|
||||
|
||||
from GlobalVarAccess gva, VarDecl lvd, string msg
|
||||
where
|
||||
misspelledVariableName(gva, lvd) and
|
||||
if globalIsLikelyCorrect(gva, lvd)
|
||||
then msg = "$@ may be a typo for '" + gva + "'."
|
||||
else msg = "'" + gva + "' may be a typo for variable $@."
|
||||
select gva, msg, lvd, lvd.getName()
|
||||
|
||||
@@ -16,5 +16,7 @@ where
|
||||
// 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'">`
|
||||
not d.getParent() instanceof CodeInAttribute
|
||||
not d.getParent() instanceof CodeInAttribute and
|
||||
// exclude babel generated directives like "@babel/helpers - typeof".
|
||||
not d.getDirectiveText().prefix(14) = "@babel/helpers"
|
||||
select d, "Unknown directive: '" + truncate(d.getDirectiveText(), 20, " ... (truncated)") + "'."
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* @tags maintainability
|
||||
* readability
|
||||
* documentation
|
||||
* @precision high
|
||||
* @precision low
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* @tags maintainability
|
||||
* readability
|
||||
* documentation
|
||||
* @precision high
|
||||
* @precision low
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* @tags maintainability
|
||||
* readability
|
||||
* documentation
|
||||
* @precision high
|
||||
* @precision low
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @tags maintainability
|
||||
* readability
|
||||
* language-features
|
||||
* @precision high
|
||||
* @precision low
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
@@ -8,6 +8,11 @@ If the same pattern variable is bound multiple times in the same object or array
|
||||
binding overwrites all of the earlier ones. This is most likely unintended and should be avoided.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
In TypeScript, a common mistake is to try to write type annotations inside a pattern. This is not
|
||||
possible, and the type annotation should come after the pattern.
|
||||
</p>
|
||||
|
||||
</overview>
|
||||
<recommendation>
|
||||
|
||||
@@ -34,6 +39,21 @@ From context, it appears that the second binding should have been for variable <
|
||||
|
||||
<sample src="examples/NonLinearPatternGood.js" />
|
||||
|
||||
<p>
|
||||
This can sometimes happen in TypeScript, due to the apparant similarity between property patterns
|
||||
and type annotations. In the following example, the function uses a pattern parameter with properties <code>x</code>
|
||||
and <code>y</code>. These appear to have type <code>number</code>, but are in fact untyped properties both stored in a variable named <code>number</code>.
|
||||
</p>
|
||||
|
||||
<sample src="examples/NonLinearPatternTS.ts" />
|
||||
|
||||
<p>
|
||||
It is not possible to specify type annotations inside a pattern. The correct way is to specify the type
|
||||
after the parameter:
|
||||
</p>
|
||||
|
||||
<sample src="examples/NonLinearPatternTSGood.ts" />
|
||||
|
||||
</example>
|
||||
<references>
|
||||
<li>Mozilla Developer Network: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment">Destructuring assignment</a>.</li>
|
||||
|
||||
@@ -14,12 +14,55 @@
|
||||
|
||||
import javascript
|
||||
|
||||
from BindingPattern p, string n, VarDecl v, VarDecl w
|
||||
class RootDestructuringPattern extends BindingPattern {
|
||||
RootDestructuringPattern() {
|
||||
this instanceof DestructuringPattern and
|
||||
not this = any(PropertyPattern p).getValuePattern() and
|
||||
not this = any(ArrayPattern p).getAnElement()
|
||||
}
|
||||
|
||||
/** Holds if this pattern has multiple bindings for `name`. */
|
||||
predicate hasConflictingBindings(string name) {
|
||||
exists(VarRef v, VarRef w |
|
||||
v = getABindingVarRef() and
|
||||
w = getABindingVarRef() and
|
||||
name = v.getName() and
|
||||
name = w.getName() and
|
||||
v != w
|
||||
)
|
||||
}
|
||||
|
||||
/** Gets the first occurrence of the conflicting binding `name`. */
|
||||
VarDecl getFirstClobberedVarDecl(string name) {
|
||||
hasConflictingBindings(name) and
|
||||
result =
|
||||
min(VarDecl decl |
|
||||
decl = getABindingVarRef() and decl.getName() = name
|
||||
|
|
||||
decl order by decl.getLocation().getStartLine(), decl.getLocation().getStartColumn()
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if variables in this pattern may resemble type annotations. */
|
||||
predicate resemblesTypeAnnotation() {
|
||||
hasConflictingBindings(_) and // Restrict size of predicate.
|
||||
this instanceof Parameter and
|
||||
this instanceof ObjectPattern and
|
||||
not exists(getTypeAnnotation()) and
|
||||
getFile().getFileType().isTypeScript()
|
||||
}
|
||||
}
|
||||
|
||||
from RootDestructuringPattern p, string n, VarDecl v, VarDecl w, string message
|
||||
where
|
||||
v = p.getABindingVarRef() and
|
||||
v = p.getFirstClobberedVarDecl(n) and
|
||||
w = p.getABindingVarRef() and
|
||||
v.getName() = n and
|
||||
w.getName() = n and
|
||||
v != w and
|
||||
v.getLocation().startsBefore(w.getLocation())
|
||||
select w, "Repeated binding of pattern variable '" + n + "' previously bound $@.", v, "here"
|
||||
if p.resemblesTypeAnnotation()
|
||||
then
|
||||
message =
|
||||
"The pattern variable '" + n +
|
||||
"' appears to be a type, but is a variable previously bound $@."
|
||||
else message = "Repeated binding of pattern variable '" + n + "' previously bound $@."
|
||||
select w, message, v, "here"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* @tags reliability
|
||||
* maintainability
|
||||
* language-features
|
||||
* @precision high
|
||||
* @precision low
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* @id js/json-in-javascript-file
|
||||
* @tags maintainability
|
||||
* language-features
|
||||
* @precision high
|
||||
* @precision low
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
function distance({x: number, y: number}) {
|
||||
return Math.sqrt(x*x + y*y);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
function distance({x, y}: {x: number, y: number}) {
|
||||
return Math.sqrt(x*x + y*y);
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
* @tags reliability
|
||||
* maintainability
|
||||
* frameworks/node.js
|
||||
* @precision medium
|
||||
* @precision low
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @description If unnecessary package dependencies are included in package.json, the
|
||||
* package will become harder to install.
|
||||
* @kind problem
|
||||
* @problem.severity warning
|
||||
* @problem.severity recommendation
|
||||
* @id js/node/unused-npm-dependency
|
||||
* @tags maintainability
|
||||
* frameworks/node.js
|
||||
|
||||
@@ -27,11 +27,10 @@ class DangerousScheme extends string {
|
||||
/** Returns a node that refers to the scheme of `url`. */
|
||||
DataFlow::SourceNode schemeOf(DataFlow::Node url) {
|
||||
// url.split(":")[0]
|
||||
exists(DataFlow::MethodCallNode split |
|
||||
split.getMethodName() = "split" and
|
||||
split.getArgument(0).getStringValue() = ":" and
|
||||
result = split.getAPropertyRead("0") and
|
||||
url = split.getReceiver()
|
||||
exists(StringSplitCall split |
|
||||
split.getSeparator() = ":" and
|
||||
result = split.getASubstringRead(0) and
|
||||
url = split.getBaseString()
|
||||
)
|
||||
or
|
||||
// url.getScheme(), url.getProtocol(), getScheme(url), getProtocol(url)
|
||||
@@ -62,18 +61,14 @@ DataFlow::Node schemeCheck(DataFlow::Node nd, DangerousScheme scheme) {
|
||||
sw.getSubstring().mayHaveStringValue(scheme)
|
||||
)
|
||||
or
|
||||
// check of the form `array.includes(getScheme(nd))`
|
||||
exists(InclusionTest test, DataFlow::ArrayCreationNode array | test = result |
|
||||
schemeOf(nd).flowsTo(test.getContainedNode()) and
|
||||
array.flowsTo(test.getContainerNode()) and
|
||||
array.getAnElement().mayHaveStringValue(scheme.getWithOrWithoutColon())
|
||||
)
|
||||
or
|
||||
// check of the form `getScheme(nd) === scheme`
|
||||
exists(EqualityTest test, Expr op1, Expr op2 | test.flow() = result |
|
||||
test.hasOperands(op1, op2) and
|
||||
schemeOf(nd).flowsToExpr(op1) and
|
||||
op2.mayHaveStringValue(scheme.getWithOrWithoutColon())
|
||||
exists(MembershipCandidate candidate |
|
||||
result = candidate.getTest()
|
||||
or
|
||||
// fall back to the candidate if the test itself is implicit
|
||||
not exists(candidate.getTest()) and result = candidate
|
||||
|
|
||||
candidate.getAMemberString() = scheme.getWithOrWithoutColon() and
|
||||
schemeOf(nd).flowsTo(candidate)
|
||||
)
|
||||
or
|
||||
// propagate through trimming, case conversion, and regexp replace
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>
|
||||
|
||||
Dynamically constructing a shell command with inputs from exported
|
||||
functions may inadvertently change the meaning of the shell command.
|
||||
|
||||
Clients using the exported function may use inputs containing
|
||||
characters that the shell interprets in a special way, for instance
|
||||
quotes and spaces.
|
||||
|
||||
This can result in the shell command misbehaving, or even
|
||||
allowing a malicious user to execute arbitrary commands on the system.
|
||||
</p>
|
||||
|
||||
|
||||
</overview>
|
||||
<recommendation>
|
||||
|
||||
<p>
|
||||
If possible, provide the dynamic arguments to the shell as an array
|
||||
using a safe API such as <code>child_process.execFile</code> to avoid
|
||||
interpretation by the shell.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Alternatively, if the shell command must be constructed
|
||||
dynamically, then add code to ensure that special characters
|
||||
do not alter the shell command unexpectedly.
|
||||
</p>
|
||||
|
||||
</recommendation>
|
||||
<example>
|
||||
|
||||
<p>
|
||||
The following example shows a dynamically constructed shell
|
||||
command that downloads a file from a remote URL.
|
||||
</p>
|
||||
|
||||
<sample src="examples/unsafe-shell-command-construction.js" />
|
||||
|
||||
<p>
|
||||
The shell command will, however, fail to work as intended if the
|
||||
input contains spaces or other special characters interpreted in a
|
||||
special way by the shell.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Even worse, a client might pass in user-controlled
|
||||
data, not knowing that the input is interpreted as a shell command.
|
||||
This could allow a malicious user to provide the input <code>http://example.org; cat /etc/passwd</code>
|
||||
in order to execute the command <code>cat /etc/passwd</code>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To avoid such potentially catastrophic behaviors, provide the
|
||||
inputs from exported functions as an argument that does not
|
||||
get interpreted by a shell:
|
||||
</p>
|
||||
|
||||
<sample src="examples/unsafe-shell-command-construction_fixed.js" />
|
||||
|
||||
</example>
|
||||
<references>
|
||||
|
||||
<li>
|
||||
OWASP:
|
||||
<a href="https://www.owasp.org/index.php/Command_Injection">Command Injection</a>.
|
||||
</li>
|
||||
|
||||
</references>
|
||||
</qhelp>
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @name Unsafe shell command constructed from library input
|
||||
* @description Using externally controlled strings in a command line may allow a malicious
|
||||
* user to change the meaning of the command.
|
||||
* @kind path-problem
|
||||
* @problem.severity error
|
||||
* @precision high
|
||||
* @id js/shell-command-constructed-from-input
|
||||
* @tags correctness
|
||||
* security
|
||||
* external/cwe/cwe-078
|
||||
* external/cwe/cwe-088
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import semmle.javascript.security.dataflow.UnsafeShellCommandConstruction::UnsafeShellCommandConstruction
|
||||
import DataFlow::PathGraph
|
||||
|
||||
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, Sink sinkNode
|
||||
where cfg.hasFlowPath(source, sink) and sinkNode = sink.getNode()
|
||||
select sinkNode.getAlertLocation(), source, sink, "$@ based on libary input is later used in $@.",
|
||||
sinkNode.getAlertLocation(), sinkNode.getSinkType(), sinkNode.getCommandExecution(),
|
||||
"shell command"
|
||||
@@ -0,0 +1,5 @@
|
||||
var cp = require("child_process");
|
||||
|
||||
module.exports = function download(path, callback) {
|
||||
cp.exec("wget " + path, callback);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
var cp = require("child_process");
|
||||
|
||||
module.exports = function download(path, callback) {
|
||||
cp.execFile("wget", [path], callback);
|
||||
}
|
||||
@@ -16,7 +16,8 @@ import semmle.javascript.security.dataflow.UnsafeJQueryPlugin::UnsafeJQueryPlugi
|
||||
import DataFlow::PathGraph
|
||||
|
||||
from
|
||||
Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, JQueryPluginMethod plugin
|
||||
Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink,
|
||||
JQuery::JQueryPluginMethod plugin
|
||||
where
|
||||
cfg.hasFlowPath(source, sink) and
|
||||
source.getNode().(Source).getPlugin() = plugin and
|
||||
|
||||
70
javascript/ql/src/Security/CWE-079/XssThroughDom.qhelp
Normal file
70
javascript/ql/src/Security/CWE-079/XssThroughDom.qhelp
Normal file
@@ -0,0 +1,70 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>
|
||||
Extracting text from a DOM node and interpreting it as HTML can lead to a cross-site scripting vulnerability.
|
||||
</p>
|
||||
<p>
|
||||
A webpage with this vulnerability reads text from the DOM, and afterwards adds the text as HTML to the DOM.
|
||||
Using text from the DOM as HTML effectively unescapes the text, and thereby invalidates any escaping done on the text.
|
||||
If an attacker is able to control the safe sanitized text, then this vulnerability can be exploited to perform a cross-site scripting attack.
|
||||
</p>
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>
|
||||
To guard against cross-site scripting, consider using contextual output encoding/escaping before
|
||||
writing text to the page, or one of the other solutions that are mentioned in the References section below.
|
||||
</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>
|
||||
The following example shows a webpage using a <code>data-target</code> attribute
|
||||
to select and manipulate a DOM element using the JQuery library. In the example, the
|
||||
<code>data-target</code> attribute is read into the <code>target</code> variable, and the
|
||||
<code>$</code> function is then supposed to use the <code>target</code> variable as a CSS
|
||||
selector to determine which element should be manipulated.
|
||||
</p>
|
||||
<sample src="examples/XssThroughDom.js" />
|
||||
<p>
|
||||
However, if an attacker can control the <code>data-target</code> attribute,
|
||||
then the value of <code>target</code> can be used to cause the <code>$</code> function
|
||||
to execute arbitary JavaScript.
|
||||
</p>
|
||||
<p>
|
||||
The above vulnerability can be fixed by using <code>$.find</code> instead of <code>$</code>.
|
||||
The <code>$.find</code> function will only interpret <code>target</code> as a CSS selector
|
||||
and never as HTML, thereby preventing an XSS attack.
|
||||
</p>
|
||||
<sample src="examples/XssThroughDomFixed.js" />
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>
|
||||
OWASP:
|
||||
<a href="https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html">DOM based
|
||||
XSS Prevention Cheat Sheet</a>.
|
||||
</li>
|
||||
<li>
|
||||
OWASP:
|
||||
<a href="https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html">XSS
|
||||
(Cross Site Scripting) Prevention Cheat Sheet</a>.
|
||||
</li>
|
||||
<li>
|
||||
OWASP
|
||||
<a href="https://owasp.org/www-community/attacks/DOM_Based_XSS">DOM Based XSS</a>.
|
||||
</li>
|
||||
<li>
|
||||
OWASP
|
||||
<a href="https://owasp.org/www-community/Types_of_Cross-Site_Scripting">Types of Cross-Site
|
||||
Scripting</a>.
|
||||
</li>
|
||||
<li>
|
||||
Wikipedia: <a href="http://en.wikipedia.org/wiki/Cross-site_scripting">Cross-site scripting</a>.
|
||||
</li>
|
||||
</references>
|
||||
</qhelp>
|
||||
21
javascript/ql/src/Security/CWE-079/XssThroughDom.ql
Normal file
21
javascript/ql/src/Security/CWE-079/XssThroughDom.ql
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @name Cross-site scripting through DOM
|
||||
* @description Writing user-controlled DOM to HTML can allow for
|
||||
* a cross-site scripting vulnerability.
|
||||
* @kind path-problem
|
||||
* @problem.severity error
|
||||
* @precision medium
|
||||
* @id js/xss-through-dom
|
||||
* @tags security
|
||||
* external/cwe/cwe-079
|
||||
* external/cwe/cwe-116
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import semmle.javascript.security.dataflow.XssThroughDom::XssThroughDom
|
||||
import DataFlow::PathGraph
|
||||
|
||||
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||
where cfg.hasFlowPath(source, sink)
|
||||
select sink.getNode(), source, sink, "Cross-site scripting vulnerability due to $@.",
|
||||
source.getNode(), "DOM text"
|
||||
@@ -0,0 +1,4 @@
|
||||
$("button").click(function () {
|
||||
var target = $(this).attr("data-target");
|
||||
$(target).hide();
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
$("button").click(function () {
|
||||
var target = $(this).attr("data-target");
|
||||
$.find(target).hide();
|
||||
});
|
||||
@@ -1,18 +1,21 @@
|
||||
const pg = require('pg');
|
||||
const pool = new pg.Pool(config);
|
||||
const app = require("express")(),
|
||||
pg = require("pg"),
|
||||
pool = new pg.Pool(config);
|
||||
|
||||
function handler(req, res) {
|
||||
app.get("search", function handler(req, res) {
|
||||
// BAD: the category might have SQL special characters in it
|
||||
var query1 = "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='"
|
||||
+ req.params.category + "' ORDER BY PRICE";
|
||||
var query1 =
|
||||
"SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='" +
|
||||
req.params.category +
|
||||
"' ORDER BY PRICE";
|
||||
pool.query(query1, [], function(err, results) {
|
||||
// process results
|
||||
});
|
||||
|
||||
// GOOD: use parameters
|
||||
var query2 = "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY=$1"
|
||||
+ " ORDER BY PRICE";
|
||||
var query2 =
|
||||
"SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY=$1" + " ORDER BY PRICE";
|
||||
pool.query(query2, [req.params.category], function(err, results) {
|
||||
// process results
|
||||
// process results
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>
|
||||
|
||||
Sanitizing untrusted input for HTML meta-characters is an important
|
||||
technique for preventing cross-site scripting attacks. Usually, this
|
||||
is done by escaping <code><</code>, <code>></code>,
|
||||
<code>&</code> and <code>"</code>. However, the context in which
|
||||
the sanitized value is used decides the characters that
|
||||
need to be sanitized.
|
||||
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
||||
As a consequence, some programs only sanitize
|
||||
<code><</code> and <code>></code> since those are the most
|
||||
common dangerous characters. The lack of sanitization for
|
||||
<code>"</code> is problematic when an incompletely sanitized
|
||||
value is used as an HTML attribute in a string that
|
||||
later is parsed as HTML.
|
||||
|
||||
</p>
|
||||
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
|
||||
<p>
|
||||
|
||||
Sanitize all relevant HTML meta-characters when
|
||||
constructing HTML dynamically, and pay special attention to where the
|
||||
sanitized value is used.
|
||||
|
||||
</p>
|
||||
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
|
||||
<p>
|
||||
|
||||
The following example code writes part of an HTTP request (which is
|
||||
controlled by the user) to an HTML attribute of the server response.
|
||||
|
||||
The user-controlled value is, however, not sanitized for
|
||||
<code>"</code>. This leaves the website vulnerable to cross-site
|
||||
scripting since an attacker can use a string like <code>"
|
||||
onclick="alert(42)</code> to inject JavaScript code into the response.
|
||||
|
||||
</p>
|
||||
<sample src="examples/IncompleteHtmlAttributeSanitization.js" />
|
||||
|
||||
|
||||
<p>
|
||||
|
||||
Sanitizing the user-controlled data for
|
||||
<code>"</code> helps prevent the vulnerability:
|
||||
|
||||
</p>
|
||||
|
||||
<sample src="examples/IncompleteHtmlAttributeSanitizationGood.js" />
|
||||
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>
|
||||
OWASP:
|
||||
<a href="https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html">DOM based
|
||||
XSS Prevention Cheat Sheet</a>.
|
||||
</li>
|
||||
<li>
|
||||
OWASP:
|
||||
<a href="https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html">XSS
|
||||
(Cross Site Scripting) Prevention Cheat Sheet</a>.
|
||||
</li>
|
||||
<li>
|
||||
OWASP
|
||||
<a href="https://owasp.org/www-community/Types_of_Cross-Site_Scripting">Types of Cross-Site</a>.
|
||||
</li>
|
||||
<li>
|
||||
Wikipedia: <a href="http://en.wikipedia.org/wiki/Cross-site_scripting">Cross-site scripting</a>.
|
||||
</li>
|
||||
</references>
|
||||
|
||||
</qhelp>
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @name Incomplete HTML attribute sanitization
|
||||
* @description Writing incompletely sanitized values to HTML
|
||||
* attribute strings can lead to a cross-site
|
||||
* scripting vulnerability.
|
||||
* @kind path-problem
|
||||
* @problem.severity warning
|
||||
* @precision high
|
||||
* @id js/incomplete-html-attribute-sanitization
|
||||
* @tags security
|
||||
* external/cwe/cwe-079
|
||||
* external/cwe/cwe-116
|
||||
* external/cwe/cwe-20
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import DataFlow::PathGraph
|
||||
import semmle.javascript.security.dataflow.IncompleteHtmlAttributeSanitization::IncompleteHtmlAttributeSanitization
|
||||
import semmle.javascript.security.IncompleteBlacklistSanitizer
|
||||
|
||||
/**
|
||||
* Gets a pretty string of the dangerous characters for `sink`.
|
||||
*/
|
||||
string prettyPrintDangerousCharaters(Sink sink) {
|
||||
result =
|
||||
strictconcat(string s |
|
||||
s = describeCharacters(sink.getADangerousCharacter())
|
||||
|
|
||||
s, ", " order by s
|
||||
).regexpReplaceAll(",(?=[^,]+$)", " or")
|
||||
}
|
||||
|
||||
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||
where cfg.hasFlowPath(source, sink)
|
||||
select sink.getNode(), source, sink,
|
||||
// this message is slightly sub-optimal as we do not have an easy way
|
||||
// to get the flow labels that reach the sink, so the message includes
|
||||
// all of them in a disjunction
|
||||
"Cross-site scripting vulnerability as the output of $@ may contain " +
|
||||
prettyPrintDangerousCharaters(sink.getNode()) + " when it reaches this attribute definition.",
|
||||
source.getNode(), "this final HTML sanitizer step"
|
||||
99
javascript/ql/src/Security/CWE-116/UnsafeHtmlExpansion.qhelp
Normal file
99
javascript/ql/src/Security/CWE-116/UnsafeHtmlExpansion.qhelp
Normal file
@@ -0,0 +1,99 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>
|
||||
|
||||
Sanitizing untrusted input for HTML meta-characters is an
|
||||
important technique for preventing cross-site scripting attacks. But
|
||||
even a sanitized input can be dangerous to use if it is modified
|
||||
further before a browser treats it as HTML.
|
||||
|
||||
A seemingly innocent transformation that expands a
|
||||
self-closing HTML tag from <code><div attr="{sanitized}"/></code>
|
||||
to <code><div attr="{sanitized}"></div></code> may
|
||||
in fact cause cross-site scripting vulnerabilities.
|
||||
|
||||
</p>
|
||||
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>
|
||||
|
||||
Use a well-tested sanitization library if at all
|
||||
possible, and avoid modifying sanitized values further before treating
|
||||
them as HTML.
|
||||
|
||||
</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
|
||||
<p>
|
||||
|
||||
The following function transforms a self-closing HTML tag
|
||||
to a pair of open/close tags. It does so for all non-<code>img</code>
|
||||
and non-<code>area</code> tags, by using a regular expression with two
|
||||
capture groups. The first capture group corresponds to the name of the
|
||||
tag, and the second capture group to the content of the tag.
|
||||
|
||||
</p>
|
||||
|
||||
<sample src="examples/UnsafeHtmlExpansion.js" />
|
||||
|
||||
<p>
|
||||
|
||||
While it is generally known regular expressions are
|
||||
ill-suited for parsing HTML, variants of this particular transformation
|
||||
pattern have long been considered safe.
|
||||
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
||||
However, the function is not safe. As an example, consider
|
||||
the following string:
|
||||
|
||||
|
||||
</p>
|
||||
|
||||
<sample src="examples/UnsafeHtmlExpansion-original.html" />
|
||||
|
||||
<p>
|
||||
|
||||
When the above function transforms the string, it becomes
|
||||
a string that results in an alert when a browser treats it as HTML.
|
||||
|
||||
</p>
|
||||
|
||||
<sample src="examples/UnsafeHtmlExpansion-transformed.html" />
|
||||
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>jQuery:
|
||||
<a href="https://blog.jquery.com/2020/04/10/jquery-3-5-0-released/">Security fixes in jQuery 3.5.0</a>
|
||||
</li>
|
||||
<li>
|
||||
OWASP:
|
||||
<a href="https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html">DOM based
|
||||
XSS Prevention Cheat Sheet</a>.
|
||||
</li>
|
||||
<li>
|
||||
OWASP:
|
||||
<a href="https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html">XSS
|
||||
(Cross Site Scripting) Prevention Cheat Sheet</a>.
|
||||
</li>
|
||||
<li>
|
||||
OWASP
|
||||
<a href="https://owasp.org/www-community/Types_of_Cross-Site_Scripting">Types of Cross-Site</a>.
|
||||
</li>
|
||||
<li>
|
||||
Wikipedia: <a href="http://en.wikipedia.org/wiki/Cross-site_scripting">Cross-site scripting</a>.
|
||||
</li>
|
||||
</references>
|
||||
|
||||
</qhelp>
|
||||
58
javascript/ql/src/Security/CWE-116/UnsafeHtmlExpansion.ql
Normal file
58
javascript/ql/src/Security/CWE-116/UnsafeHtmlExpansion.ql
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @name Unsafe expansion of self-closing HTML tag
|
||||
* @description Using regular expressions to expand self-closing HTML
|
||||
* tags may lead to cross-site scripting vulnerabilities.
|
||||
* @kind problem
|
||||
* @problem.severity warning
|
||||
* @precision very-high
|
||||
* @id js/unsafe-html-expansion
|
||||
* @tags correctness
|
||||
* security
|
||||
* external/cwe/cwe-079
|
||||
* external/cwe/cwe-116
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
/**
|
||||
* A regular expression that captures the name and content of a
|
||||
* self-closing HTML tag such as `<div id='foo'/>`.
|
||||
*/
|
||||
class SelfClosingTagRecognizer extends DataFlow::RegExpCreationNode {
|
||||
SelfClosingTagRecognizer() {
|
||||
exists(RegExpSequence seq, RegExpGroup name, RegExpGroup content |
|
||||
// `/.../g`
|
||||
RegExp::isGlobal(this.getFlags()) and
|
||||
this.getRoot() = seq.getRootTerm() and
|
||||
// `/<.../`
|
||||
seq.getChild(0).getConstantValue() = "<" and
|
||||
// `/...\/>/`
|
||||
seq.getLastChild().getPredecessor().getConstantValue() = "/" and
|
||||
seq.getLastChild().getConstantValue() = ">" and
|
||||
// `/...((...)...).../`
|
||||
seq.getAChild() = content and
|
||||
content.getNumber() = 1 and
|
||||
name.getNumber() = 2 and
|
||||
name = content.getChild(0).(RegExpSequence).getChild(0) and
|
||||
// `/...(([a-z]+)...).../` or `/...(([a-z][...]*)...).../`
|
||||
exists(RegExpQuantifier quant | name.getAChild*() = quant |
|
||||
quant instanceof RegExpStar or
|
||||
quant instanceof RegExpPlus
|
||||
) and
|
||||
// `/...((...)[^>]*).../`
|
||||
exists(RegExpCharacterClass lazy |
|
||||
name.getSuccessor().(RegExpStar).getChild(0) = lazy and
|
||||
lazy.isInverted() and
|
||||
lazy.getAChild().getConstantValue() = ">"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
from SelfClosingTagRecognizer regexp, StringReplaceCall replace
|
||||
where
|
||||
regexp.getAReference().flowsTo(replace.getArgument(0)) and
|
||||
replace.getRawReplacement().mayHaveStringValue("<$1></$2>")
|
||||
select replace,
|
||||
"This self-closing HTML tag expansion invalidates prior sanitization as $@ may match part of an attribute value.",
|
||||
regexp, "this regular expression"
|
||||
@@ -0,0 +1,9 @@
|
||||
var app = require('express')();
|
||||
|
||||
app.get('/user/:id', function(req, res) {
|
||||
let id = req.params.id;
|
||||
id = id.replace(/<|>/g, ""); // BAD
|
||||
let userHtml = `<div data-id="${id}">${getUserName(id) || "Unknown name"}</div>`;
|
||||
// ...
|
||||
res.send(prefix + userHtml + suffix);
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
var app = require('express')();
|
||||
|
||||
app.get('/user/:id', function(req, res) {
|
||||
let id = req.params.id;
|
||||
id = id.replace(/<|>|&|"/g, ""); // GOOD
|
||||
let userHtml = `<div data-id="${id}">${getUserName(id) || "Unknown name"}</div>`;
|
||||
// ...
|
||||
res.send(prefix + userHtml + suffix);
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
<div alt="
|
||||
<x" title="/>
|
||||
<img src=url404 onerror=alert(1)>"/>
|
||||
@@ -0,0 +1,3 @@
|
||||
<img alt="
|
||||
<x" title="></x" >
|
||||
<img src=url404 onerror=alert(1)>"/>
|
||||
@@ -0,0 +1,4 @@
|
||||
function expandSelfClosingTags(html) {
|
||||
var rxhtmlTag = /<(?!img|area)(([a-z][^\w\/>]*)[^>]*)\/>/gi;
|
||||
return html.replace(rxhtmlTag, "<$1></$2>"); // BAD
|
||||
}
|
||||
@@ -1 +1,7 @@
|
||||
console.log("Unauthorized access attempt by " + user, ip);
|
||||
const app = require("express")();
|
||||
|
||||
app.get("unauthorized", function handler(req, res) {
|
||||
let user = req.query.user;
|
||||
let ip = req.connection.remoteAddress;
|
||||
console.log("Unauthorized access attempt by " + user, ip);
|
||||
});
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
console.log("Unauthorized access attempt by %s", user, ip);
|
||||
const app = require("express")();
|
||||
|
||||
app.get("unauthorized", function handler(req, res) {
|
||||
let user = req.query.user;
|
||||
let ip = req.connection.remoteAddress;
|
||||
console.log("Unauthorized access attempt by %s", user, ip);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
const crypto = require('crypto');
|
||||
|
||||
var secretText = obj.getSecretText();
|
||||
|
||||
const desCipher = crypto.createCipher('des', key);
|
||||
let desEncrypted = cipher.write(secretText, 'utf8', 'hex'); // BAD: weak encryption
|
||||
let desEncrypted = desCipher.write(secretText, 'utf8', 'hex'); // BAD: weak encryption
|
||||
|
||||
const aesCipher = crypto.createCipher('aes-128', key);
|
||||
let aesEncrypted = cipher.update(secretText, 'utf8', 'hex'); // GOOD: strong encryption
|
||||
let aesEncrypted = aesCipher.update(secretText, 'utf8', 'hex'); // GOOD: strong encryption
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
var express = require('express')
|
||||
var cookieParser = require('cookie-parser')
|
||||
var passport = require('passport')
|
||||
var app = require("express")(),
|
||||
cookieParser = require("cookie-parser"),
|
||||
passport = require("passport");
|
||||
|
||||
var app = express()
|
||||
app.use(cookieParser());
|
||||
app.use(passport.authorize({ session: true }));
|
||||
|
||||
app.use(cookieParser())
|
||||
app.use(passport.authorize({ session: true }))
|
||||
|
||||
app.post('/changeEmail', ..., function (req, res) {
|
||||
})
|
||||
app.post("/changeEmail", function(req, res) {
|
||||
let newEmail = req.cookies["newEmail"];
|
||||
// ...
|
||||
});
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
var express = require('express')
|
||||
var cookieParser = require('cookie-parser')
|
||||
var passport = require('passport')
|
||||
var csrf = require('csurf')
|
||||
var app = require("express")(),
|
||||
cookieParser = require("cookie-parser"),
|
||||
passport = require("passport"),
|
||||
csrf = require("csurf");
|
||||
|
||||
var app = express()
|
||||
|
||||
app.use(cookieParser())
|
||||
app.use(passport.authorize({ session: true }))
|
||||
app.use(csrf({ cookie:true }))
|
||||
|
||||
app.post('/changeEmail', ..., function (req, res) {
|
||||
})
|
||||
app.use(cookieParser());
|
||||
app.use(passport.authorize({ session: true }));
|
||||
app.use(csrf({ cookie: true }));
|
||||
app.post("/changeEmail", function(req, res) {
|
||||
let newEmail = req.cookies["newEmail"];
|
||||
// ...
|
||||
});
|
||||
|
||||
@@ -21,11 +21,10 @@ import semmle.javascript.DynamicPropertyAccess
|
||||
*
|
||||
* We restrict this to parameter nodes to focus on "deep assignment" functions.
|
||||
*/
|
||||
class SplitCall extends MethodCallNode {
|
||||
class SplitCall extends StringSplitCall {
|
||||
SplitCall() {
|
||||
getMethodName() = "split" and
|
||||
getArgument(0).mayHaveStringValue(".") and
|
||||
getReceiver().getALocalSource() instanceof ParameterNode
|
||||
getSeparator() = "." and
|
||||
getBaseString().getALocalSource() instanceof ParameterNode
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,8 +449,10 @@ class BlacklistInclusionGuard extends DataFlow::LabeledBarrierGuardNode, Inclusi
|
||||
*/
|
||||
class WhitelistInclusionGuard extends DataFlow::LabeledBarrierGuardNode {
|
||||
WhitelistInclusionGuard() {
|
||||
this instanceof TaintTracking::PositiveIndexOfSanitizer or
|
||||
this instanceof TaintTracking::InclusionSanitizer
|
||||
this instanceof TaintTracking::PositiveIndexOfSanitizer
|
||||
or
|
||||
this instanceof TaintTracking::MembershipTestSanitizer and
|
||||
not this = any(MembershipCandidate::ObjectPropertyNameMembershipCandidate c).getTest() // handled with more precision in `HasOwnPropertyGuard`
|
||||
}
|
||||
|
||||
override predicate blocks(boolean outcome, Expr e, DataFlow::FlowLabel lbl) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const jsyaml = require("js-yaml");
|
||||
const app = require("express")(),
|
||||
jsyaml = require("js-yaml");
|
||||
|
||||
function requestHandler(req, res) {
|
||||
app.get("load", function(req, res) {
|
||||
let data = jsyaml.load(req.params.data);
|
||||
// ...
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const jsyaml = require("js-yaml");
|
||||
const app = require("express")(),
|
||||
jsyaml = require("js-yaml");
|
||||
|
||||
function requestHandler(req, res) {
|
||||
app.get("load", function(req, res) {
|
||||
let data = jsyaml.safeLoad(req.params.data);
|
||||
// ...
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const app = require("express")();
|
||||
|
||||
app.get('/some/path', function(req, res) {
|
||||
// BAD: a request parameter is incorporated without validation into a URL redirect
|
||||
res.redirect(req.param("target"));
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const app = require("express")();
|
||||
|
||||
const VALID_REDIRECT = "http://cwe.mitre.org/data/definitions/601.html";
|
||||
|
||||
app.get('/some/path', function(req, res) {
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
const libxml = require('libxmljs');
|
||||
var doc = libxml.parseXml(xmlSrc, { noent: true });
|
||||
const app = require("express")(),
|
||||
libxml = require("libxmljs");
|
||||
|
||||
app.post("upload", (req, res) => {
|
||||
let xmlSrc = req.body,
|
||||
doc = libxml.parseXml(xmlSrc, { noent: true });
|
||||
});
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
const libxml = require('libxmljs');
|
||||
var doc = libxml.parseXml(xmlSrc);
|
||||
const app = require("express")(),
|
||||
libxml = require("libxmljs");
|
||||
|
||||
app.post("upload", (req, res) => {
|
||||
let xmlSrc = req.body,
|
||||
doc = libxml.parseXml(xmlSrc);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
const expat = require('node-expat');
|
||||
var parser = new expat.Parser();
|
||||
parser.on('startElement', handleStart);
|
||||
parser.on('text', handleText);
|
||||
parser.write(xmlSrc);
|
||||
const app = require("express")(),
|
||||
expat = require("node-expat");
|
||||
|
||||
app.post("upload", (req, res) => {
|
||||
let xmlSrc = req.body,
|
||||
parser = new expat.Parser();
|
||||
parser.on("startElement", handleStart);
|
||||
parser.on("text", handleText);
|
||||
parser.write(xmlSrc);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
const sax = require('sax');
|
||||
var parser = sax.parser(true);
|
||||
parser.onopentag = handleStart;
|
||||
parser.ontext = handleText;
|
||||
parser.write(xmlSrc);
|
||||
const app = require("express")(),
|
||||
sax = require("sax");
|
||||
|
||||
app.post("upload", (req, res) => {
|
||||
let xmlSrc = req.body,
|
||||
parser = sax.parser(true);
|
||||
parser.onopentag = handleStart;
|
||||
parser.ontext = handleText;
|
||||
parser.write(xmlSrc);
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const pg = require('pg')
|
||||
const pg = require("pg");
|
||||
|
||||
const client = new pg.Client({
|
||||
user: 'dbuser',
|
||||
host: 'database.server.com',
|
||||
database: 'mydb',
|
||||
password: 'secretpassword',
|
||||
port: 3211,
|
||||
})
|
||||
client.connect()
|
||||
user: "bob",
|
||||
host: "database.server.com",
|
||||
database: "mydb",
|
||||
password: "correct-horse-battery-staple",
|
||||
port: 3211
|
||||
});
|
||||
client.connect();
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
var express = require('express'),
|
||||
path = require('path'),
|
||||
var app = express();
|
||||
var app = require("express")(),
|
||||
path = require("path");
|
||||
|
||||
app.get('/user-files', function(req, res) {
|
||||
var file = req.param('file');
|
||||
if (file.indexOf('..') !== -1) { // BAD
|
||||
// forbid paths outside the /public directory
|
||||
res.status(400).send('Bad request');
|
||||
} else {
|
||||
var absolute = path.resolve('/public/' + file);
|
||||
console.log("Sending file: %s", absolute);
|
||||
res.sendFile(absolute);
|
||||
}
|
||||
app.get("/user-files", function(req, res) {
|
||||
var file = req.param("file");
|
||||
if (file.indexOf("..") !== -1) {
|
||||
// BAD
|
||||
// forbid paths outside the /public directory
|
||||
res.status(400).send("Bad request");
|
||||
} else {
|
||||
var absolute = path.resolve("/public/" + file);
|
||||
console.log("Sending file: %s", absolute);
|
||||
res.sendFile(absolute);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
var express = require('express'),
|
||||
path = require('path'),
|
||||
var app = express();
|
||||
var app = require("express")(),
|
||||
path = require("path");
|
||||
|
||||
app.get('/user-files', function(req, res) {
|
||||
var file = req.param('file');
|
||||
if (typeof path !== 'string' || path.indexOf('..') !== -1) { // GOOD
|
||||
// forbid paths outside the /public directory
|
||||
res.status(400).send('Bad request');
|
||||
} else {
|
||||
var full = path.resolve('/public/' + file);
|
||||
console.log("Sending file: %s", full);
|
||||
res.sendFile(full);
|
||||
}
|
||||
app.get("/user-files", function(req, res) {
|
||||
var file = req.param("file");
|
||||
if (typeof path !== 'string' || file.indexOf("..") !== -1) {
|
||||
// BAD
|
||||
// forbid paths outside the /public directory
|
||||
res.status(400).send("Bad request");
|
||||
} else {
|
||||
var absolute = path.resolve("/public/" + file);
|
||||
console.log("Sending file: %s", absolute);
|
||||
res.sendFile(absolute);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const crypto = require("crypto");
|
||||
function hashPassword(password) {
|
||||
var crypto = require("crypto");
|
||||
var hasher = crypto.createHash('md5');
|
||||
var hashed = hasher.update(password).digest("hex"); // BAD
|
||||
return hashed;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const bcrypt = require("bcrypt");
|
||||
function hashPassword(password, salt) {
|
||||
var bcrypt = require('bcrypt');
|
||||
var hashed = bcrypt.hashSync(password, salt); // GOOD
|
||||
return hashed;
|
||||
var hashed = bcrypt.hashSync(password, salt); // GOOD
|
||||
return hashed;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import http from 'http';
|
||||
import url from 'url';
|
||||
|
||||
var server = http.createServer(function(req, res) {
|
||||
var target = url.parse(request.url, true).query.target;
|
||||
var target = url.parse(req.url, true).query.target;
|
||||
|
||||
// BAD: `target` is controlled by the attacker
|
||||
http.get('https://' + target + ".example.com/data/", res => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import http from 'http';
|
||||
import url from 'url';
|
||||
|
||||
var server = http.createServer(function(req, res) {
|
||||
var target = url.parse(request.url, true).query.target;
|
||||
var target = url.parse(req.url, true).query.target;
|
||||
|
||||
var subdomain;
|
||||
if (target === 'EU') {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* @problem.severity recommendation
|
||||
* @id js/single-run-loop
|
||||
* @tags readability
|
||||
* @precision high
|
||||
* @precision low
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @id js/nested-loops-with-same-variable
|
||||
* @tags maintainability
|
||||
* correctness
|
||||
* @precision medium
|
||||
* @precision low
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @id js/return-outside-function
|
||||
* @tags reliability
|
||||
* correctness
|
||||
* @precision medium
|
||||
* @precision low
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
@@ -62,6 +62,14 @@ predicate isInitialParameterUse(Expr e) {
|
||||
not p.isRestParameter()
|
||||
)
|
||||
or
|
||||
// same as above, but for captured variables
|
||||
exists(SimpleParameter p, LocalVariable var |
|
||||
var = p.getVariable() and
|
||||
var.isCaptured() and
|
||||
e = var.getAnAccess() and
|
||||
not p.isRestParameter()
|
||||
)
|
||||
or
|
||||
isInitialParameterUse(e.(LogNotExpr).getOperand())
|
||||
}
|
||||
|
||||
|
||||
@@ -2,3 +2,8 @@
|
||||
- qlpack: codeql-javascript
|
||||
- apply: lgtm-selectors.yml
|
||||
from: codeql-suite-helpers
|
||||
# These are only for IDE use.
|
||||
- exclude:
|
||||
tags contain:
|
||||
- ide-contextual-queries/local-definitions
|
||||
- ide-contextual-queries/local-references
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
- description: Security-and-quality queries for JavaScript
|
||||
- qlpack: codeql-javascript
|
||||
- apply: security-and-quality-selectors.yml
|
||||
from: codeql-suite-helpers
|
||||
@@ -0,0 +1,4 @@
|
||||
- description: Security-extended queries for JavaScript
|
||||
- qlpack: codeql-javascript
|
||||
- apply: security-extended-selectors.yml
|
||||
from: codeql-suite-helpers
|
||||
@@ -6,169 +6,8 @@
|
||||
* @id js/jump-to-definition
|
||||
*/
|
||||
|
||||
import javascript
|
||||
private import Declarations.Declarations
|
||||
import definitions
|
||||
|
||||
/**
|
||||
* Gets the kind of reference that `r` represents.
|
||||
*
|
||||
* References in callee position have kind `"M"` (for "method"), all
|
||||
* others have kind `"V"` (for "variable").
|
||||
*
|
||||
* For example, in the expression `f(x)`, `f` has kind `"M"` while
|
||||
* `x` has kind `"V"`.
|
||||
*/
|
||||
string refKind(RefExpr r) {
|
||||
if exists(InvokeExpr invk | r = invk.getCallee().getUnderlyingReference())
|
||||
then result = "M"
|
||||
else result = "V"
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a class, function or object literal `va` may refer to.
|
||||
*/
|
||||
ASTNode lookupDef(VarAccess va) {
|
||||
exists(AbstractValue av | av = va.analyze().getAValue() |
|
||||
result = av.(AbstractClass).getClass() or
|
||||
result = av.(AbstractFunction).getFunction() or
|
||||
result = av.(AbstractObjectLiteral).getObjectExpr()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `va` is of kind `kind` and `def` is the unique class,
|
||||
* function or object literal it refers to.
|
||||
*/
|
||||
predicate variableDefLookup(VarAccess va, ASTNode def, string kind) {
|
||||
count(lookupDef(va)) = 1 and
|
||||
def = lookupDef(va) and
|
||||
kind = refKind(va)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if variable access `va` is of kind `kind` and refers to the
|
||||
* variable declaration.
|
||||
*
|
||||
* For example, in the statement `var x = 42, y = x;`, the initializing
|
||||
* expression of `y` is a variable access `x` of kind `"V"` that refers to
|
||||
* the declaration `x = 42`.
|
||||
*/
|
||||
predicate variableDeclLookup(VarAccess va, VarDecl decl, string kind) {
|
||||
// restrict to declarations in same file to avoid accidentally picking up
|
||||
// unrelated global definitions
|
||||
decl = firstRefInTopLevel(va.getVariable(), Decl(), va.getTopLevel()) and
|
||||
kind = refKind(va)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if path expression `path`, which appears in a CommonJS `require`
|
||||
* call or an ES 2015 import statement, imports module `target`; `kind`
|
||||
* is always "I" (for "import").
|
||||
*
|
||||
* For example, in the statement `var a = require("./a")`, the path expression
|
||||
* `"./a"` imports a module `a` in the same folder.
|
||||
*/
|
||||
predicate importLookup(ASTNode path, Module target, string kind) {
|
||||
kind = "I" and
|
||||
(
|
||||
exists(Import i |
|
||||
path = i.getImportedPath() and
|
||||
target = i.getImportedModule()
|
||||
)
|
||||
or
|
||||
exists(ReExportDeclaration red |
|
||||
path = red.getImportedPath() and
|
||||
target = red.getReExportedModule()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a node that may write the property read by `prn`.
|
||||
*/
|
||||
ASTNode getAWrite(DataFlow::PropRead prn) {
|
||||
exists(DataFlow::AnalyzedNode base, DefiniteAbstractValue baseVal, string propName |
|
||||
base = prn.getBase() and
|
||||
propName = prn.getPropertyName() and
|
||||
baseVal = base.getAValue().getAPrototype*()
|
||||
|
|
||||
// write to a property on baseVal
|
||||
exists(AnalyzedPropertyWrite apw |
|
||||
result = apw.getAstNode() and
|
||||
apw.writes(baseVal, propName, _)
|
||||
)
|
||||
or
|
||||
// non-static class members aren't covered by `AnalyzedPropWrite`, so have to be handled
|
||||
// separately
|
||||
exists(ClassDefinition c, MemberDefinition m |
|
||||
m = c.getMember(propName) and
|
||||
baseVal.(AbstractInstance).getConstructor().(AbstractClass).getClass() = c and
|
||||
result = m.getNameExpr()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `prop` is the property name expression of a property read that
|
||||
* may read the property written by `write`. Furthermore, `write` must be the
|
||||
* only such property write. Parameter `kind` is always bound to `"M"`
|
||||
* at the moment.
|
||||
*/
|
||||
predicate propertyLookup(Expr prop, ASTNode write, string kind) {
|
||||
exists(DataFlow::PropRead prn | prop = prn.getPropertyNameExpr() |
|
||||
count(getAWrite(prn)) = 1 and
|
||||
write = getAWrite(prn) and
|
||||
kind = "M"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `ref` is an identifier that refers to a type declared at `decl`.
|
||||
*/
|
||||
predicate typeLookup(ASTNode ref, ASTNode decl, string kind) {
|
||||
exists(TypeAccess typeAccess |
|
||||
ref = typeAccess.getIdentifier() and
|
||||
decl = typeAccess.getTypeName().getADefinition() and
|
||||
kind = "T"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `ref` is the callee name of an invocation of `decl`.
|
||||
*/
|
||||
predicate typedInvokeLookup(ASTNode ref, ASTNode decl, string kind) {
|
||||
not variableDefLookup(ref, decl, _) and
|
||||
not propertyLookup(ref, decl, _) and
|
||||
exists(InvokeExpr invoke, Expr callee |
|
||||
callee = invoke.getCallee().getUnderlyingReference() and
|
||||
(ref = callee.(Identifier) or ref = callee.(DotExpr).getPropertyNameExpr()) and
|
||||
decl = invoke.getResolvedCallee() and
|
||||
kind = "M"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `ref` is a JSDoc type annotation referring to a class defined at `decl`.
|
||||
*/
|
||||
predicate jsdocTypeLookup(JSDocNamedTypeExpr ref, ASTNode decl, string kind) {
|
||||
decl = ref.getClass().getAstNode() and
|
||||
kind = "T"
|
||||
}
|
||||
|
||||
from Locatable ref, ASTNode decl, string kind
|
||||
where
|
||||
variableDefLookup(ref, decl, kind)
|
||||
or
|
||||
// prefer definitions over declarations
|
||||
not variableDefLookup(ref, _, _) and variableDeclLookup(ref, decl, kind)
|
||||
or
|
||||
importLookup(ref, decl, kind)
|
||||
or
|
||||
propertyLookup(ref, decl, kind)
|
||||
or
|
||||
typeLookup(ref, decl, kind)
|
||||
or
|
||||
typedInvokeLookup(ref, decl, kind)
|
||||
or
|
||||
jsdocTypeLookup(ref, decl, kind)
|
||||
select ref, decl, kind
|
||||
from Locatable e, ASTNode def, string kind
|
||||
where def = definitionOf(e, kind)
|
||||
select e, def, kind
|
||||
|
||||
188
javascript/ql/src/definitions.qll
Normal file
188
javascript/ql/src/definitions.qll
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* Provides classes and predicates related to jump-to-definition links
|
||||
* in the code viewer.
|
||||
*/
|
||||
|
||||
import javascript
|
||||
private import Declarations.Declarations
|
||||
|
||||
/**
|
||||
* Gets the kind of reference that `r` represents.
|
||||
*
|
||||
* References in callee position have kind `"M"` (for "method"), all
|
||||
* others have kind `"V"` (for "variable").
|
||||
*
|
||||
* For example, in the expression `f(x)`, `f` has kind `"M"` while
|
||||
* `x` has kind `"V"`.
|
||||
*/
|
||||
private string refKind(RefExpr r) {
|
||||
if exists(InvokeExpr invk | r = invk.getCallee().getUnderlyingReference())
|
||||
then result = "M"
|
||||
else result = "V"
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a class, function or object literal `va` may refer to.
|
||||
*/
|
||||
private ASTNode lookupDef(VarAccess va) {
|
||||
exists(AbstractValue av | av = va.analyze().getAValue() |
|
||||
result = av.(AbstractClass).getClass() or
|
||||
result = av.(AbstractFunction).getFunction() or
|
||||
result = av.(AbstractObjectLiteral).getObjectExpr()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `va` is of kind `kind` and `def` is the unique class,
|
||||
* function or object literal it refers to.
|
||||
*/
|
||||
private predicate variableDefLookup(VarAccess va, ASTNode def, string kind) {
|
||||
count(lookupDef(va)) = 1 and
|
||||
def = lookupDef(va) and
|
||||
kind = refKind(va)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if variable access `va` is of kind `kind` and refers to the
|
||||
* variable declaration.
|
||||
*
|
||||
* For example, in the statement `var x = 42, y = x;`, the initializing
|
||||
* expression of `y` is a variable access `x` of kind `"V"` that refers to
|
||||
* the declaration `x = 42`.
|
||||
*/
|
||||
private predicate variableDeclLookup(VarAccess va, VarDecl decl, string kind) {
|
||||
// restrict to declarations in same file to avoid accidentally picking up
|
||||
// unrelated global definitions
|
||||
decl = firstRefInTopLevel(va.getVariable(), Decl(), va.getTopLevel()) and
|
||||
kind = refKind(va)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if path expression `path`, which appears in a CommonJS `require`
|
||||
* call or an ES 2015 import statement, imports module `target`; `kind`
|
||||
* is always "I" (for "import").
|
||||
*
|
||||
* For example, in the statement `var a = require("./a")`, the path expression
|
||||
* `"./a"` imports a module `a` in the same folder.
|
||||
*/
|
||||
private predicate importLookup(ASTNode path, Module target, string kind) {
|
||||
kind = "I" and
|
||||
(
|
||||
exists(Import i |
|
||||
path = i.getImportedPath() and
|
||||
target = i.getImportedModule()
|
||||
)
|
||||
or
|
||||
exists(ReExportDeclaration red |
|
||||
path = red.getImportedPath() and
|
||||
target = red.getReExportedModule()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a node that may write the property read by `prn`.
|
||||
*/
|
||||
private ASTNode getAWrite(DataFlow::PropRead prn) {
|
||||
exists(DataFlow::AnalyzedNode base, DefiniteAbstractValue baseVal, string propName |
|
||||
base = prn.getBase() and
|
||||
propName = prn.getPropertyName() and
|
||||
baseVal = base.getAValue().getAPrototype*()
|
||||
|
|
||||
// write to a property on baseVal
|
||||
exists(AnalyzedPropertyWrite apw |
|
||||
result = apw.getAstNode() and
|
||||
apw.writes(baseVal, propName, _)
|
||||
)
|
||||
or
|
||||
// non-static class members aren't covered by `AnalyzedPropWrite`, so have to be handled
|
||||
// separately
|
||||
exists(ClassDefinition c, MemberDefinition m |
|
||||
m = c.getMember(propName) and
|
||||
baseVal.(AbstractInstance).getConstructor().(AbstractClass).getClass() = c and
|
||||
result = m.getNameExpr()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `prop` is the property name expression of a property read that
|
||||
* may read the property written by `write`. Furthermore, `write` must be the
|
||||
* only such property write. Parameter `kind` is always bound to `"M"`
|
||||
* at the moment.
|
||||
*/
|
||||
private predicate propertyLookup(Expr prop, ASTNode write, string kind) {
|
||||
exists(DataFlow::PropRead prn | prop = prn.getPropertyNameExpr() |
|
||||
count(getAWrite(prn)) = 1 and
|
||||
write = getAWrite(prn) and
|
||||
kind = "M"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `ref` is an identifier that refers to a type declared at `decl`.
|
||||
*/
|
||||
private predicate typeLookup(ASTNode ref, ASTNode decl, string kind) {
|
||||
exists(TypeAccess typeAccess |
|
||||
ref = typeAccess.getIdentifier() and
|
||||
decl = typeAccess.getTypeName().getADefinition() and
|
||||
kind = "T"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `ref` is the callee name of an invocation of `decl`.
|
||||
*/
|
||||
private predicate typedInvokeLookup(ASTNode ref, ASTNode decl, string kind) {
|
||||
not variableDefLookup(ref, decl, _) and
|
||||
not propertyLookup(ref, decl, _) and
|
||||
exists(InvokeExpr invoke, Expr callee |
|
||||
callee = invoke.getCallee().getUnderlyingReference() and
|
||||
(ref = callee.(Identifier) or ref = callee.(DotExpr).getPropertyNameExpr()) and
|
||||
decl = invoke.getResolvedCallee() and
|
||||
kind = "M"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `ref` is a JSDoc type annotation referring to a class defined at `decl`.
|
||||
*/
|
||||
private predicate jsdocTypeLookup(JSDocNamedTypeExpr ref, ASTNode decl, string kind) {
|
||||
decl = ref.getClass().getAstNode() and
|
||||
kind = "T"
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an element, of kind `kind`, that element `e` uses, if any.
|
||||
*
|
||||
* The `kind` is a string representing what kind of use it is:
|
||||
* - `"M"` for function and method calls
|
||||
* - `"T"` for uses of types
|
||||
* - `"V"` for variable accesses
|
||||
* - `"I"` for imports
|
||||
*/
|
||||
cached
|
||||
ASTNode definitionOf(Locatable e, string kind) {
|
||||
variableDefLookup(e, result, kind)
|
||||
or
|
||||
// prefer definitions over declarations
|
||||
not variableDefLookup(e, _, _) and variableDeclLookup(e, result, kind)
|
||||
or
|
||||
importLookup(e, result, kind)
|
||||
or
|
||||
propertyLookup(e, result, kind)
|
||||
or
|
||||
typeLookup(e, result, kind)
|
||||
or
|
||||
typedInvokeLookup(e, result, kind)
|
||||
or
|
||||
jsdocTypeLookup(e, result, kind)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an appropriately encoded version of a filename `name`
|
||||
* passed by the VS Code extension in order to coincide with the
|
||||
* output of `.getFile()` on locatable entities.
|
||||
*/
|
||||
cached
|
||||
File getEncodedFile(string name) { result.getAbsolutePath().replaceAll(":", "_") = name }
|
||||
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
|
||||
<p>If you use cross-origin communication between Window objects and do expect to receive messages from other sites, always verify the sender's identity using the origin and possibly source properties of the recevied `MessageEvent`. </p>
|
||||
|
||||
<p>Unexpected behaviours, like `DOM-based XSS` could occur, if the event handler for incoming data does not check the origin of the data received and handles the data in an unsafe way.</p>
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>
|
||||
Always verify the sender's identity of incoming messages.
|
||||
</p>
|
||||
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>In the first example, the `MessageEvent.data` is passed to the `eval` function withouth checking the origin. This means that any window can send arbitrary messages that will be executed in the window receiving the message</p>
|
||||
<sample src="examples/postMessageNoOriginCheck.js" />
|
||||
|
||||
<p> In the second example, the `MessageEvent.origin` is verified with an unsecure check. For example, using `event.origin.indexOf('www.example.com') > -1` can be bypassed because the string `www.example.com` could appear anywhere in `event.origin` (i.e. `www.example.com.mydomain.com`)</p>
|
||||
<sample src="examples/postMessageInsufficientCheck.js" />
|
||||
|
||||
<p> In the third example, the `MessageEvent.origin` is properly checked against a trusted origin. </p>
|
||||
<sample src="examples/postMessageWithOriginCheck.js" />
|
||||
|
||||
</example>
|
||||
|
||||
<references>
|
||||
|
||||
<li><a href="https://cwe.mitre.org/data/definitions/20.html">CWE-20: Improper Input Validation</a></li>
|
||||
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage">Window.postMessage()</a></li>
|
||||
<li><a href="https://portswigger.net/web-security/dom-based/web-message-manipulation">Web-message manipulation</a></li>
|
||||
<li><a href="https://labs.detectify.com/2016/12/08/the-pitfalls-of-postmessage/">The pitfalls of postMessage</a></li>
|
||||
|
||||
</references>
|
||||
</qhelp>
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @name Missing `MessageEvent.origin` verification in `postMessage` handlers
|
||||
* @description Missing the `MessageEvent.origin` verification in `postMessage` handlers, allows any windows to send arbitrary data to the `MessageEvent` listener.
|
||||
* This could lead to unexpected behaviour, especially when `MessageEvent.data` is used in an unsafe way.
|
||||
* @kind problem
|
||||
* @problem.severity warning
|
||||
* @precision high
|
||||
* @id js/missing-postmessageorigin-verification
|
||||
* @tags correctness
|
||||
* security
|
||||
* external/cwe/cwe-20
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import semmle.javascript.security.dataflow.DOM
|
||||
|
||||
/**
|
||||
* A method call for the insecure functions used to verify the `MessageEvent.origin`.
|
||||
*/
|
||||
class InsufficientOriginChecks extends DataFlow::Node {
|
||||
InsufficientOriginChecks() {
|
||||
exists(DataFlow::Node node |
|
||||
this.(StringOps::StartsWith).getSubstring() = node or
|
||||
this.(StringOps::Includes).getSubstring() = node or
|
||||
this.(StringOps::EndsWith).getSubstring() = node
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A function handler for the `MessageEvent`.
|
||||
*/
|
||||
class PostMessageHandler extends DataFlow::FunctionNode {
|
||||
PostMessageHandler() { this.getFunction() instanceof PostMessageEventHandler }
|
||||
}
|
||||
|
||||
/**
|
||||
* The `MessageEvent` parameter received by the handler
|
||||
*/
|
||||
class PostMessageEvent extends DataFlow::SourceNode {
|
||||
PostMessageEvent() { exists(PostMessageHandler handler | this = handler.getParameter(0)) }
|
||||
|
||||
/**
|
||||
* Holds if an access on `MessageEvent.origin` is in an `EqualityTest` and there is no call of an insufficient verification method on `MessageEvent.origin`
|
||||
*/
|
||||
predicate hasOriginChecked() {
|
||||
exists(EqualityTest test |
|
||||
this.getAPropertyRead(["origin", "source"]).flowsToExpr(test.getAnOperand())
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if there is an insufficient method call (i.e indexOf) used to verify `MessageEvent.origin`
|
||||
*/
|
||||
predicate hasOriginInsufficientlyChecked() {
|
||||
exists(InsufficientOriginChecks insufficientChecks |
|
||||
this.getAPropertyRead("origin").getAMethodCall*() = insufficientChecks
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
from PostMessageEvent event
|
||||
where not event.hasOriginChecked() or event.hasOriginInsufficientlyChecked()
|
||||
select event, "Missing or unsafe origin verification."
|
||||
@@ -0,0 +1,14 @@
|
||||
function postMessageHandler(event) {
|
||||
let origin = event.origin.toLowerCase();
|
||||
|
||||
let host = window.location.host;
|
||||
|
||||
// BAD
|
||||
if (origin.indexOf(host) === -1)
|
||||
return;
|
||||
|
||||
|
||||
eval(event.data);
|
||||
}
|
||||
|
||||
window.addEventListener('message', postMessageHandler, false);
|
||||
@@ -0,0 +1,9 @@
|
||||
function postMessageHandler(event) {
|
||||
let origin = event.origin.toLowerCase();
|
||||
|
||||
console.log(origin)
|
||||
// BAD: the origin property is not checked
|
||||
eval(event.data);
|
||||
}
|
||||
|
||||
window.addEventListener('message', postMessageHandler, false);
|
||||
@@ -0,0 +1,9 @@
|
||||
function postMessageHandler(event) {
|
||||
console.log(event.origin)
|
||||
// GOOD: the origin property is checked
|
||||
if (event.origin === 'www.example.com') {
|
||||
// do something
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', postMessageHandler, false);
|
||||
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>
|
||||
Server-Side Template Injection vulnerabilities occur when user input is embedded
|
||||
in a template in an unsafe manner allowing attackers to access the template context and
|
||||
run arbitrary code on the application server.
|
||||
</p>
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>
|
||||
Avoid including user input in any expression or template which may be dynamically rendered.
|
||||
If user input must be included, use context-specific escaping before including it or run
|
||||
the rendering engine with sandbox options.
|
||||
</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>
|
||||
The following example shows a page being rendered with user input allowing attackers to access the
|
||||
template context and run arbitrary code on the application server.
|
||||
The Pug template engine (and other template engines) provides an interpolation feature - insertion of variable values into a string of some kind.
|
||||
For example, <code>Hello #{user.username}!</code>, could be used for printing a username from a scoped variable user,
|
||||
but the <code>user.username</code> expression will be executed as JavaScript.
|
||||
Unsafe injection of user input in a template therefore allows an attacker to inject arbitrary JavaScript code.
|
||||
For example, a payload of <code>#{global.process.exit(1)}</code> will cause the below server to crash.
|
||||
</p>
|
||||
|
||||
<sample src="examples/ServerSideTemplateInjection.js" />
|
||||
</example>
|
||||
|
||||
<example>
|
||||
<p>
|
||||
The example below provides an example of how to use a template engine without any risk of Server-Side Template Injection.
|
||||
Instead of concatenating user input onto the template, the template uses a placeholder and safely inserts
|
||||
the user input.
|
||||
</p>
|
||||
|
||||
<sample src="examples/ServerSideTemplateInjectionSafe.js" />
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>
|
||||
OWASP:
|
||||
<a href="https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/18-Testing_for_Server_Side_Template_Injection">Server Side Template Injection</a>.
|
||||
</li>
|
||||
<li>
|
||||
PortSwigger Research Blog:
|
||||
<a href="https://portswigger.net/research/server-side-template-injection">Server-Side Template Injection</a>.
|
||||
</li>
|
||||
</references>
|
||||
</qhelp>
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @name Server Side Template Injection
|
||||
* @description Rendering templates with unsanitized user input allows a malicious user arbitrary
|
||||
* code execution.
|
||||
* @kind path-problem
|
||||
* @problem.severity error
|
||||
* @precision high
|
||||
* @id js/server-side-template-injection
|
||||
* @tags security
|
||||
* external/cwe/cwe-094
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import DataFlow
|
||||
import DataFlow::PathGraph
|
||||
|
||||
class ServerSideTemplateInjectionConfiguration extends TaintTracking::Configuration {
|
||||
ServerSideTemplateInjectionConfiguration() { this = "ServerSideTemplateInjectionConfiguration" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) { sink instanceof ServerSideTemplateInjectionSink }
|
||||
}
|
||||
|
||||
abstract class ServerSideTemplateInjectionSink extends DataFlow::Node { }
|
||||
|
||||
class SSTIPugSink extends ServerSideTemplateInjectionSink {
|
||||
SSTIPugSink() {
|
||||
exists(CallNode compile, ModuleImportNode renderImport |
|
||||
renderImport = moduleImport(["pug", "jade"]) and
|
||||
(
|
||||
compile = renderImport.getAMemberCall("compile")
|
||||
or
|
||||
compile = renderImport.getAMemberCall("render")
|
||||
) and
|
||||
this = compile.getArgument(0)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SSTIDotSink extends ServerSideTemplateInjectionSink {
|
||||
SSTIDotSink() {
|
||||
exists(CallNode compile |
|
||||
compile = moduleImport("dot").getAMemberCall("template") and
|
||||
this = compile.getArgument(0)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SSTIEjsSink extends ServerSideTemplateInjectionSink {
|
||||
SSTIEjsSink() { this = moduleImport("ejs").getAMemberCall("render").getArgument(0) }
|
||||
}
|
||||
|
||||
class SSTINunjucksSink extends ServerSideTemplateInjectionSink {
|
||||
SSTINunjucksSink() {
|
||||
this = moduleImport("nunjucks").getAMemberCall("renderString").getArgument(0)
|
||||
}
|
||||
}
|
||||
|
||||
from DataFlow::PathNode source, DataFlow::PathNode sink, ServerSideTemplateInjectionConfiguration c
|
||||
where c.hasFlowPath(source, sink)
|
||||
select sink.getNode(), source, sink,
|
||||
"$@ flows to here and unsafely used as part of rendered template", source.getNode(),
|
||||
"User-provided value"
|
||||
@@ -0,0 +1,35 @@
|
||||
const express = require('express')
|
||||
var bodyParser = require('body-parser');
|
||||
const app = express()
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
//Dependent of Templating engine
|
||||
var jade = require('pug');
|
||||
const port = 5061
|
||||
|
||||
function getHTML(input) {
|
||||
var template = `
|
||||
doctype
|
||||
html
|
||||
head
|
||||
title= 'Hello world'
|
||||
body
|
||||
form(action='/' method='post')
|
||||
label(for='name') Name:
|
||||
input#name.form-control(type='text', placeholder='' name='name')
|
||||
button.btn.btn-primary(type='submit') Submit
|
||||
p Hello `+ input
|
||||
var fn = jade.compile(template);
|
||||
var html = fn();
|
||||
console.log(html);
|
||||
return html;
|
||||
}
|
||||
|
||||
app.post('/', (request, response) => {
|
||||
var input = request.param('name', "")
|
||||
var html = getHTML(input)
|
||||
response.send(html);
|
||||
})
|
||||
|
||||
app.listen(port, () => { console.log(`server is listening on ${port}`) })
|
||||
@@ -0,0 +1,34 @@
|
||||
const express = require('express')
|
||||
var bodyParser = require('body-parser');
|
||||
const app = express()
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
//Dependent of Templating engine
|
||||
var jade = require('pug');
|
||||
const port = 5061
|
||||
|
||||
function getHTML(input) {
|
||||
var template = `
|
||||
doctype
|
||||
html
|
||||
head
|
||||
title= 'Hello world'
|
||||
body
|
||||
form(action='/' method='post')
|
||||
label(for='name') Name:
|
||||
input#name.form-control(type='text', placeholder='' name='name')
|
||||
button.btn.btn-primary(type='submit') Submit
|
||||
p Hello #{username}`
|
||||
var fn = jade.compile(template);
|
||||
var html = fn({username: input});
|
||||
console.log(html);
|
||||
return html;
|
||||
}
|
||||
|
||||
app.post('/', (request, response) => {
|
||||
var input = request.param('name', "")
|
||||
var html = getHTML(input)
|
||||
response.send(html);
|
||||
})
|
||||
|
||||
app.listen(port, () => { console.log(`server is listening on ${port}`) })
|
||||
325
javascript/ql/src/experimental/poi/PoI.qll
Normal file
325
javascript/ql/src/experimental/poi/PoI.qll
Normal file
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* Provides classes and predicates for discovering points of interest
|
||||
* in an unknown code base.
|
||||
*
|
||||
* To use this module, subclass the
|
||||
* `ActivePoI` class, override *one* of its `is` predicates, and use
|
||||
* `alertQuery` as a `@kind problem` query . This will present
|
||||
* the desired points of interest as alerts that are easily browsable
|
||||
* in a codeql IDE. By itself, this is no different from an ordinary
|
||||
* query, but the strength of this module lies in its extensibility
|
||||
* and standard library:
|
||||
*
|
||||
* - points of interest can be added, removed and mixed seamlessly
|
||||
* - this module comes with a collection of standard points of interest (see `StandardPoIs`)
|
||||
*
|
||||
* A global configuration for the points of interest (see
|
||||
* `PoIConfiguration`) can be used to easily manage multiple points of
|
||||
* interests, and to restrict the points of interest to specific
|
||||
* corners of the code base.
|
||||
*
|
||||
* Below is an example use of this module that will produce an alert
|
||||
* for each route handler and route handler setup in a file named
|
||||
* "server-core.js". The route setup alerts will contain a link to its
|
||||
* associated route handler.
|
||||
*
|
||||
* ```
|
||||
* /**
|
||||
* * @kind problem
|
||||
* *\/
|
||||
*
|
||||
* import PoI
|
||||
*
|
||||
* class Configuration extends PoIConfiguration {
|
||||
* Configuration() { this = "Configuration" }
|
||||
*
|
||||
* override predicate shown(DataFlow::Node n) { n.getFile().getBaseName() = "server-core.js" }
|
||||
* }
|
||||
*
|
||||
* class RouteHandlerPoI extends ActivePoI {
|
||||
* RouteHandlerPoI() { this = "RouteHandlerPoI" }
|
||||
* override predicate is(DataFlow::Node l0) { l0 instanceof Express::RouteHandler }
|
||||
* }
|
||||
*
|
||||
* class RouteSetupAndRouteHandlerPoI extends ActivePoI {
|
||||
* RouteSetupAndRouteHandlerPoI() { this = "RouteSetupAndRouteHandlerPoI" }
|
||||
*
|
||||
* override predicate is(DataFlow::Node l0, DataFlow::Node l1, string t1) {
|
||||
* l0.asExpr().(Express::RouteSetup).getARouteHandler() = l1 and t1 = "routehandler"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* query predicate problems = alertQuery/6;
|
||||
* ```
|
||||
*/
|
||||
|
||||
import javascript
|
||||
private import DataFlow
|
||||
private import filters.ClassifyFiles
|
||||
private import semmle.javascript.RestrictedLocations
|
||||
|
||||
/**
|
||||
* Provides often used points of interest.
|
||||
*
|
||||
* Note that these points of interest should not extend
|
||||
* `ActivePoI`, and that they can be enabled on
|
||||
* demand like this:
|
||||
*
|
||||
* ```
|
||||
* class MyPoI extends ServerRelatedPoI, ActivePoI {}
|
||||
* ```
|
||||
*/
|
||||
private module StandardPoIs {
|
||||
/**
|
||||
* An unpromoted route setup candidate.
|
||||
*/
|
||||
class UnpromotedRouteSetupPoI extends PoI {
|
||||
UnpromotedRouteSetupPoI() { this = "UnpromotedRouteSetupPoI" }
|
||||
|
||||
override predicate is(Node l0) {
|
||||
l0 instanceof HTTP::RouteSetupCandidate and not l0.asExpr() instanceof HTTP::RouteSetup
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An unpromoted route handler candidate.
|
||||
*/
|
||||
class UnpromotedRouteHandlerPoI extends PoI {
|
||||
UnpromotedRouteHandlerPoI() { this = "UnpromotedRouteHandlerPoI" }
|
||||
|
||||
override predicate is(Node l0) {
|
||||
l0 instanceof HTTP::RouteHandlerCandidate and not l0 instanceof HTTP::RouteHandler
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An unpromoted route handler candidate, with explanatory data flow information.
|
||||
*/
|
||||
class UnpromotedRouteHandlerWithFlowPoI extends PoI {
|
||||
UnpromotedRouteHandlerWithFlowPoI() { this = "UnpromotedRouteHandlerWithFlowPoI" }
|
||||
|
||||
private DataFlow::SourceNode track(HTTP::RouteHandlerCandidate cand, DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result = cand
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = track(cand, t2).track(t2, t))
|
||||
}
|
||||
|
||||
override predicate is(Node l0, Node l1, string t1) {
|
||||
l0 instanceof HTTP::RouteHandlerCandidate and
|
||||
not l0 instanceof HTTP::RouteHandler and
|
||||
l1 = track(l0, TypeTracker::end()) and
|
||||
(if l1 = l0 then t1 = "ends here" else t1 = "starts/ends here")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A callee that is unknown.
|
||||
*/
|
||||
class UnknownCalleePoI extends PoI {
|
||||
UnknownCalleePoI() { this = "UnknownCalleePoI" }
|
||||
|
||||
override predicate is(Node l0) {
|
||||
exists(InvokeNode invk | l0 = invk.getCalleeNode() and not exists(invk.getACallee()))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of remote flow.
|
||||
*/
|
||||
class RemoteFlowSourcePoI extends PoI {
|
||||
RemoteFlowSourcePoI() { this = "RemoteFlowSourcePoI" }
|
||||
|
||||
override predicate is(Node l0) { l0 instanceof RemoteFlowSource }
|
||||
}
|
||||
|
||||
/**
|
||||
* A "source" for any active configuration.
|
||||
*/
|
||||
class SourcePoI extends PoI {
|
||||
SourcePoI() { this = "SourcePoI" }
|
||||
|
||||
override predicate is(Node l0) {
|
||||
exists(Configuration cfg | cfg.isSource(l0) or cfg.isSource(l0, _))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A "sink" for any active configuration.
|
||||
*/
|
||||
class SinkPoI extends PoI {
|
||||
SinkPoI() { this = "SinkPoI" }
|
||||
|
||||
override predicate is(Node l0) {
|
||||
exists(Configuration cfg | cfg.isSink(l0) or cfg.isSink(l0, _))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A "barrier" for any active configuration.
|
||||
*/
|
||||
class BarrierPoI extends PoI {
|
||||
BarrierPoI() { this = "BarrierPoI" }
|
||||
|
||||
override predicate is(Node l0) {
|
||||
exists(Configuration cfg |
|
||||
cfg.isBarrier(l0) or
|
||||
cfg.isBarrierEdge(l0, _) or
|
||||
cfg.isBarrierEdge(l0, _, _) or
|
||||
cfg.isLabeledBarrier(l0, _)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides groups of often used points of interest.
|
||||
*/
|
||||
module StandardPoIGroups {
|
||||
/**
|
||||
* A server-related point of interest.
|
||||
*/
|
||||
class ServerRelatedPoI extends PoI {
|
||||
ServerRelatedPoI() {
|
||||
this instanceof UnpromotedRouteSetupPoI or
|
||||
this instanceof UnpromotedRouteHandlerPoI or
|
||||
this instanceof UnpromotedRouteHandlerWithFlowPoI
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A configuration-related point of interest.
|
||||
*/
|
||||
class DataFlowConfigurationPoI extends PoI {
|
||||
DataFlowConfigurationPoI() {
|
||||
this instanceof SourcePoI or
|
||||
this instanceof SinkPoI
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
import StandardPoIGroups
|
||||
}
|
||||
|
||||
import StandardPoIs
|
||||
|
||||
/**
|
||||
* A tagging interface for a custom point of interest that should be
|
||||
* enabled in the absence of an explicit
|
||||
* `PoIConfiguration::enabled/1`.
|
||||
*/
|
||||
abstract class ActivePoI extends PoI {
|
||||
bindingset[this]
|
||||
ActivePoI() { any() }
|
||||
}
|
||||
|
||||
private module PoIConfigDefaults {
|
||||
predicate enabled(PoI poi) { poi instanceof ActivePoI }
|
||||
|
||||
predicate shown(Node n) { not classify(n.getFile(), _) }
|
||||
}
|
||||
|
||||
/**
|
||||
* A configuration for the points of interest to display.
|
||||
*/
|
||||
abstract class PoIConfiguration extends string {
|
||||
bindingset[this]
|
||||
PoIConfiguration() { any() }
|
||||
|
||||
/**
|
||||
* Holds if the points of interest from `poi` should be shown.
|
||||
*/
|
||||
predicate enabled(PoI poi) { PoIConfigDefaults::enabled(poi) }
|
||||
|
||||
/**
|
||||
* Holds if the points of interest `n` should be shown.
|
||||
*/
|
||||
predicate shown(Node n) { PoIConfigDefaults::shown(n) }
|
||||
}
|
||||
|
||||
/**
|
||||
* A class of points of interest.
|
||||
*
|
||||
* Note that only one of the `is/1`, `is/3`, `is/5` methods should
|
||||
* be overridden, as two overrides will degrade the alert UI
|
||||
* slightly.
|
||||
*/
|
||||
abstract class PoI extends string {
|
||||
bindingset[this]
|
||||
PoI() { any() }
|
||||
|
||||
/**
|
||||
* Holds if `l0` is a point of interest.
|
||||
*/
|
||||
predicate is(Node l0) { none() }
|
||||
|
||||
/**
|
||||
* Holds if `l0` is a point of interest, with `l1` as an auxiliary location described by `t1`.
|
||||
*/
|
||||
predicate is(Node l0, Node l1, string t1) { none() }
|
||||
|
||||
/**
|
||||
* Holds if `l0` is a point of interest, with `l1` and `l2` as auxiliary locations described by `t1` and `t2`.
|
||||
*/
|
||||
predicate is(Node l0, Node l1, string t1, Node l2, string t2) { none() }
|
||||
|
||||
/**
|
||||
* Gets the message format for the point of interest.
|
||||
*/
|
||||
string getFormat() {
|
||||
is(_) and result = ""
|
||||
or
|
||||
is(_, _, _) and result = "$@"
|
||||
or
|
||||
is(_, _, _, _, _) and result = "$@ $@"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An alert query for a point of interest.
|
||||
*
|
||||
* Should be used as:
|
||||
*
|
||||
* ```
|
||||
* query predicate problems = alertQuery/6;
|
||||
* ```
|
||||
*
|
||||
* Or alternatively:
|
||||
*
|
||||
* ```
|
||||
* from Locatable l1line, string msg, Node l2, string s2, Node l3, string s3
|
||||
* where alertQuery(l1line, msg, l2, s2, l3, s3)
|
||||
* select l1line, msg, l2, s2, l3, s3
|
||||
* ```
|
||||
*
|
||||
* Note that some points of interest do not have auxiliary
|
||||
* locations, so `l2`,`l3`, `s2`, `s3` may have placeholder values.
|
||||
*/
|
||||
predicate alertQuery(Locatable l1line, string msg, Node l2, string s2, Node l3, string s3) {
|
||||
exists(PoI poi, Node l1, string m |
|
||||
l1.getAstNode().(FirstLineOf) = l1line and
|
||||
(
|
||||
not exists(PoIConfiguration cfg) and
|
||||
PoIConfigDefaults::enabled(poi) and
|
||||
PoIConfigDefaults::shown(l1)
|
||||
or
|
||||
exists(PoIConfiguration cfg |
|
||||
cfg.enabled(poi) and
|
||||
cfg.shown(l1)
|
||||
)
|
||||
) and
|
||||
m = poi.getFormat() and
|
||||
if m = "" then msg = poi else msg = poi + ": " + m
|
||||
|
|
||||
poi.is(l1) and
|
||||
l1 = l2 and
|
||||
s2 = "irrelevant" and
|
||||
l1 = l3 and
|
||||
s3 = "irrelevant"
|
||||
or
|
||||
poi.is(l1, l2, s2) and
|
||||
l1 = l3 and
|
||||
s3 = "irrelevant"
|
||||
or
|
||||
poi.is(l1, l2, s2, l3, s3)
|
||||
)
|
||||
}
|
||||
14
javascript/ql/src/external/CodeDuplication.qll
vendored
14
javascript/ql/src/external/CodeDuplication.qll
vendored
@@ -261,6 +261,11 @@ predicate similarContainers(StmtContainer sc, StmtContainer other, float percent
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL: do not use.
|
||||
*
|
||||
* Holds if `line` in `f` is similar to a line somewhere else.
|
||||
*/
|
||||
predicate similarLines(File f, int line) {
|
||||
exists(SimilarBlock b | b.sourceFile() = f and line in [b.sourceStartLine() .. b.sourceEndLine()])
|
||||
}
|
||||
@@ -275,6 +280,7 @@ private predicate similarLinesPerEquivalenceClass(int equivClass, int lines, Fil
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if `coveredLines` lines of `f` are similar to lines in `otherFile`. */
|
||||
pragma[noopt]
|
||||
private predicate similarLinesCovered(File f, int coveredLines, File otherFile) {
|
||||
exists(int numLines | numLines = f.getNumberOfLines() |
|
||||
@@ -296,6 +302,11 @@ private predicate similarLinesCovered(File f, int coveredLines, File otherFile)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL: do not use.
|
||||
*
|
||||
* Holds if `line` in `f` is duplicated by a line somewhere else.
|
||||
*/
|
||||
predicate duplicateLines(File f, int line) {
|
||||
exists(DuplicateBlock b |
|
||||
b.sourceFile() = f and line in [b.sourceStartLine() .. b.sourceEndLine()]
|
||||
@@ -312,6 +323,7 @@ private predicate duplicateLinesPerEquivalenceClass(int equivClass, int lines, F
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if `coveredLines` lines of `f` are duplicates of lines in `otherFile`. */
|
||||
pragma[noopt]
|
||||
private predicate duplicateLinesCovered(File f, int coveredLines, File otherFile) {
|
||||
exists(int numLines | numLines = f.getNumberOfLines() |
|
||||
@@ -333,6 +345,7 @@ private predicate duplicateLinesCovered(File f, int coveredLines, File otherFile
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if most of `f` (`percent`%) is similar to `other`. */
|
||||
predicate similarFiles(File f, File other, int percent) {
|
||||
exists(int covered, int total |
|
||||
similarLinesCovered(f, covered, other) and
|
||||
@@ -343,6 +356,7 @@ predicate similarFiles(File f, File other, int percent) {
|
||||
not duplicateFiles(f, other, _)
|
||||
}
|
||||
|
||||
/** Holds if most of `f` (`percent`%) is duplicated by `other`. */
|
||||
predicate duplicateFiles(File f, File other, int percent) {
|
||||
exists(int covered, int total |
|
||||
duplicateLinesCovered(f, covered, other) and
|
||||
|
||||
@@ -7,85 +7,8 @@
|
||||
* @id js/file-classifier
|
||||
*/
|
||||
|
||||
import semmle.javascript.GeneratedCode
|
||||
import semmle.javascript.frameworks.Testing
|
||||
import semmle.javascript.frameworks.Templating
|
||||
import semmle.javascript.dependencies.FrameworkLibraries
|
||||
|
||||
/**
|
||||
* Holds if `e` may be caused by parsing a template file as plain HTML or JavaScript.
|
||||
*
|
||||
* We use two heuristics: check for the presence of a known template delimiter preceding
|
||||
* the error on the same line, and check whether the file name contains `template` or
|
||||
* `templates`.
|
||||
*/
|
||||
predicate maybeCausedByTemplate(JSParseError e) {
|
||||
exists(File f | f = e.getFile() |
|
||||
e.getLine().indexOf(Templating::getADelimiter()) <= e.getLocation().getStartColumn()
|
||||
or
|
||||
f.getAbsolutePath().regexpMatch("(?i).*\\btemplates?\\b.*")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `e` is an expression in the form `o.p1.p2.p3....pn`.
|
||||
*/
|
||||
private predicate isNestedDotExpr(DotExpr e) {
|
||||
e.getBase() instanceof VarAccess or
|
||||
isNestedDotExpr(e.getBase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `tl` only contains variable declarations and field reads.
|
||||
*/
|
||||
private predicate looksLikeExterns(TopLevel tl) {
|
||||
forex(Stmt s | s.getParent() = tl |
|
||||
exists(VarDeclStmt vds | vds = s |
|
||||
forall(VariableDeclarator vd | vd = vds.getADecl() | not exists(vd.getInit()))
|
||||
)
|
||||
or
|
||||
isNestedDotExpr(s.(ExprStmt).getExpr())
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `f` is classified as belonging to `category`.
|
||||
*
|
||||
* There are currently four categories:
|
||||
* - `"generated"`: `f` contains generated or minified code;
|
||||
* - `"test"`: `f` contains test code;
|
||||
* - `"externs"`: `f` contains externs declarations;
|
||||
* - `"library"`: `f` contains library code;
|
||||
* - `"template"`: `f` contains template code.
|
||||
*/
|
||||
predicate classify(File f, string category) {
|
||||
isGenerated(f.getATopLevel()) and category = "generated"
|
||||
or
|
||||
(
|
||||
exists(Test t | t.getFile() = f)
|
||||
or
|
||||
exists(string stemExt | stemExt = "test" or stemExt = "spec" |
|
||||
f = getTestFile(any(File orig), stemExt)
|
||||
)
|
||||
or
|
||||
f.getAbsolutePath().regexpMatch(".*/__(mocks|tests)__/.*")
|
||||
) and
|
||||
category = "test"
|
||||
or
|
||||
(f.getATopLevel().isExterns() or looksLikeExterns(f.getATopLevel())) and
|
||||
category = "externs"
|
||||
or
|
||||
f.getATopLevel() instanceof FrameworkLibraryInstance and category = "library"
|
||||
or
|
||||
exists(JSParseError err | maybeCausedByTemplate(err) |
|
||||
f = err.getFile() and category = "template"
|
||||
)
|
||||
or
|
||||
// Polymer templates
|
||||
exists(HTML::Element elt | elt.getName() = "template" |
|
||||
f = elt.getFile() and category = "template"
|
||||
)
|
||||
}
|
||||
import javascript
|
||||
import ClassifyFiles
|
||||
|
||||
from File f, string category
|
||||
where classify(f, category)
|
||||
|
||||
85
javascript/ql/src/filters/ClassifyFiles.qll
Normal file
85
javascript/ql/src/filters/ClassifyFiles.qll
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Provides classes and predicates for classifying files as containing
|
||||
* generated code, test code, externs declarations, library code or
|
||||
* template code.
|
||||
*/
|
||||
|
||||
import semmle.javascript.GeneratedCode
|
||||
import semmle.javascript.frameworks.Testing
|
||||
import semmle.javascript.frameworks.Templating
|
||||
import semmle.javascript.dependencies.FrameworkLibraries
|
||||
|
||||
/**
|
||||
* Holds if `e` may be caused by parsing a template file as plain HTML or JavaScript.
|
||||
*
|
||||
* We use two heuristics: check for the presence of a known template delimiter preceding
|
||||
* the error on the same line, and check whether the file name contains `template` or
|
||||
* `templates`.
|
||||
*/
|
||||
predicate maybeCausedByTemplate(JSParseError e) {
|
||||
exists(File f | f = e.getFile() |
|
||||
e.getLine().indexOf(Templating::getADelimiter()) <= e.getLocation().getStartColumn()
|
||||
or
|
||||
f.getAbsolutePath().regexpMatch("(?i).*\\btemplates?\\b.*")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `e` is an expression in the form `o.p1.p2.p3....pn`.
|
||||
*/
|
||||
private predicate isNestedDotExpr(DotExpr e) {
|
||||
e.getBase() instanceof VarAccess or
|
||||
isNestedDotExpr(e.getBase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `tl` only contains variable declarations and field reads.
|
||||
*/
|
||||
private predicate looksLikeExterns(TopLevel tl) {
|
||||
forex(Stmt s | s.getParent() = tl |
|
||||
exists(VarDeclStmt vds | vds = s |
|
||||
forall(VariableDeclarator vd | vd = vds.getADecl() | not exists(vd.getInit()))
|
||||
)
|
||||
or
|
||||
isNestedDotExpr(s.(ExprStmt).getExpr())
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `f` is classified as belonging to `category`.
|
||||
*
|
||||
* There are currently four categories:
|
||||
* - `"generated"`: `f` contains generated or minified code;
|
||||
* - `"test"`: `f` contains test code;
|
||||
* - `"externs"`: `f` contains externs declarations;
|
||||
* - `"library"`: `f` contains library code;
|
||||
* - `"template"`: `f` contains template code.
|
||||
*/
|
||||
predicate classify(File f, string category) {
|
||||
isGenerated(f.getATopLevel()) and category = "generated"
|
||||
or
|
||||
(
|
||||
exists(Test t | t.getFile() = f)
|
||||
or
|
||||
exists(string stemExt | stemExt = "test" or stemExt = "spec" |
|
||||
f = getTestFile(any(File orig), stemExt)
|
||||
)
|
||||
or
|
||||
f.getAbsolutePath().regexpMatch(".*/__(mocks|tests)__/.*")
|
||||
) and
|
||||
category = "test"
|
||||
or
|
||||
(f.getATopLevel().isExterns() or looksLikeExterns(f.getATopLevel())) and
|
||||
category = "externs"
|
||||
or
|
||||
f.getATopLevel() instanceof FrameworkLibraryInstance and category = "library"
|
||||
or
|
||||
exists(JSParseError err | maybeCausedByTemplate(err) |
|
||||
f = err.getFile() and category = "template"
|
||||
)
|
||||
or
|
||||
// Polymer templates
|
||||
exists(HTML::Element elt | elt.getName() = "template" |
|
||||
f = elt.getFile() and category = "template"
|
||||
)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import semmle.javascript.Base64
|
||||
import semmle.javascript.CFG
|
||||
import semmle.javascript.Classes
|
||||
import semmle.javascript.Closure
|
||||
import semmle.javascript.Collections
|
||||
import semmle.javascript.Comments
|
||||
import semmle.javascript.Concepts
|
||||
import semmle.javascript.Constants
|
||||
@@ -36,6 +37,7 @@ import semmle.javascript.JsonParsers
|
||||
import semmle.javascript.JSX
|
||||
import semmle.javascript.Lines
|
||||
import semmle.javascript.Locations
|
||||
import semmle.javascript.MembershipCandidates
|
||||
import semmle.javascript.Modules
|
||||
import semmle.javascript.NodeJS
|
||||
import semmle.javascript.NPM
|
||||
|
||||
16
javascript/ql/src/localDefinitions.ql
Normal file
16
javascript/ql/src/localDefinitions.ql
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @name Jump-to-definition links
|
||||
* @description Generates use-definition pairs that provide the data
|
||||
* for jump-to-definition in the code viewer.
|
||||
* @kind definitions
|
||||
* @id js/ide-jump-to-definition
|
||||
* @tags ide-contextual-queries/local-definitions
|
||||
*/
|
||||
|
||||
import definitions
|
||||
|
||||
external string selectedSourceFile();
|
||||
|
||||
from Locatable e, ASTNode def, string kind
|
||||
where def = definitionOf(e, kind) and e.getFile() = getEncodedFile(selectedSourceFile())
|
||||
select e, def, kind
|
||||
16
javascript/ql/src/localReferences.ql
Normal file
16
javascript/ql/src/localReferences.ql
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @name Find-references links
|
||||
* @description Generates use-definition pairs that provide the data
|
||||
* for find-references in the code viewer.
|
||||
* @kind definitions
|
||||
* @id js/ide-find-references
|
||||
* @tags ide-contextual-queries/local-references
|
||||
*/
|
||||
|
||||
import definitions
|
||||
|
||||
external string selectedSourceFile();
|
||||
|
||||
from Locatable e, ASTNode def, string kind
|
||||
where def = definitionOf(e, kind) and def.getFile() = getEncodedFile(selectedSourceFile())
|
||||
select e, def, kind
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user