Compare commits

...

45 Commits

Author SHA1 Message Date
Aditya Sharad
05ef589b79 Merge pull request #433 from rdmarsh2/rdmarsh/cpp/buffersize-backport
C++: Backport "Improve memberMayBeVarSize" to rc/1.18
2018-11-08 11:54:40 +00:00
Geoffrey White
57dafe2734 CPP: BinaryArithmeticOperation -> AddExpr. 2018-11-07 15:14:39 -08:00
Geoffrey White
313624fd2e CPP: Support builtin offsetof. 2018-11-07 15:14:39 -08:00
Geoffrey White
79ff559f7a CPP: Don't require alloc in memberMayBeVarSize. 2018-11-07 15:11:53 -08:00
Geoffrey White
19a53cde79 CPP: Fix array size bug in memberMayBeVarSize. 2018-11-07 15:11:53 -08:00
Geoffrey White
3610d35321 CPP: Include offsetof type calculations in memberMayBeVarSize. 2018-11-07 15:11:52 -08:00
Geoffrey White
b9c727c772 CPP: Include sizeof(expr) expressions in isDynamicallyAllocatedWithDifferentSize. 2018-11-07 15:11:52 -08:00
Arthur Baars
f159c7e240 Merge pull request #429 from adityasharad/version/1.18.2-release
Version: Bump to 1.18.2 release.
2018-11-07 15:52:41 +01:00
Aditya Sharad
ed49c623f1 Version: Bump to 1.18.2 release. 2018-11-07 14:36:40 +00:00
Luke Cartey
4939db1c96 Merge pull request #411 from adityasharad/cpp/constant-exprs-fn-value-performance
C++: Improve performance of `ExprEvaluator::getFunctionValue`.
2018-11-07 13:05:15 +00:00
Aditya Sharad
bfa4c30784 C++: Improve performance of ExprEvaluator::getFunctionValue.
Changes the `forex` range to join on both `this` (the current `ExprEvaluator`) and `ret` (the expected function return value),
so that we look at the relevant return values rather than all interesting functions.
2018-11-05 16:51:23 +00:00
Arthur Baars
19f238a51a Merge pull request #399 from adityasharad/version/1.18.2-dev
Version: Bump to 1.18.2 dev.
2018-11-02 08:56:33 +01:00
Aditya Sharad
3291a30bf4 Version: Bump to 1.18.2 dev. 2018-11-01 18:46:56 +00:00
Geoffrey White
40ad2c9db9 Merge pull request #397 from pavgust/fix/suspicious-memset-perf
SuspiciousCallToMemset: Simplify pointer indirection computation
2018-11-01 18:46:30 +00:00
Pavel Avgustinov
d5c8ea38b1 SuspiciousCallToMemset: Simplify pointer indirection computation 2018-11-01 14:34:04 +00:00
Arthur Baars
778a484ce0 Merge pull request #382 from adityasharad/version/1.18.1-release
Version: Bump to 1.18.1 release.
2018-10-29 21:30:54 +01:00
Aditya Sharad
5e7b7818df Version: Bump to 1.18.1 release. 2018-10-29 18:02:58 +00:00
Aditya Sharad
c88db424fa Merge pull request #343 from geoffw0/av-35-1.18
CPP: Fix hasXMacro performance.
2018-10-23 10:24:16 +01:00
Max Schaefer
3522200e90 Merge pull request #342 from xiemaisi/rc/1.18-cherry-picks
JavaScript: 1.18.1 cherry-picks
2018-10-22 20:03:22 +01:00
Geoffrey White
de1556042a CPP: Fix hasXMacro performance. 2018-10-22 19:43:04 +01:00
Max Schaefer
25224cc4a0 Revert "TypeScript: disable queries that rely on token information"
This reverts commit 003b600e24.
2018-10-22 11:06:11 +01:00
Esben Sparre Andreasen
2e49cd117a JS: avoid flagging early returns in js/user-controlled-bypass
(cherry picked from commit ffbbb807f4)
2018-10-19 08:30:03 +01:00
Asger F
f9634040b0 TypeScript: add test case with mixed rescanned tokens
(cherry picked from commit 057af7c865)
2018-10-19 08:30:03 +01:00
Asger F
39c788f4f1 TypeScript: test case for tokens starting with ">"
(cherry picked from commit d3a1df644c)
2018-10-19 08:30:03 +01:00
Asger F
2abe34b2f9 TypeScript: test case for whitespace before a rescanned token
(cherry picked from commit a199035a05)
2018-10-19 08:30:03 +01:00
Asger F
cbf06ae74d TypeScript: test case for tokenization of template literals
(cherry picked from commit 9146cc26bd)
2018-10-19 08:30:03 +01:00
Asger F
4d7e762629 TS: test case for type expansion through type parameter bound
(cherry picked from commit 8bc92bd534)
2018-10-19 08:30:03 +01:00
Max Schaefer
374fd597d7 JavaScript: Reinstate override.
(cherry picked from commit df5a8651c3)
2018-10-19 08:30:03 +01:00
Max Schaefer
b0425a298c JavaScript: Eliminate slow antijoin predicate.
(cherry picked from commit 0cfd04dfa2)
2018-10-19 08:30:03 +01:00
Max Schaefer
5167d43fbc JavaScript: Refactor AnalyzedPropertyWrite::writes to enable correct modelling of variable exports.
(cherry picked from commit 080f974663)
2018-10-19 08:30:03 +01:00
Max Schaefer
898ba94837 JavaScript: Address review comments.
(cherry picked from commit 6835815673)
2018-10-19 08:30:03 +01:00
Max Schaefer
2b7d69aaf4 JavaScript: Add support for Google Cloud Spanner.
(cherry picked from commit cd284b2f97)
2018-10-19 08:30:03 +01:00
Tom Hvitved
b282444740 Revert "JavaScript: Patch CFG to improve support for non-top level import declarations."
This reverts commit f05e777e64.
2018-10-19 08:30:03 +01:00
Max Schaefer
5e75a62f5c JavaScript: Add test case for type inference in the presence of non-toplevel imports.
(cherry picked from commit 8b7bb8cecc)
2018-10-19 08:30:03 +01:00
Max Schaefer
e683b51611 JavaScript: Generalise code that assumes imports only appear at the toplevel.
(cherry picked from commit db32dc2bdf)
2018-10-19 08:30:03 +01:00
Max Schaefer
de108a843d JavaScript: Patch CFG to improve support for non-top level import declarations. 2018-10-19 08:30:03 +01:00
Max Schaefer
6a75ebbae2 JavaScript: Update model of DOMException.
cf. https://developer.mozilla.org/en-US/docs/Web/API/DOMException/DOMException

