diff --git a/javascript/externs/web/w3c_dom1.js b/javascript/externs/web/w3c_dom1.js
index 6f43c00a3efc..bf85793fec6b 100644
--- a/javascript/externs/web/w3c_dom1.js
+++ b/javascript/externs/web/w3c_dom1.js
@@ -25,9 +25,11 @@
/**
* @constructor
+ * @param {string=} message
+ * @param {string=} message
* @see http://www.w3.org/TR/1998/REC-DOM-Level-1-19981001/level-one-core.html#ID-17189187
*/
-function DOMException() {}
+function DOMException(message, name) {}
/**
* @type {number}
diff --git a/javascript/ql/src/Expressions/ImplicitOperandConversion.qhelp b/javascript/ql/src/Expressions/ImplicitOperandConversion.qhelp
index 7313c503e78d..667db45513e1 100644
--- a/javascript/ql/src/Expressions/ImplicitOperandConversion.qhelp
+++ b/javascript/ql/src/Expressions/ImplicitOperandConversion.qhelp
@@ -39,7 +39,7 @@ property of the name stored in variable member:
However, this test is ineffective as written: the operator ! binds more
-tighly than in, so it is applied first. Applying ! to a
+tightly than in, so it is applied first. Applying ! to a
non-empty string yields false, so the in operator actually
ends up checking whether obj contains a property called "false".
diff --git a/javascript/ql/src/LanguageFeatures/EmptyArrayInit.ql b/javascript/ql/src/LanguageFeatures/EmptyArrayInit.ql
index 70c89715bf85..48ae48112446 100644
--- a/javascript/ql/src/LanguageFeatures/EmptyArrayInit.ql
+++ b/javascript/ql/src/LanguageFeatures/EmptyArrayInit.ql
@@ -45,5 +45,4 @@ class OmittedArrayElement extends ArrayExpr {
}
from OmittedArrayElement ae
-where not ae.getFile().getFileType().isTypeScript() // ignore quirks in TypeScript tokenizer
select ae, "Avoid omitted array elements."
\ No newline at end of file
diff --git a/javascript/ql/src/LanguageFeatures/SemicolonInsertion.ql b/javascript/ql/src/LanguageFeatures/SemicolonInsertion.ql
index 2eef68fe5f4f..fc52539be8db 100644
--- a/javascript/ql/src/LanguageFeatures/SemicolonInsertion.ql
+++ b/javascript/ql/src/LanguageFeatures/SemicolonInsertion.ql
@@ -36,8 +36,7 @@ where s.hasSemicolonInserted() and
asi = strictcount(Stmt ss | asi(sc, ss, true)) and
nstmt = strictcount(Stmt ss | asi(sc, ss, _)) and
perc = ((1-asi/nstmt)*100).floor() and
- perc >= 90 and
- not s.getFile().getFileType().isTypeScript() // ignore some quirks in the TypeScript tokenizer
+ perc >= 90
select (LastLineOf)s, "Avoid automated semicolon insertion " +
"(" + perc + "% of all statements in $@ have an explicit semicolon).",
sc, "the enclosing " + sctype
\ No newline at end of file
diff --git a/javascript/ql/src/Security/CWE-807/ConditionalBypass.ql b/javascript/ql/src/Security/CWE-807/ConditionalBypass.ql
index ca7188ce5eb2..e939bb61f8a1 100644
--- a/javascript/ql/src/Security/CWE-807/ConditionalBypass.ql
+++ b/javascript/ql/src/Security/CWE-807/ConditionalBypass.ql
@@ -3,7 +3,7 @@
* @description Conditions that the user controls are not suited for making security-related decisions.
* @kind problem
* @problem.severity error
- * @precision high
+ * @precision medium
* @id js/user-controlled-bypass
* @tags security
* external/cwe/cwe-807
@@ -83,8 +83,32 @@ predicate isTaintedGuardForSensitiveAction(Sink sink, DataFlow::Node source, Sen
)
}
+/**
+ * Holds if `e` effectively guards access to `action` by returning or throwing early.
+ *
+ * Example: `if (e) return; action(x)`.
+ */
+predicate isEarlyAbortGuard(Sink e, SensitiveAction action) {
+ exists (IfStmt guard |
+ // `e` is in the condition of an if-statement ...
+ e.asExpr().getParentExpr*() = guard.getCondition() and
+ // ... where the then-branch always throws or returns
+ exists (Stmt abort |
+ abort instanceof ThrowStmt or
+ abort instanceof ReturnStmt |
+ abort.nestedIn(guard) and
+ abort.getBasicBlock().(ReachableBasicBlock).postDominates(guard.getThen().getBasicBlock() )
+ ) and
+ // ... and the else-branch does not exist
+ not exists (guard.getElse()) |
+ // ... and `action` is outside the if-statement
+ not action.asExpr().getEnclosingStmt().nestedIn(guard)
+ )
+}
+
from DataFlow::Node source, DataFlow::Node sink, SensitiveAction action
-where isTaintedGuardForSensitiveAction(sink, source, action)
+where isTaintedGuardForSensitiveAction(sink, source, action) and
+ not isEarlyAbortGuard(sink, action)
select sink, "This condition guards a sensitive $@, but $@ controls it.",
action, "action",
source, "a user-provided value"
diff --git a/javascript/ql/src/Statements/MisleadingIndentationAfterControlStmt.ql b/javascript/ql/src/Statements/MisleadingIndentationAfterControlStmt.ql
index 84e11138f7bc..0629224ed503 100644
--- a/javascript/ql/src/Statements/MisleadingIndentationAfterControlStmt.ql
+++ b/javascript/ql/src/Statements/MisleadingIndentationAfterControlStmt.ql
@@ -39,7 +39,6 @@ where misleadingIndentationCandidate(ctrl, s1, s2) and
f.hasIndentation(ctrlStartLine, indent, _) and
f.hasIndentation(startLine1, indent, _) and
f.hasIndentation(startLine2, indent, _) and
- not s2 instanceof EmptyStmt and
- not f.getFileType().isTypeScript() // ignore quirks in TypeScript tokenizer
+ not s2 instanceof EmptyStmt
select (FirstLineOf)s2, "The indentation of this statement suggests that it is controlled by $@, while in fact it is not.",
(FirstLineOf)ctrl, "this statement"
\ No newline at end of file
diff --git a/javascript/ql/src/semmle/javascript/ES2015Modules.qll b/javascript/ql/src/semmle/javascript/ES2015Modules.qll
index f5cf676f3d50..4cfa9dcb04d8 100644
--- a/javascript/ql/src/semmle/javascript/ES2015Modules.qll
+++ b/javascript/ql/src/semmle/javascript/ES2015Modules.qll
@@ -27,7 +27,7 @@ class ES2015Module extends Module {
/** Gets an export declaration in this module. */
ExportDeclaration getAnExport() {
- result.getContainer() = this
+ result.getTopLevel() = this
}
override Module getAnImportedModule() {
@@ -55,7 +55,7 @@ class ES2015Module extends Module {
/** An import declaration. */
class ImportDeclaration extends Stmt, Import, @importdeclaration {
override ES2015Module getEnclosingModule() {
- result = getContainer()
+ result = getTopLevel()
}
override PathExprInModule getImportedPath() {
@@ -254,7 +254,7 @@ class BulkReExportDeclaration extends ReExportDeclaration, @exportalldeclaration
* but we ignore this subtlety.
*/
private predicate isShadowedFromBulkExport(BulkReExportDeclaration reExport, string name) {
- exists (ExportNamedDeclaration other | other.getContainer() = reExport.getEnclosingModule() |
+ exists (ExportNamedDeclaration other | other.getTopLevel() = reExport.getEnclosingModule() |
other.getAnExportedDecl().getName() = name
or
other.getASpecifier().getExportedName() = name)
diff --git a/javascript/ql/src/semmle/javascript/dataflow/AbstractProperties.qll b/javascript/ql/src/semmle/javascript/dataflow/AbstractProperties.qll
index ed362d313d20..9d5ee2065328 100644
--- a/javascript/ql/src/semmle/javascript/dataflow/AbstractProperties.qll
+++ b/javascript/ql/src/semmle/javascript/dataflow/AbstractProperties.qll
@@ -96,8 +96,7 @@ class AbstractProtoProperty extends AbstractProperty {
* which in turn introduces a materialization.
*/
private AbstractValue getAnAssignedValue(AbstractValue b, string p) {
- exists (AnalyzedPropertyWrite apw, DataFlow::AnalyzedNode afn |
- apw.writes(b, p, afn) and
- result = afn.getALocalValue()
+ exists (AnalyzedPropertyWrite apw |
+ apw.writesValue(b, p, result)
)
}
diff --git a/javascript/ql/src/semmle/javascript/dataflow/internal/InterModuleTypeInference.qll b/javascript/ql/src/semmle/javascript/dataflow/internal/InterModuleTypeInference.qll
index 6efa6f9b0cf9..117e6be397be 100644
--- a/javascript/ql/src/semmle/javascript/dataflow/internal/InterModuleTypeInference.qll
+++ b/javascript/ql/src/semmle/javascript/dataflow/internal/InterModuleTypeInference.qll
@@ -268,6 +268,12 @@ private class AnalyzedVariableExport extends AnalyzedPropertyWrite, DataFlow::Va
propName = name and
source = varDef.getSource().analyze()
}
+
+ override predicate writesValue(AbstractValue baseVal, string propName, AbstractValue val) {
+ baseVal = TAbstractExportsObject(export.getEnclosingModule()) and
+ propName = name and
+ val = varDef.getAnAssignedValue()
+ }
}
/**
@@ -301,7 +307,7 @@ private class AnalyzedExportAssign extends AnalyzedPropertyWrite, DataFlow::Valu
}
override predicate writes(AbstractValue baseVal, string propName, DataFlow::AnalyzedNode source) {
- baseVal = TAbstractModuleObject(exportAssign.getContainer()) and
+ baseVal = TAbstractModuleObject(exportAssign.getTopLevel()) and
propName = "exports" and
source = this
}
diff --git a/javascript/ql/src/semmle/javascript/dataflow/internal/InterProceduralTypeInference.qll b/javascript/ql/src/semmle/javascript/dataflow/internal/InterProceduralTypeInference.qll
index e71561b4139c..49c60bc3eba8 100644
--- a/javascript/ql/src/semmle/javascript/dataflow/internal/InterProceduralTypeInference.qll
+++ b/javascript/ql/src/semmle/javascript/dataflow/internal/InterProceduralTypeInference.qll
@@ -160,18 +160,26 @@ private class IIFEWithAnalyzedReturnFlow extends CallWithAnalyzedReturnFlow {
}
+/**
+ * Gets the only access to `v`, which is the variable declared by `fn`.
+ *
+ * This predicate is not defined for global functions `fn`, or for
+ * local variables `v` that do not have exactly one access.
+ */
+private VarAccess getOnlyAccess(FunctionDeclStmt fn, LocalVariable v) {
+ v = fn.getVariable() and
+ result = v.getAnAccess() and
+ strictcount(v.getAnAccess()) = 1
+}
+
/** A function that only is used locally, making it amenable to type inference. */
class LocalFunction extends Function {
DataFlow::Impl::ExplicitInvokeNode invk;
LocalFunction() {
- this instanceof FunctionDeclStmt and
- exists (LocalVariable v, Expr callee |
- callee = invk.getCalleeNode().asExpr() and
- v = getVariable() and
- v.getAnAccess() = callee and
- forall(VarAccess o | o = v.getAnAccess() | o = callee) and
+ exists (LocalVariable v |
+ getOnlyAccess(this, v) = invk.getCalleeNode().asExpr() and
not exists(v.getAnAssignedExpr()) and
not exists(ExportDeclaration export | export.exportsAs(v, _))
) and
diff --git a/javascript/ql/src/semmle/javascript/dataflow/internal/PropertyTypeInference.qll b/javascript/ql/src/semmle/javascript/dataflow/internal/PropertyTypeInference.qll
index 8eafaa532224..c9160e4a4a71 100644
--- a/javascript/ql/src/semmle/javascript/dataflow/internal/PropertyTypeInference.qll
+++ b/javascript/ql/src/semmle/javascript/dataflow/internal/PropertyTypeInference.qll
@@ -96,8 +96,24 @@ abstract class AnalyzedPropertyWrite extends DataFlow::Node {
/**
* Holds if this property write assigns `source` to property `propName` of one of the
* concrete objects represented by `baseVal`.
+ *
+ * Note that not all property writes have an explicit `source` node; use predicate
+ * `writesValue` below to cover these cases.
*/
- abstract predicate writes(AbstractValue baseVal, string propName, DataFlow::AnalyzedNode source);
+ predicate writes(AbstractValue baseVal, string propName, DataFlow::AnalyzedNode source) {
+ none()
+ }
+
+ /**
+ * Holds if this property write assigns `val` to property `propName` of one of the
+ * concrete objects represented by `baseVal`.
+ */
+ predicate writesValue(AbstractValue baseVal, string propName, AbstractValue val) {
+ exists (AnalyzedNode source |
+ writes(baseVal, propName, source) and
+ val = source.getALocalValue()
+ )
+ }
/**
* Holds if the flow information for the base node of this property write is incomplete
diff --git a/javascript/ql/src/semmle/javascript/frameworks/SQL.qll b/javascript/ql/src/semmle/javascript/frameworks/SQL.qll
index d31df8973f10..f933b25b4776 100644
--- a/javascript/ql/src/semmle/javascript/frameworks/SQL.qll
+++ b/javascript/ql/src/semmle/javascript/frameworks/SQL.qll
@@ -387,3 +387,105 @@ private module Sequelize {
}
}
}
+
+/**
+ * Provides classes modelling the Google Cloud Spanner library.
+ */
+private module Spanner {
+ /**
+ * Gets a node that refers to the `Spanner` class
+ */
+ DataFlow::SourceNode spanner() {
+ // older versions
+ result = DataFlow::moduleImport("@google-cloud/spanner")
+ or
+ // newer versions
+ result = DataFlow::moduleMember("@google-cloud/spanner", "Spanner")
+ }
+
+ /**
+ * Gets a node that refers to an instance of the `Database` class.
+ */
+ DataFlow::SourceNode database() {
+ result = spanner().getAnInvocation().getAMethodCall("instance").getAMethodCall("database")
+ }
+
+ /**
+ * Gets a node that refers to an instance of the `v1.SpannerClient` class.
+ */
+ DataFlow::SourceNode v1SpannerClient() {
+ result = spanner().getAPropertyRead("v1").getAPropertyRead("SpannerClient").getAnInstantiation()
+ }
+
+ /**
+ * Gets a node that refers to a transaction object.
+ */
+ DataFlow::SourceNode transaction() {
+ result = database().getAMethodCall("runTransaction").getCallback(0).getParameter(1)
+ }
+
+ /**
+ * A call to a Spanner method that executes a SQL query.
+ */
+ abstract class SqlExecution extends DatabaseAccess, DataFlow::InvokeNode {
+ /**
+ * Gets the position of the query argument; default is zero, which can be overridden
+ * by subclasses.
+ */
+ int getQueryArgumentPosition() {
+ result = 0
+ }
+
+ override DataFlow::Node getAQueryArgument() {
+ result = getArgument(getQueryArgumentPosition()) or
+ result = getOptionArgument(getQueryArgumentPosition(), "sql")
+ }
+ }
+
+ /**
+ * A call to `Database.run`, `Database.runPartitionedUpdate` or `Database.runStream`.
+ */
+ class DatabaseRunCall extends SqlExecution {
+ DatabaseRunCall() {
+ exists (string run | run = "run" or run = "runPartitionedUpdate" or run = "runStream" |
+ this = database().getAMethodCall(run)
+ )
+ }
+ }
+
+ /**
+ * A call to `Transaction.run`, `Transaction.runStream` or `Transaction.runUpdate`.
+ */
+ class TransactionRunCall extends SqlExecution {
+ TransactionRunCall() {
+ exists (string run | run = "run" or run = "runStream" or run = "runUpdate" |
+ this = transaction().getAMethodCall(run)
+ )
+ }
+ }
+
+ /**
+ * A call to `v1.SpannerClient.executeSql` or `v1.SpannerClient.executeStreamingSql`.
+ */
+ class ExecuteSqlCall extends SqlExecution {
+ ExecuteSqlCall() {
+ exists (string exec | exec = "executeSql" or exec = "executeStreamingSql" |
+ this = v1SpannerClient().getAMethodCall(exec)
+ )
+ }
+
+ override DataFlow::Node getAQueryArgument() {
+ // `executeSql` and `executeStreamingSql` do not accept query strings directly
+ result = getOptionArgument(0, "sql")
+ }
+ }
+
+ /**
+ * An expression that is interpreted as a SQL string.
+ */
+ class QueryString extends SQL::SqlString {
+ QueryString() {
+ this = any(SqlExecution se).getAQueryArgument().asExpr()
+ }
+ }
+}
diff --git a/javascript/ql/test/library-tests/Flow/AbstractValues.expected b/javascript/ql/test/library-tests/Flow/AbstractValues.expected
index 0f3244a73cbd..e7379efebeca 100644
--- a/javascript/ql/test/library-tests/Flow/AbstractValues.expected
+++ b/javascript/ql/test/library-tests/Flow/AbstractValues.expected
@@ -4,10 +4,12 @@
| ChatListScreen.js:3:1:5:1 | instance of function foo |
| a2.js:1:1:2:0 | exports object of module a2 |
| a2.js:1:1:2:0 | module object of module a2 |
-| a.js:1:1:13:0 | exports object of module a |
-| a.js:1:1:13:0 | module object of module a |
+| a.js:1:1:18:0 | exports object of module a |
+| a.js:1:1:18:0 | module object of module a |
| a.js:3:8:5:1 | function setX |
| a.js:3:8:5:1 | instance of function setX |
+| a.js:15:1:17:1 | function bump |
+| a.js:15:1:17:1 | instance of function bump |
| amd2.js:1:1:4:0 | exports object of module amd2 |
| amd2.js:1:1:4:0 | module object of module amd2 |
| amd2.js:1:8:3:1 | anonymous function |
@@ -36,8 +38,8 @@
| arguments.js:30:2:33:1 | anonymous function |
| arguments.js:30:2:33:1 | arguments object of anonymous function |
| arguments.js:30:2:33:1 | instance of anonymous function |
-| b.js:1:1:55:0 | exports object of module b |
-| b.js:1:1:55:0 | module object of module b |
+| b.js:1:1:58:0 | exports object of module b |
+| b.js:1:1:58:0 | module object of module b |
| backend.js:1:1:3:0 | exports object of module backend |
| backend.js:1:1:3:0 | module object of module backend |
| backend.js:1:17:1:18 | object literal |
@@ -180,6 +182,10 @@
| n.js:3:16:3:23 | object literal |
| namespace-reexport.js:1:1:4:0 | exports object of module namespace-reexport |
| namespace-reexport.js:1:1:4:0 | module object of module namespace-reexport |
+| nestedImport.js:1:1:13:0 | exports object of module nestedImport |
+| nestedImport.js:1:1:13:0 | module object of module nestedImport |
+| nestedImport.js:9:1:12:1 | function tst |
+| nestedImport.js:9:1:12:1 | instance of function tst |
| nodeJsClient.js:1:1:6:0 | exports object of module nodeJsClient |
| nodeJsClient.js:1:1:6:0 | module object of module nodeJsClient |
| nodeJsLib.js:1:1:4:0 | exports object of module nodeJsLib |
diff --git a/javascript/ql/test/library-tests/Flow/a.js b/javascript/ql/test/library-tests/Flow/a.js
index 5319d461a5ba..e733d3361276 100644
--- a/javascript/ql/test/library-tests/Flow/a.js
+++ b/javascript/ql/test/library-tests/Flow/a.js
@@ -10,3 +10,8 @@ let z = someGlobal;
export let w;
w = "w";
+
+export let notAlwaysZero = 0;
+function bump() {
+ ++notAlwaysZero;
+}
diff --git a/javascript/ql/test/library-tests/Flow/abseval.expected b/javascript/ql/test/library-tests/Flow/abseval.expected
index dbc1a06ebfe4..11deddd1ab92 100644
--- a/javascript/ql/test/library-tests/Flow/abseval.expected
+++ b/javascript/ql/test/library-tests/Flow/abseval.expected
@@ -6,6 +6,7 @@
| a.js:9:5:9:5 | z | a.js:9:9:9:18 | someGlobal | file://:0:0:0:0 | indefinite value (global) |
| a.js:9:5:9:5 | z | a.js:9:9:9:18 | someGlobal | file://:0:0:0:0 | non-zero value |
| a.js:9:5:9:5 | z | a.js:9:9:9:18 | someGlobal | file://:0:0:0:0 | true |
+| a.js:14:12:14:24 | notAlwaysZero | a.js:14:28:14:28 | 0 | file://:0:0:0:0 | 0 |
| amd.js:2:7:2:7 | m | amd.js:2:11:2:13 | mod | amd.js:1:1:7:0 | module object of module amd |
| amd.js:2:7:2:7 | m | amd.js:2:11:2:13 | mod | file://:0:0:0:0 | indefinite value (call) |
| amd.js:3:7:3:7 | e | amd.js:3:11:3:13 | exp | amd.js:1:1:7:0 | exports object of module amd |
@@ -54,9 +55,12 @@
| b.js:42:5:42:7 | z11 | b.js:42:11:42:18 | toString | file://:0:0:0:0 | indefinite value (import) |
| b.js:45:5:45:7 | z12 | b.js:45:11:45:12 | f2 | ts2.ts:1:1:6:0 | exports object of module ts2 |
| b.js:45:5:45:7 | z12 | b.js:45:11:45:12 | f2 | ts2.ts:1:10:1:22 | anonymous function |
+| b.js:45:5:45:7 | z12 | b.js:45:11:45:12 | f2 | ts2.ts:4:12:4:13 | object literal |
| b.js:48:5:48:7 | z13 | b.js:48:11:48:11 | w | file://:0:0:0:0 | non-empty, non-numeric string |
| b.js:51:5:51:7 | z14 | b.js:51:11:51:24 | foo_reexported | file://:0:0:0:0 | indefinite value (import) |
| b.js:54:5:54:7 | z15 | b.js:54:11:54:19 | something | file://:0:0:0:0 | indefinite value (import) |
+| b.js:57:5:57:7 | z16 | b.js:57:11:57:23 | notAlwaysZero | file://:0:0:0:0 | 0 |
+| b.js:57:5:57:7 | z16 | b.js:57:11:57:23 | notAlwaysZero | file://:0:0:0:0 | non-zero value |
| backend.js:1:7:1:13 | Backend | backend.js:1:17:1:18 | {} | backend.js:1:17:1:18 | object literal |
| classAccessors.js:10:9:10:11 | myX | classAccessors.js:10:15:10:20 | this.x | file://:0:0:0:0 | indefinite value (call) |
| classAccessors.js:10:9:10:11 | myX | classAccessors.js:10:15:10:20 | this.x | file://:0:0:0:0 | indefinite value (heap) |
@@ -171,6 +175,10 @@
| mixed.js:9:5:9:6 | fn | mixed.js:9:10:9:19 | __filename | file://:0:0:0:0 | numeric string |
| mixed.js:10:5:10:6 | dn | mixed.js:10:10:10:18 | __dirname | file://:0:0:0:0 | non-empty, non-numeric string |
| mixed.js:10:5:10:6 | dn | mixed.js:10:10:10:18 | __dirname | file://:0:0:0:0 | numeric string |
+| nestedImport.js:2:5:2:6 | x1 | nestedImport.js:2:10:2:12 | foo | esLib.js:3:8:3:24 | function foo |
+| nestedImport.js:6:7:6:8 | x2 | nestedImport.js:6:12:6:14 | foo | file://:0:0:0:0 | indefinite value (import) |
+| nestedImport.js:6:7:6:8 | x2 | nestedImport.js:6:12:6:14 | foo | nodeJsLib.js:3:15:3:37 | function nodeJsFoo |
+| nestedImport.js:11:7:11:8 | x3 | nestedImport.js:11:12:11:14 | foo | esLib.js:3:8:3:24 | function foo |
| nodeJsClient.js:1:5:1:6 | nj | nodeJsClient.js:1:10:1:31 | require ... JsLib') | file://:0:0:0:0 | indefinite value (call) |
| nodeJsClient.js:1:5:1:6 | nj | nodeJsClient.js:1:10:1:31 | require ... JsLib') | nodeJsLib.js:1:1:4:0 | exports object of module nodeJsLib |
| nodeJsClient.js:1:5:1:6 | nj | nodeJsClient.js:1:10:1:31 | require ... JsLib') | nodeJsLib.js:1:18:1:43 | function nodeJsModule |
diff --git a/javascript/ql/test/library-tests/Flow/b.js b/javascript/ql/test/library-tests/Flow/b.js
index 6c8632f53483..f1d9c7c66067 100644
--- a/javascript/ql/test/library-tests/Flow/b.js
+++ b/javascript/ql/test/library-tests/Flow/b.js
@@ -52,3 +52,6 @@ let z14 = foo_reexported;
import { something } from './reexport-unknown';
let z15 = something;
+
+import { notAlwaysZero } from './a';
+let z16 = notAlwaysZero;
diff --git a/javascript/ql/test/library-tests/Flow/nestedImport.js b/javascript/ql/test/library-tests/Flow/nestedImport.js
new file mode 100644
index 000000000000..c52953db2cfb
--- /dev/null
+++ b/javascript/ql/test/library-tests/Flow/nestedImport.js
@@ -0,0 +1,12 @@
+import { foo } from './esLib';
+let x1 = foo;
+
+if (!foo) {
+ import { foo } from './nodeJsLib';
+ let x2 = foo;
+}
+
+function tst() {
+ import { foo } from './esLib';
+ let x3 = foo;
+}
diff --git a/javascript/ql/test/library-tests/Flow/types.expected b/javascript/ql/test/library-tests/Flow/types.expected
index 786fef8fcb96..bdbff344ac5f 100644
--- a/javascript/ql/test/library-tests/Flow/types.expected
+++ b/javascript/ql/test/library-tests/Flow/types.expected
@@ -2,6 +2,7 @@
| a.js:1:12:1:12 | x | a.js:1:16:1:16 | 0 | number |
| a.js:1:19:1:19 | y | a.js:1:23:1:23 | 0 | number |
| a.js:9:5:9:5 | z | a.js:9:9:9:18 | someGlobal | boolean, class, date, function, null, number, object, regular expression,string or undefined |
+| a.js:14:12:14:24 | notAlwaysZero | a.js:14:28:14:28 | 0 | number |
| amd.js:2:7:2:7 | m | amd.js:2:11:2:13 | mod | boolean, class, date, function, null, number, object, regular expression,string or undefined |
| amd.js:3:7:3:7 | e | amd.js:3:11:3:13 | exp | boolean, class, date, function, null, number, object, regular expression,string or undefined |
| arguments.js:2:7:2:7 | y | arguments.js:2:11:2:11 | x | number |
@@ -32,6 +33,7 @@
| b.js:48:5:48:7 | z13 | b.js:48:11:48:11 | w | string |
| b.js:51:5:51:7 | z14 | b.js:51:11:51:24 | foo_reexported | boolean, class, date, function, null, number, object, regular expression,string or undefined |
| b.js:54:5:54:7 | z15 | b.js:54:11:54:19 | something | boolean, class, date, function, null, number, object, regular expression,string or undefined |
+| b.js:57:5:57:7 | z16 | b.js:57:11:57:23 | notAlwaysZero | number |
| backend.js:1:7:1:13 | Backend | backend.js:1:17:1:18 | {} | object |
| classAccessors.js:10:9:10:11 | myX | classAccessors.js:10:15:10:20 | this.x | boolean, class, date, function, null, number, object, regular expression,string or undefined |
| classAccessors.js:11:9:11:11 | myY | classAccessors.js:11:15:11:20 | this.y | boolean, class, date, function, null, number, object, regular expression,string or undefined |
@@ -98,6 +100,9 @@
| mixed.js:8:5:8:7 | exp | mixed.js:8:11:8:17 | exports | object |
| mixed.js:9:5:9:6 | fn | mixed.js:9:10:9:19 | __filename | string |
| mixed.js:10:5:10:6 | dn | mixed.js:10:10:10:18 | __dirname | string |
+| nestedImport.js:2:5:2:6 | x1 | nestedImport.js:2:10:2:12 | foo | function |
+| nestedImport.js:6:7:6:8 | x2 | nestedImport.js:6:12:6:14 | foo | boolean, class, date, function, null, number, object, regular expression,string or undefined |
+| nestedImport.js:11:7:11:8 | x3 | nestedImport.js:11:12:11:14 | foo | function |
| nodeJsClient.js:1:5:1:6 | nj | nodeJsClient.js:1:10:1:31 | require ... JsLib') | boolean, class, date, function, null, number, object, regular expression,string or undefined |
| nodeJsClient.js:2:5:2:6 | es | nodeJsClient.js:2:10:2:27 | require('./esLib') | boolean, class, date, function, null, number, object, regular expression,string or undefined |
| nodeJsClient.js:4:5:4:6 | x1 | nodeJsClient.js:4:10:4:15 | nj.foo | boolean, class, date, function, null, number, object, regular expression,string or undefined |
diff --git a/javascript/ql/test/library-tests/TypeScript/ExpansiveTypes/ExpansiveTypes.expected b/javascript/ql/test/library-tests/TypeScript/ExpansiveTypes/ExpansiveTypes.expected
index 152299ee5878..8ced7c1fb7d0 100644
--- a/javascript/ql/test/library-tests/TypeScript/ExpansiveTypes/ExpansiveTypes.expected
+++ b/javascript/ql/test/library-tests/TypeScript/ExpansiveTypes/ExpansiveTypes.expected
@@ -18,4 +18,5 @@
| ExpansiveMethod in expansive_signature.ts | has no properties |
| ExpansiveParameter in expansive_signature.ts | has no properties |
| ExpansiveSignature in expansive_signature.ts | has no properties |
+| ExpansiveSignatureTypeBound in expansive_signature.ts | has no properties |
| ExpansiveX in used_from_expansion.ts | has no properties |
diff --git a/javascript/ql/test/library-tests/TypeScript/ExpansiveTypes/NonExpansiveTypes.expected b/javascript/ql/test/library-tests/TypeScript/ExpansiveTypes/NonExpansiveTypes.expected
index edf58f7f0e15..a150036fa210 100644
--- a/javascript/ql/test/library-tests/TypeScript/ExpansiveTypes/NonExpansiveTypes.expected
+++ b/javascript/ql/test/library-tests/TypeScript/ExpansiveTypes/NonExpansiveTypes.expected
@@ -25,6 +25,7 @@
| ExpansiveMethod in expansive_signature.ts | has properties |
| ExpansiveParameter in expansive_signature.ts | has properties |
| ExpansiveSignature in expansive_signature.ts | has properties |
+| ExpansiveSignatureTypeBound in expansive_signature.ts | has properties |
| ExpansiveX in used_from_expansion.ts | has properties |
| Function in global scope | has properties |
| Intl.CollatorOptions in global scope | has properties |
diff --git a/javascript/ql/test/library-tests/TypeScript/ExpansiveTypes/expansive_signature.ts b/javascript/ql/test/library-tests/TypeScript/ExpansiveTypes/expansive_signature.ts
index d0ce77d194c1..6bdfd8e1f2aa 100644
--- a/javascript/ql/test/library-tests/TypeScript/ExpansiveTypes/expansive_signature.ts
+++ b/javascript/ql/test/library-tests/TypeScript/ExpansiveTypes/expansive_signature.ts
@@ -19,3 +19,7 @@ interface ExpansiveMethod {
interface ExpansiveFunctionType {
x: () => ExpansiveFunctionType;
}
+
+interface ExpansiveSignatureTypeBound {
+ foo : { >(x: G): G };
+}
diff --git a/javascript/ql/test/library-tests/TypeScript/Tokens/PrettyPrintTokens.expected b/javascript/ql/test/library-tests/TypeScript/Tokens/PrettyPrintTokens.expected
new file mode 100644
index 000000000000..81e03e435923
--- /dev/null
+++ b/javascript/ql/test/library-tests/TypeScript/Tokens/PrettyPrintTokens.expected
@@ -0,0 +1,21 @@
+| greaterThanTS.ts:2 | console . log ( x >= 1 ) ; |
+| greaterThanTS.ts:3 | console . log ( x >>= 1 ) ; |
+| greaterThanTS.ts:4 | console . log ( x >>>= 1 ) ; |
+| greaterThanTS.ts:5 | console . log ( x >> 1 ) ; |
+| greaterThanTS.ts:6 | console . log ( x >>> 1 ) ; |
+| greaterThanTS.ts:8 | console . log ( x >= 1 ) ; |
+| greaterThanTS.ts:9 | console . log ( x >= 1 ) ; |
+| greaterThanTS.ts:10 | console . log ( x >= 1 ) ; |
+| mixed.ts:2 | console . log ( x >= 1 ) ; |
+| mixed.ts:3 | console . log ( `${ /r/g >= 1 }` ) ; |
+| mixed.ts:4 | console . log ( /r/g ) ; |
+| mixed.ts:5 | console . log ( `${ 1 }${ 1 }` ) ; |
+| regexpTS.ts:2 | console . log ( /foo/g ) ; |
+| regexpTS.ts:3 | console . log ( /foo/g ) ; |
+| templateLiteralsJS.js:2 | console . log ( ` template~without~placeholders ` ) ; |
+| templateLiteralsJS.js:3 | console . log ( ` template~with~placeholder~ ${ x } . ` ) ; |
+| templateLiteralsTS.ts:2 | console . log ( `template~without~placeholders` ) ; |
+| templateLiteralsTS.ts:3 | console . log ( `template~with~placeholder~${ x }.` ) ; |
+| templateLiteralsTS.ts:4 | console . log ( `template~with~placeholder~${ x }.` ) ; |
+| templateLiteralsTS.ts:5 | console . log ( `template~with~placeholder~${ x }.` ) ; |
+| templateLiteralsTS.ts:6 | console . log ( `template~with~placeholder~${ x }.` ) ; |
diff --git a/javascript/ql/test/library-tests/TypeScript/Tokens/PrettyPrintTokens.ql b/javascript/ql/test/library-tests/TypeScript/Tokens/PrettyPrintTokens.ql
new file mode 100644
index 000000000000..e3344a5075cb
--- /dev/null
+++ b/javascript/ql/test/library-tests/TypeScript/Tokens/PrettyPrintTokens.ql
@@ -0,0 +1,15 @@
+import javascript
+
+Token getATokenAtLine(File file, int line) {
+ result.getFile() = file and
+ result.getLocation().getStartLine() = line
+}
+
+bindingset[line]
+string getTokenStringAtLine(File file, int line) {
+ result = concat(Token tok | tok = getATokenAtLine(file, line) | tok.toString().replaceAll(" ", "~") + " " order by tok.getLocation().getStartColumn())
+}
+
+from File file, int line
+where exists(CallExpr call | call.getFile() = file and call.getLocation().getStartLine() = line)
+select file.getBaseName() + ":" + line, getTokenStringAtLine(file, line)
diff --git a/javascript/ql/test/library-tests/TypeScript/Tokens/greaterThanTS.ts b/javascript/ql/test/library-tests/TypeScript/Tokens/greaterThanTS.ts
new file mode 100644
index 000000000000..391971677f51
--- /dev/null
+++ b/javascript/ql/test/library-tests/TypeScript/Tokens/greaterThanTS.ts
@@ -0,0 +1,11 @@
+function f(x) {
+ console.log(x >= 1);
+ console.log(x >>= 1);
+ console.log(x >>>= 1);
+ console.log(x >> 1);
+ console.log(x >>> 1);
+
+ console.log(x>=1);
+ console.log(x>= 1);
+ console.log(x >=1);
+}
diff --git a/javascript/ql/test/library-tests/TypeScript/Tokens/mixed.ts b/javascript/ql/test/library-tests/TypeScript/Tokens/mixed.ts
new file mode 100644
index 000000000000..7f01ada79ad9
--- /dev/null
+++ b/javascript/ql/test/library-tests/TypeScript/Tokens/mixed.ts
@@ -0,0 +1,6 @@
+function f(x) {
+ console.log(x >= 1);
+ console.log(`${/r/g >= 1}`);
+ console.log( /r/g);
+ console.log( `${1}${1}`);
+}
diff --git a/javascript/ql/test/library-tests/TypeScript/Tokens/regexpTS.ts b/javascript/ql/test/library-tests/TypeScript/Tokens/regexpTS.ts
new file mode 100644
index 000000000000..ca269c3db89a
--- /dev/null
+++ b/javascript/ql/test/library-tests/TypeScript/Tokens/regexpTS.ts
@@ -0,0 +1,4 @@
+function f() {
+ console.log(/foo/g);
+ console.log( /foo/g);
+}
diff --git a/javascript/ql/test/library-tests/TypeScript/Tokens/templateLiteralsJS.js b/javascript/ql/test/library-tests/TypeScript/Tokens/templateLiteralsJS.js
new file mode 100644
index 000000000000..689e596474a1
--- /dev/null
+++ b/javascript/ql/test/library-tests/TypeScript/Tokens/templateLiteralsJS.js
@@ -0,0 +1,4 @@
+function f(x) {
+ console.log(`template without placeholders`);
+ console.log(`template with placeholder ${x}.`);
+}
diff --git a/javascript/ql/test/library-tests/TypeScript/Tokens/templateLiteralsTS.ts b/javascript/ql/test/library-tests/TypeScript/Tokens/templateLiteralsTS.ts
new file mode 100644
index 000000000000..803685637b28
--- /dev/null
+++ b/javascript/ql/test/library-tests/TypeScript/Tokens/templateLiteralsTS.ts
@@ -0,0 +1,7 @@
+function f(x) {
+ console.log(`template without placeholders`);
+ console.log(`template with placeholder ${x}.`);
+ console.log(`template with placeholder ${x }.`);
+ console.log(`template with placeholder ${ x}.`);
+ console.log(`template with placeholder ${ x }.`);
+}
diff --git a/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected b/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected
index d4866f842e94..ace4b4fa5488 100644
--- a/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected
+++ b/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected
@@ -14,4 +14,24 @@
| postgres3.js:15:16:15:40 | 'SELECT ... s name' |
| sequelize2.js:10:17:10:118 | 'SELECT ... Y name' |
| sequelize.js:8:17:8:118 | 'SELECT ... Y name' |
+| spanner2.js:5:26:5:35 | "SQL code" |
+| spanner2.js:7:35:7:44 | "SQL code" |
+| spanner.js:6:8:6:17 | "SQL code" |
+| spanner.js:7:8:7:26 | { sql: "SQL code" } |
+| spanner.js:7:15:7:24 | "SQL code" |
+| spanner.js:8:25:8:34 | "SQL code" |
+| spanner.js:9:25:9:43 | { sql: "SQL code" } |
+| spanner.js:9:32:9:41 | "SQL code" |
+| spanner.js:10:14:10:23 | "SQL code" |
+| spanner.js:11:14:11:31 | { sql: "SQL code"} |
+| spanner.js:11:21:11:30 | "SQL code" |
+| spanner.js:14:10:14:19 | "SQL code" |
+| spanner.js:15:10:15:28 | { sql: "SQL code" } |
+| spanner.js:15:17:15:26 | "SQL code" |
+| spanner.js:16:16:16:25 | "SQL code" |
+| spanner.js:17:16:17:34 | { sql: "SQL code" } |
+| spanner.js:17:23:17:32 | "SQL code" |
+| spanner.js:18:16:18:25 | "SQL code" |
+| spanner.js:19:16:19:34 | { sql: "SQL code" } |
+| spanner.js:19:23:19:32 | "SQL code" |
| sqlite.js:7:8:7:45 | "UPDATE ... id = ?" |
diff --git a/javascript/ql/test/library-tests/frameworks/SQL/spanner.js b/javascript/ql/test/library-tests/frameworks/SQL/spanner.js
new file mode 100644
index 000000000000..d42762230062
--- /dev/null
+++ b/javascript/ql/test/library-tests/frameworks/SQL/spanner.js
@@ -0,0 +1,20 @@
+const { Spanner } = require("@google-cloud/spanner");
+const spanner = new Spanner();
+const instance = spanner.instance('inst');
+const db = instance.database('db');
+
+db.run("SQL code", (err, rows) => {});
+db.run({ sql: "SQL code" }, (err, rows) => {});
+db.runPartitionedUpdate("SQL code", (err, rows) => {});
+db.runPartitionedUpdate({ sql: "SQL code" }, (err, rows) => {});
+db.runStream("SQL code");
+db.runStream({ sql: "SQL code"});
+
+db.runTransaction((err, tx) => {
+ tx.run("SQL code");
+ tx.run({ sql: "SQL code" });
+ tx.runStream("SQL code");
+ tx.runStream({ sql: "SQL code" });
+ tx.runUpdate("SQL code");
+ tx.runUpdate({ sql: "SQL code" });
+});
\ No newline at end of file
diff --git a/javascript/ql/test/library-tests/frameworks/SQL/spanner2.js b/javascript/ql/test/library-tests/frameworks/SQL/spanner2.js
new file mode 100644
index 000000000000..9795d0f3a39e
--- /dev/null
+++ b/javascript/ql/test/library-tests/frameworks/SQL/spanner2.js
@@ -0,0 +1,7 @@
+const spanner = require("@google-cloud/spanner");
+const client = new spanner.v1.SpannerClient({});
+
+client.executeSql("not SQL code", (err, rows) => {});
+client.executeSql({ sql: "SQL code" }, (err, rows) => {});
+client.executeStreamingSql("not SQL code", (err, rows) => {});
+client.executeStreamingSql({ sql: "SQL code" }, (err, rows) => {});
diff --git a/javascript/ql/test/query-tests/LanguageFeatures/SemicolonInsertion/template_literal.ts b/javascript/ql/test/query-tests/LanguageFeatures/SemicolonInsertion/template_literal.ts
deleted file mode 100644
index 01ce984f8e30..000000000000
--- a/javascript/ql/test/query-tests/LanguageFeatures/SemicolonInsertion/template_literal.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-function foo(arg) {
- console.log(arg);
- console.log(arg);
- console.log(arg);
- console.log(arg);
- console.log(arg);
- console.log(arg);
- console.log(arg);
- console.log(arg);
- console.log(arg);
- console.log(`Unknown option '${arg}'.`);
-}
diff --git a/javascript/ql/test/query-tests/Security/CWE-807/ConditionalBypass.expected b/javascript/ql/test/query-tests/Security/CWE-807/ConditionalBypass.expected
index 36c6f163cf5f..0ee7194b924b 100644
--- a/javascript/ql/test/query-tests/Security/CWE-807/ConditionalBypass.expected
+++ b/javascript/ql/test/query-tests/Security/CWE-807/ConditionalBypass.expected
@@ -10,3 +10,5 @@
| tst.js:76:9:76:10 | v1 | This condition guards a sensitive $@, but $@ controls it. | tst.js:78:9:78:22 | process.exit() | action | tst.js:75:14:75:24 | req.cookies | a user-provided value |
| tst.js:76:9:76:10 | v1 | This condition guards a sensitive $@, but $@ controls it. | tst.js:78:9:78:22 | process.exit() | action | tst.js:75:39:75:58 | req.params.requestId | a user-provided value |
| tst.js:90:9:90:41 | req.coo ... secret" | This condition guards a sensitive $@, but $@ controls it. | tst.js:92:9:92:22 | process.exit() | action | tst.js:90:9:90:19 | req.cookies | a user-provided value |
+| tst.js:111:13:111:32 | req.query.vulnerable | This condition guards a sensitive $@, but $@ controls it. | tst.js:114:9:114:16 | verify() | action | tst.js:111:13:111:32 | req.query.vulnerable | a user-provided value |
+| tst.js:118:13:118:32 | req.query.vulnerable | This condition guards a sensitive $@, but $@ controls it. | tst.js:121:13:121:20 | verify() | action | tst.js:118:13:118:32 | req.query.vulnerable | a user-provided value |
diff --git a/javascript/ql/test/query-tests/Security/CWE-807/tst.js b/javascript/ql/test/query-tests/Security/CWE-807/tst.js
index 84235fa0d3f1..aa00c47f1ebd 100644
--- a/javascript/ql/test/query-tests/Security/CWE-807/tst.js
+++ b/javascript/ql/test/query-tests/Security/CWE-807/tst.js
@@ -99,3 +99,34 @@ app.get('/user/:id', function(req, res) {
console.log(commit.author().toString());
}
});
+
+app.get('/user/:id', function(req, res) {
+ if (!req.body || !username || !password || riskAssessnment == null) { // OK: early return below
+ res.status(400).send({ error: '...', id: '...' });
+ return
+ }
+ customerLogin.customerLogin(username, password, riskAssessment, clientIpAddress);
+
+ while (!verified) {
+ if (req.query.vulnerable) { // NOT OK
+ break;
+ }
+ verify();
+ }
+
+ while (!verified) {
+ if (req.query.vulnerable) { // NOT OK
+ break;
+ } else {
+ verify();
+ }
+ }
+
+ while (!verified) {
+ if (req.query.vulnerable) { // OK: early return
+ return;
+ }
+ verify();
+ }
+
+});