(cherry picked from commit 8cc7f5c242)
2018-10-17 11:38:29 +01:00
Max Schaefer
d57e93d5c6 JavaScript: Fix typo in query help.
(cherry picked from commit 1ab943c16b)
2018-10-17 11:38:29 +01:00
Geoffrey White
e2a001f925 Merge pull request #285 from jbj/primitive-bb-joinorder
C++: Speed up primitive basic block calculation
2018-10-06 19:47:49 +01:00
Jonas Jensen
11e03b3161 C++: Fix primitive_basic_block_member join order
This predicate looked like a join of two already-computed predicates,
but it was a bit more complicated because the `*` operator expands into
two cases: the reflexive case and the transitive case. The join order
for the transitive case placed the `PrimitiveBasicBlock` charpred call
_after_ the `member_step+` call, which means that all the tuples of
`member_step+` passed through the pipeline.

This commit changes the implementation by fully writing out the
expansion of `*` into two cases, where the base case is manually
specialised to make sure the join orderer doesn't get tempted into
reusing the same strategy for both cases. This speeds up the predicate
from 2m38s to 1s on a snapshot of our own C/C++ code.
2018-10-05 14:26:04 +02:00
Jonas Jensen
265852058d C++: Faster implementation of BB entry node
The existing implementation of `primitive_basic_block_entry_node` was
"cleverly" computing two properties about `node` with a single
`strictcount`: whether `node` had multiple predecessors and whether any
of those predecessors had more than once successor. This was fast enough
on most snapshots, but on the snapshot of our own code it took 37
seconds to compute `primitive_basic_block_entry_node` and its auxiliary
predicates. This is likely to have affected other large snapshots too.

With this change, the property is computed like in our other languages,
and it brings the run time down to 4 seconds.
2018-10-05 14:20:03 +02:00
Jonas Jensen
7691c0a345 Merge pull request #280 from pavgust/imp/use-in-own-init
UseInOwnInitialiser: Refactor logic slightly.
2018-10-04 20:25:36 +02:00
Pavel Avgustinov
6d77a791ac UseInOwnInitialiser: Refactor logic slightly.
By pulling out the class `VariableAccessInInitialiser`, we can
avoid some redundant work on pathological databases, improving
performance.
2018-10-04 14:25:59 +01:00
Nick Rolfe
828d3cb138 Merge pull request #250 from adityasharad/version/1.18.1-dev
Version: Bump to 1.18.1 dev.
2018-10-01 10:59:52 +01:00
Aditya Sharad
1c71a856e1 Version: Bump to 1.18.1 dev. 2018-09-28 16:39:44 +01:00
44 changed files with 461 additions and 97 deletions

View File

@@ -52,13 +52,13 @@ Type stripType(Type t) {
/**
* Holds if `t` points to `base` via a specified number of levels of pointer
* indirection. Intermediate typedefs and array types are allowed.
* indirection. Intermediate typedefs and array types are allowed. Note that
* `base` is a stripped type (via `stripType`).
*/
predicate pointerIndirection(Type t, int indirection, Type base) {
exists(Type u |
u = stripType(t) and
u = stripType(base) and
not u instanceof PointerType and
(
base = stripType(t) and
not base instanceof PointerType and
indirection = 0
) or (
pointerIndirection(stripType(t).(PointerType).getBaseType(), indirection - 1, base)

View File

@@ -11,10 +11,21 @@
import cpp
from Initializer init, Variable v, VariableAccess va
where init.getDeclaration() = v
and va.getTarget() = v
and va.getParent*() = init
class VariableAccessInInitializer extends VariableAccess {
Variable var;
Initializer init;
VariableAccessInInitializer() {
init.getDeclaration() = var and
init.getExpr().getAChild*() = this
}
predicate initializesItself(Variable v, Initializer i) {
v = var and i = init and var = this.getTarget()
}
}
from Initializer init, Variable v, VariableAccessInInitializer va
where va.initializesItself(v, init)
and (
va.hasLValueToRValueConversion() or
exists (Assignment assn | assn.getLValue() = va) or

View File

@@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Semmle C/C++ Default Queries
Bundle-SymbolicName: com.semmle.plugin.semmlecode.cpp.queries;singleton:=true
Bundle-Version: 1.18.0
Bundle-Version: 1.18.2
Bundle-Vendor: Semmle Ltd.
Bundle-ActivationPolicy: lazy
Require-Bundle: com.semmle.plugin.qdt.ui;bundle-version="[1.18.0,1.18.0]"
Require-Bundle: com.semmle.plugin.qdt.ui;bundle-version="[1.18.2,1.18.2]"

View File

@@ -107,15 +107,17 @@ predicate defUndef(File f, string macroName) {
}
/**
* Header file `hf` looks like it contains an x-macro called
* `macroName`. That is, a macro that is used to interpret the
* Header file `hf` looks like it contains an x-macro.
* That is, a macro that is used to interpret the
* data in `hf`, usually defined just before including that file
* and undefined immediately afterwards.
*/
predicate hasXMacro(HeaderFile hf, string macroName) {
usesMacro(hf, macroName) and
forex(Include i | i.getIncludedFile() = hf |
defUndef(i.getFile(), macroName)
predicate hasXMacro(HeaderFile hf) {
exists(string macroName |
usesMacro(hf, macroName) and
forex(File f | f.getAnIncludedFile() = hf |
defUndef(f, macroName)
)
)
}
@@ -135,7 +137,7 @@ where not hf instanceof IncludeGuardedHeader
exists(UsingEntry ue | ue.getFile() = hf)
)
// Exclude files which look like they contain 'x-macros'
and not hasXMacro(hf, _)
and not hasXMacro(hf)
// Exclude files which are always #imported.
and not forex(Include i | i.getIncludedFile() = hf | i instanceof Import)
// Exclude files which are only included once.

View File

@@ -1,28 +1,5 @@
import cpp
/**
* Holds if `sizeof(s)` occurs as part of the parameter of a dynamic
* memory allocation (`malloc`, `realloc`, etc.), except if `sizeof(s)`
* only ever occurs as the immediate parameter to allocations.
*
* For example, holds for `s` if it occurs as
* ```
* malloc(sizeof(s) + 100 * sizeof(char))
* ```
* but not if it only ever occurs as
* ```
* malloc(sizeof(s))
* ```
*/
private predicate isDynamicallyAllocatedWithDifferentSize(Class s) {
exists(SizeofTypeOperator sof |
sof.getTypeOperand().getUnspecifiedType() = s |
// Check all ancestor nodes except the immediate parent for
// allocations.
isStdLibAllocationExpr(sof.getParent().(Expr).getParent+())
)
}
/**
* Holds if `v` is a member variable of `c` that looks like it might be variable sized in practice. For
* example:
@@ -33,15 +10,40 @@ private predicate isDynamicallyAllocatedWithDifferentSize(Class s) {
* };
* ```
* This requires that `v` is an array of size 0 or 1, and `v` is the last member of `c`. In addition,
* there must be at least one instance where a `c` pointer is allocated with additional space.
* there must be at least one instance where a `c` pointer is allocated with additional space. For
* example, holds for `c` if it occurs as
* ```
* malloc(sizeof(c) + 100 * sizeof(char))
* ```
* but not if it only ever occurs as
* ```
* malloc(sizeof(c))
* ```
*/
predicate memberMayBeVarSize(Class c, MemberVariable v) {
exists(int i |
// `v` is the last field in `c`
i = max(int j | c.getCanonicalMember(j) instanceof Field | j) and
v = c.getCanonicalMember(i) and
v.getType().getUnspecifiedType().(ArrayType).getSize() <= 1
) and
isDynamicallyAllocatedWithDifferentSize(c)
// v is an array of size at most 1
v.getType().getUnspecifiedType().(ArrayType).getArraySize() <= 1
) and (
exists(SizeofOperator so |
// `sizeof(c)` is taken
so.(SizeofTypeOperator).getTypeOperand().getUnspecifiedType() = c or
so.(SizeofExprOperator).getExprOperand().getType().getUnspecifiedType() = c |
// arithmetic is performed on the result
so.getParent*() instanceof AddExpr
) or exists(AddressOfExpr aoe |
// `&(c.v)` is taken
aoe.getAddressable() = v
) or exists(BuiltInOperationOffsetOf oo |
// `offsetof(c, v)` using a builtin
oo.getAChild().(VariableAccess).getTarget() = v
)
)
}
/**

View File

@@ -419,12 +419,17 @@ library class ExprEvaluator extends int {
)
}
/** Holds if the function `f` is considered by the analysis and may return `ret`. */
pragma[noinline]
private predicate interestingReturnValue(Function f, Expr ret) {
interestingFunction(_, f) and
returnStmt(f, ret)
}
private int getFunctionValue(Function f) {
interestingFunction(_, f)
and
// All returns must have the same int value
// And it must have at least one return
forex(Expr ret | returnStmt(f, ret) | result = getValueInternalNonSubExpr(ret))
forex(Expr ret | interestingReturnValue(f, ret) | result = getValueInternalNonSubExpr(ret))
}
/**

View File

@@ -31,8 +31,11 @@ private cached module Cached {
// or the node's predecessor has more than one successor,
// then the node is the start of a new primitive basic block.
or
strictcount (Node pred, Node other
| successors_extended(pred,node) and successors_extended(pred,other)) > 1
strictcount(Node pred | successors_extended(pred, node)) > 1
or
exists(ControlFlowNode pred | successors_extended(pred, node) |
strictcount(ControlFlowNode other | successors_extended(pred, other)) > 1
)
// If the node has zero predecessors then it is the start of
// a BB. However, the C++ AST contains many nodes with zero
@@ -63,8 +66,14 @@ private cached module Cached {
/** Holds if `node` is the `pos`th control-flow node in primitive basic block `bb`. */
cached
predicate primitive_basic_block_member(Node node, PrimitiveBasicBlock bb, int pos) {
pos = getMemberIndex(node) and
member_step*(bb, node)
primitive_basic_block_entry_node(bb) and
(
pos = 0 and
node = bb
or
pos = getMemberIndex(node) and
member_step+(bb, node)
)
}
/** Gets the number of control-flow nodes in the primitive basic block `bb`. */

View File

@@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Semmle C# Default Queries
Bundle-SymbolicName: com.semmle.plugin.semmlecode.csharp.queries;singleton:=true
Bundle-Version: 1.18.0
Bundle-Version: 1.18.2
Bundle-Vendor: Semmle Ltd.
Bundle-ActivationPolicy: lazy
Require-Bundle: com.semmle.plugin.qdt.ui;bundle-version="[1.18.0,1.18.0]"
Require-Bundle: com.semmle.plugin.qdt.ui;bundle-version="[1.18.2,1.18.2]"

View File

@@ -2,8 +2,8 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Semmle Default Java Queries
Bundle-SymbolicName: com.semmle.plugin.semmlecode.queries;singleton:=true
Bundle-Version: 1.18.0
Bundle-Version: 1.18.2
Bundle-Vendor: Semmle Ltd.
Bundle-ActivationPolicy: lazy
Require-Bundle: com.semmle.plugin.qdt.ui;bundle-version="[1.18.0,1.18.0]"
Require-Bundle: com.semmle.plugin.qdt.ui;bundle-version="[1.18.2,1.18.2]"

View File

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

View File

@@ -39,7 +39,7 @@ property of the name stored in variable <code>member</code>:
<p>
However, this test is ineffective as written: the operator <code>!</code> binds more
tighly than <code>in</code>, so it is applied first. Applying <code>!</code> to a
tightly than <code>in</code>, so it is applied first. Applying <code>!</code> to a
non-empty string yields <code>false</code>, so the <code>in</code> operator actually
ends up checking whether <code>obj</code> contains a property called <code>"false"</code>.
</p>

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Semmle JavaScript Default Queries
Bundle-SymbolicName: com.semmle.plugin.semmlecode.javascript.queries;singleton:=true
Bundle-Version: 1.18.0
Bundle-Version: 1.18.2
Bundle-Vendor: Semmle Ltd.
Bundle-ActivationPolicy: lazy
Require-Bundle: com.semmle.plugin.qdt.ui;bundle-version="[1.18.0,1.18.0]"
Require-Bundle: com.semmle.plugin.qdt.ui;bundle-version="[1.18.2,1.18.2]"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,3 +10,8 @@ let z = someGlobal;
export let w;
w = "w";
export let notAlwaysZero = 0;
function bump() {
++notAlwaysZero;
}

View File

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

View File

@@ -52,3 +52,6 @@ let z14 = foo_reexported;
import { something } from './reexport-unknown';
let z15 = something;
import { notAlwaysZero } from './a';
let z16 = notAlwaysZero;

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,3 +19,7 @@ interface ExpansiveMethod<T> {
interface ExpansiveFunctionType<T> {
x: () => ExpansiveFunctionType<T[]>;
}
interface ExpansiveSignatureTypeBound<T> {
foo : { <G extends ExpansiveSignatureTypeBound<T[]>>(x: G): G };
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
function f(x) {
console.log(x >= 1);
console.log(`${/r/g >= 1}`);
console.log( /r/g);
console.log( `${1}${1}`);
}

View File

@@ -0,0 +1,4 @@
function f() {
console.log(/foo/g);
console.log( /foo/g);
}

View File

@@ -0,0 +1,4 @@
function f(x) {
console.log(`template without placeholders`);
console.log(`template with placeholder ${x}.`);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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