Merge remote-tracking branch 'origin/main' into nickrolfe/incomplete_sanitization

This commit is contained in:
Nick Rolfe
2022-04-20 12:05:22 +01:00
84 changed files with 3698 additions and 1442 deletions

View File

@@ -383,7 +383,8 @@
"csharp/ql/test/TestUtilities/InlineExpectationsTest.qll",
"java/ql/test/TestUtilities/InlineExpectationsTest.qll",
"python/ql/test/TestUtilities/InlineExpectationsTest.qll",
"ruby/ql/test/TestUtilities/InlineExpectationsTest.qll"
"ruby/ql/test/TestUtilities/InlineExpectationsTest.qll",
"ql/ql/test/TestUtilities/InlineExpectationsTest.qll"
],
"C++ ExternalAPIs": [
"cpp/ql/src/Security/CWE/CWE-020/ExternalAPIs.qll",

View File

@@ -84,6 +84,7 @@ private int fileHeaderLimit(File f) {
fc = fileFirstComment(f) and
result =
min(int line |
// code ending the initial comments
exists(DeclarationEntry de, Location l |
l = de.getLocation() and
l.getFile() = f and
@@ -105,7 +106,13 @@ private int fileHeaderLimit(File f) {
line > fc
)
or
// end of the file
line = f.getMetrics().getNumberOfLines()
or
// rarely, we've seen extremely long sequences of initial comments
// (and/or limitations in the above constraints) cause an overflow of
// the maximum string length. So don't look past 1000 lines regardless.
line = 1000
)
)
}

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -1,5 +1,4 @@
name: codeql/csharp-downgrades
groups: csharp
version: 0.1.0-dev
downgrades: .
library: true
library: true

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -1,6 +1,7 @@
/**
* @name Capture sink models.
* @description Finds public methods that act as sinks as they flow into a a known sink.
* @kind diagnostic
* @id cs/utils/model-generator/sink-models
*/

View File

@@ -1,6 +1,7 @@
/**
* @name Capture source models.
* @description Finds APIs that act as sources as they expose already known sources.
* @kind diagnostic
* @id cs/utils/model-generator/source-models
*/

View File

@@ -1,6 +1,7 @@
/**
* @name Capture summary models.
* @description Finds applicable summary models to be used by other queries.
* @kind diagnostic
* @id cs/utils/model-generator/summary-models
*/

View File

@@ -100,12 +100,12 @@ As a consequence, ``A != B`` has a very different meaning to the :ref:`negation
- ``1 = [1 .. 2]`` holds, because ``1 = 1``.
- ``not 1 = [1 .. 2]`` doesn't hold, because there is a common value (``1``).
#. Compare ``1`` and ``none()`` (the "empty set"):
- ``1 != none()`` doesn't hold, because there are no values in ``none()``, so no values
#. Compare ``1`` and ``int empty() { none() }`` (a predicate defining the empty set of integers):
- ``1 != empty()`` doesn't hold, because there are no values in ``empty()``, so no values
that are not equal to ``1``.
- ``1 = none()`` also doesn't hold, because there are no values in ``none()``, so no values
- ``1 = empty()`` also doesn't hold, because there are no values in ``empty()``, so no values
that are equal to ``1``.
- ``not 1 = none()`` holds, because there are no common values.
- ``not 1 = empty()`` holds, because there are no common values.
.. index:: instanceof
.. _type-checks:
@@ -295,9 +295,48 @@ necessary, since they highlight the default precedence. You usually only add par
override the default precedence, but you can also add them to make your code easier to read
(even if they aren't required).
QL also has two nullary connectives indicating the always true formula,
``any()``, and the always false formula, ``none()``.
The logical connectives in QL work similarly to Boolean connectives in other programming
languages. Here is a brief overview:
.. index:: any, true
.. _true:
``any()``
=========
The built-in predicate ``any()`` is a formula that always holds.
**Example**
The following predicate defines the set of all expressions.
.. code-block:: ql
Expr allExpressions() {
any()
}
.. index:: none, false
.. _false:
``none()``
==========
The built-in predicate ``none()`` is a formula that never holds.
**Example**
The following predicate defines the empty set of integers.
.. code-block:: ql
int emptySet() {
none()
}
.. index:: not, negation
.. _negation:

View File

@@ -387,26 +387,26 @@ from ``OneTwoThree`` and ``int``.
Non-extending subtypes
======================
Besides extending base types, classes can also declare `instanceof` relationships with other types.
Declaring a class as `instanceof Foo` is roughly equivalent to saying `this instanceof Foo` in the characteristic predicate.
The main differences are that you can call methods on Bar via `super` and you can get better optimisation.
Besides extending base types, classes can also declare ``instanceof`` relationships with other types.
Declaring a class as ``instanceof Foo`` is roughly equivalent to saying ``this instanceof Foo`` in the characteristic predicate.
The main differences are that you can call methods on ``Bar`` via ``super`` and you can get better optimisation.
.. code-block:: ql
class Foo extends int {
Foo() { this in [1 .. 10] }
string foo_method() { result = "foo" }
string fooMethod() { result = "foo" }
}
class Bar instanceof Foo {
string toString() { result = super.foo_method() }
string toString() { result = super.fooMethod() }
}
In this example, the characteristic predicate from `Foo` also applies to `Bar`.
However, `foo_method` is not exposed in `Bar`, so the query `select any(Bar b).foo_method()`
In this example, the characteristic predicate from ``Foo`` also applies to ``Bar``.
However, ``fooMethod`` is not exposed in ``Bar``, so the query ``select any(Bar b).fooMethod()``
results in a compile time error. Note from the example that it is still possible to access
methods from instanceof supertypes from within the specialising class with the `super` keyword.
methods from instanceof supertypes from within the specialising class with the ``super`` keyword.
Crucially, the instanceof **supertypes** are not **base types**.
This means that these supertypes do not participate in overriding, and any fields of such
@@ -430,10 +430,10 @@ The following example demonstrates this.
override string foo() { result = "bar" }
}
Here, the method `Bar::foo` does not override `Foo::foo`.
Instead, it overrides only `Interface::foo`.
This means that `select any(Foo f).foo()` yields only `foo`.
Had `Bar` been defined as `extends Foo`, then `select any(Foo b)` would yield `bar`.
Here, the method ``Bar::foo`` does not override ``Foo::foo``.
Instead, it overrides only ``Interface::foo``.
This means that ``select any(Foo f).foo()`` yields ``foo``.
Had ``Bar`` been defined as ``extends Foo``, then ``select any(Foo f).foo()`` would yield ``bar``.
.. _character-types:
.. _domain-types:

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -75,7 +75,11 @@ private DataFlow::Node getANotASink(NotASinkReason reason) {
*/
private DataFlow::Node getAnUnknown(Query query) {
getAtmCfg(query).isEffectiveSink(result) and
// Effective sinks should exclude sinks but this is a defensive requirement
not result = getASink(query) and
// Effective sinks should exclude NotASink but for some queries (e.g. Xss) this is currently not always the case and
// so this is a defensive requirement
not result = getANotASink(_) and
// Only consider the source code for the project being analyzed.
exists(result.getFile().getRelativePath())
}

View File

@@ -251,6 +251,50 @@ endpoints
| index.js:78:30:78:39 | "someData" | Xss | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:78:30:78:39 | "someData" | Xss | notASinkReason | LoggerMethod | string |
| index.js:78:30:78:39 | "someData" | Xss | sinkLabel | NotASink | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | NosqlInjection | hasFlowFromSource | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | NosqlInjection | isConstantExpression | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | NosqlInjection | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | NosqlInjection | notASinkReason | ClientRequest | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | NosqlInjection | notASinkReason | JQueryArgument | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | NosqlInjection | sinkLabel | NotASink | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | SqlInjection | hasFlowFromSource | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | SqlInjection | isConstantExpression | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | SqlInjection | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | SqlInjection | notASinkReason | ClientRequest | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | SqlInjection | notASinkReason | JQueryArgument | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | SqlInjection | sinkLabel | NotASink | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | TaintedPath | hasFlowFromSource | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | TaintedPath | isConstantExpression | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | TaintedPath | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | TaintedPath | notASinkReason | ClientRequest | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | TaintedPath | notASinkReason | JQueryArgument | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | TaintedPath | sinkLabel | NotASink | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | Xss | hasFlowFromSource | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | Xss | isConstantExpression | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | Xss | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | Xss | notASinkReason | ClientRequest | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | Xss | notASinkReason | JQueryArgument | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | Xss | sinkLabel | NotASink | string |
| index.js:84:12:84:18 | foo.bar | NosqlInjection | hasFlowFromSource | false | boolean |
| index.js:84:12:84:18 | foo.bar | NosqlInjection | isConstantExpression | false | boolean |
| index.js:84:12:84:18 | foo.bar | NosqlInjection | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:84:12:84:18 | foo.bar | NosqlInjection | notASinkReason | ClientRequest | string |
| index.js:84:12:84:18 | foo.bar | NosqlInjection | sinkLabel | NotASink | string |
| index.js:84:12:84:18 | foo.bar | SqlInjection | hasFlowFromSource | false | boolean |
| index.js:84:12:84:18 | foo.bar | SqlInjection | isConstantExpression | false | boolean |
| index.js:84:12:84:18 | foo.bar | SqlInjection | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:84:12:84:18 | foo.bar | SqlInjection | notASinkReason | ClientRequest | string |
| index.js:84:12:84:18 | foo.bar | SqlInjection | sinkLabel | NotASink | string |
| index.js:84:12:84:18 | foo.bar | TaintedPath | hasFlowFromSource | false | boolean |
| index.js:84:12:84:18 | foo.bar | TaintedPath | isConstantExpression | false | boolean |
| index.js:84:12:84:18 | foo.bar | TaintedPath | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:84:12:84:18 | foo.bar | TaintedPath | notASinkReason | ClientRequest | string |
| index.js:84:12:84:18 | foo.bar | TaintedPath | sinkLabel | NotASink | string |
| index.js:84:12:84:18 | foo.bar | Xss | hasFlowFromSource | false | boolean |
| index.js:84:12:84:18 | foo.bar | Xss | isConstantExpression | false | boolean |
| index.js:84:12:84:18 | foo.bar | Xss | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:84:12:84:18 | foo.bar | Xss | notASinkReason | ClientRequest | string |
| index.js:84:12:84:18 | foo.bar | Xss | sinkLabel | NotASink | string |
tokenFeatures
| applications/examples/static/epydoc/epydoc.js:2:15:2:33 | "Should be ignored" | argumentIndex | 0 |
| applications/examples/static/epydoc/epydoc.js:2:15:2:33 | "Should be ignored" | calleeAccessPath | |
@@ -412,3 +456,19 @@ tokenFeatures
| index.js:78:30:78:39 | "someData" | enclosingFunctionBody | console log someData |
| index.js:78:30:78:39 | "someData" | enclosingFunctionName | identity#functionalargument |
| index.js:78:30:78:39 | "someData" | receiverName | console |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | argumentIndex | 0 |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | calleeAccessPath | |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | calleeAccessPathWithStructuralInfo | |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | calleeApiName | |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | calleeName | ajax |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | enclosingFunctionBody | foo $ ajax url foo bar |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | enclosingFunctionName | effectiveSinkAndNotASink |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | receiverName | $ |
| index.js:84:12:84:18 | foo.bar | argumentIndex | |
| index.js:84:12:84:18 | foo.bar | calleeAccessPath | |
| index.js:84:12:84:18 | foo.bar | calleeAccessPathWithStructuralInfo | |
| index.js:84:12:84:18 | foo.bar | calleeApiName | |
| index.js:84:12:84:18 | foo.bar | calleeName | |
| index.js:84:12:84:18 | foo.bar | enclosingFunctionBody | foo $ ajax url foo bar |
| index.js:84:12:84:18 | foo.bar | enclosingFunctionName | effectiveSinkAndNotASink |
| index.js:84:12:84:18 | foo.bar | receiverName | |

View File

@@ -231,6 +231,50 @@ endpoints
| index.js:78:30:78:39 | "someData" | Xss | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:78:30:78:39 | "someData" | Xss | notASinkReason | LoggerMethod | string |
| index.js:78:30:78:39 | "someData" | Xss | sinkLabel | NotASink | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | NosqlInjection | hasFlowFromSource | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | NosqlInjection | isConstantExpression | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | NosqlInjection | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | NosqlInjection | notASinkReason | ClientRequest | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | NosqlInjection | notASinkReason | JQueryArgument | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | NosqlInjection | sinkLabel | NotASink | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | SqlInjection | hasFlowFromSource | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | SqlInjection | isConstantExpression | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | SqlInjection | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | SqlInjection | notASinkReason | ClientRequest | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | SqlInjection | notASinkReason | JQueryArgument | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | SqlInjection | sinkLabel | NotASink | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | TaintedPath | hasFlowFromSource | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | TaintedPath | isConstantExpression | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | TaintedPath | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | TaintedPath | notASinkReason | ClientRequest | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | TaintedPath | notASinkReason | JQueryArgument | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | TaintedPath | sinkLabel | NotASink | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | Xss | hasFlowFromSource | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | Xss | isConstantExpression | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | Xss | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | Xss | notASinkReason | ClientRequest | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | Xss | notASinkReason | JQueryArgument | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | Xss | sinkLabel | NotASink | string |
| index.js:84:12:84:18 | foo.bar | NosqlInjection | hasFlowFromSource | false | boolean |
| index.js:84:12:84:18 | foo.bar | NosqlInjection | isConstantExpression | false | boolean |
| index.js:84:12:84:18 | foo.bar | NosqlInjection | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:84:12:84:18 | foo.bar | NosqlInjection | notASinkReason | ClientRequest | string |
| index.js:84:12:84:18 | foo.bar | NosqlInjection | sinkLabel | NotASink | string |
| index.js:84:12:84:18 | foo.bar | SqlInjection | hasFlowFromSource | false | boolean |
| index.js:84:12:84:18 | foo.bar | SqlInjection | isConstantExpression | false | boolean |
| index.js:84:12:84:18 | foo.bar | SqlInjection | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:84:12:84:18 | foo.bar | SqlInjection | notASinkReason | ClientRequest | string |
| index.js:84:12:84:18 | foo.bar | SqlInjection | sinkLabel | NotASink | string |
| index.js:84:12:84:18 | foo.bar | TaintedPath | hasFlowFromSource | false | boolean |
| index.js:84:12:84:18 | foo.bar | TaintedPath | isConstantExpression | false | boolean |
| index.js:84:12:84:18 | foo.bar | TaintedPath | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:84:12:84:18 | foo.bar | TaintedPath | notASinkReason | ClientRequest | string |
| index.js:84:12:84:18 | foo.bar | TaintedPath | sinkLabel | NotASink | string |
| index.js:84:12:84:18 | foo.bar | Xss | hasFlowFromSource | false | boolean |
| index.js:84:12:84:18 | foo.bar | Xss | isConstantExpression | false | boolean |
| index.js:84:12:84:18 | foo.bar | Xss | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:84:12:84:18 | foo.bar | Xss | notASinkReason | ClientRequest | string |
| index.js:84:12:84:18 | foo.bar | Xss | sinkLabel | NotASink | string |
tokenFeatures
| index.js:1:25:1:33 | "express" | argumentIndex | 0 |
| index.js:1:25:1:33 | "express" | calleeAccessPath | |
@@ -384,3 +428,19 @@ tokenFeatures
| index.js:78:30:78:39 | "someData" | enclosingFunctionBody | console log someData |
| index.js:78:30:78:39 | "someData" | enclosingFunctionName | identity#functionalargument |
| index.js:78:30:78:39 | "someData" | receiverName | console |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | argumentIndex | 0 |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | calleeAccessPath | |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | calleeAccessPathWithStructuralInfo | |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | calleeApiName | |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | calleeName | ajax |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | enclosingFunctionBody | foo $ ajax url foo bar |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | enclosingFunctionName | effectiveSinkAndNotASink |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | receiverName | $ |
| index.js:84:12:84:18 | foo.bar | argumentIndex | |
| index.js:84:12:84:18 | foo.bar | calleeAccessPath | |
| index.js:84:12:84:18 | foo.bar | calleeAccessPathWithStructuralInfo | |
| index.js:84:12:84:18 | foo.bar | calleeApiName | |
| index.js:84:12:84:18 | foo.bar | calleeName | |
| index.js:84:12:84:18 | foo.bar | enclosingFunctionBody | foo $ ajax url foo bar |
| index.js:84:12:84:18 | foo.bar | enclosingFunctionName | effectiveSinkAndNotASink |
| index.js:84:12:84:18 | foo.bar | receiverName | |

View File

@@ -31,6 +31,50 @@ endpoints
| index.js:28:13:28:28 | UNDEFINED_GLOBAL | NosqlInjection | isConstantExpression | false | boolean |
| index.js:28:13:28:28 | UNDEFINED_GLOBAL | NosqlInjection | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:28:13:28:28 | UNDEFINED_GLOBAL | NosqlInjection | sinkLabel | Sink | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | NosqlInjection | hasFlowFromSource | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | NosqlInjection | isConstantExpression | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | NosqlInjection | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | NosqlInjection | notASinkReason | ClientRequest | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | NosqlInjection | notASinkReason | JQueryArgument | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | NosqlInjection | sinkLabel | NotASink | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | SqlInjection | hasFlowFromSource | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | SqlInjection | isConstantExpression | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | SqlInjection | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | SqlInjection | notASinkReason | ClientRequest | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | SqlInjection | notASinkReason | JQueryArgument | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | SqlInjection | sinkLabel | NotASink | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | TaintedPath | hasFlowFromSource | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | TaintedPath | isConstantExpression | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | TaintedPath | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | TaintedPath | notASinkReason | ClientRequest | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | TaintedPath | notASinkReason | JQueryArgument | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | TaintedPath | sinkLabel | NotASink | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | Xss | hasFlowFromSource | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | Xss | isConstantExpression | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | Xss | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | Xss | notASinkReason | ClientRequest | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | Xss | notASinkReason | JQueryArgument | string |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | Xss | sinkLabel | NotASink | string |
| index.js:84:12:84:18 | foo.bar | NosqlInjection | hasFlowFromSource | false | boolean |
| index.js:84:12:84:18 | foo.bar | NosqlInjection | isConstantExpression | false | boolean |
| index.js:84:12:84:18 | foo.bar | NosqlInjection | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:84:12:84:18 | foo.bar | NosqlInjection | notASinkReason | ClientRequest | string |
| index.js:84:12:84:18 | foo.bar | NosqlInjection | sinkLabel | NotASink | string |
| index.js:84:12:84:18 | foo.bar | SqlInjection | hasFlowFromSource | false | boolean |
| index.js:84:12:84:18 | foo.bar | SqlInjection | isConstantExpression | false | boolean |
| index.js:84:12:84:18 | foo.bar | SqlInjection | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:84:12:84:18 | foo.bar | SqlInjection | notASinkReason | ClientRequest | string |
| index.js:84:12:84:18 | foo.bar | SqlInjection | sinkLabel | NotASink | string |
| index.js:84:12:84:18 | foo.bar | TaintedPath | hasFlowFromSource | false | boolean |
| index.js:84:12:84:18 | foo.bar | TaintedPath | isConstantExpression | false | boolean |
| index.js:84:12:84:18 | foo.bar | TaintedPath | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:84:12:84:18 | foo.bar | TaintedPath | notASinkReason | ClientRequest | string |
| index.js:84:12:84:18 | foo.bar | TaintedPath | sinkLabel | NotASink | string |
| index.js:84:12:84:18 | foo.bar | Xss | hasFlowFromSource | false | boolean |
| index.js:84:12:84:18 | foo.bar | Xss | isConstantExpression | false | boolean |
| index.js:84:12:84:18 | foo.bar | Xss | isExcludedFromEndToEndEvaluation | false | boolean |
| index.js:84:12:84:18 | foo.bar | Xss | notASinkReason | ClientRequest | string |
| index.js:84:12:84:18 | foo.bar | Xss | sinkLabel | NotASink | string |
tokenFeatures
| index.js:9:15:9:45 | { 'isAd ... Admin } | argumentIndex | 0 |
| index.js:9:15:9:45 | { 'isAd ... Admin } | calleeAccessPath | mongoose model find |
@@ -64,3 +108,19 @@ tokenFeatures
| index.js:28:13:28:28 | UNDEFINED_GLOBAL | enclosingFunctionBody | User find UNDEFINED_GLOBAL |
| index.js:28:13:28:28 | UNDEFINED_GLOBAL | enclosingFunctionName | notConstantExpression |
| index.js:28:13:28:28 | UNDEFINED_GLOBAL | receiverName | User |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | argumentIndex | 0 |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | calleeAccessPath | |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | calleeAccessPathWithStructuralInfo | |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | calleeApiName | |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | calleeName | ajax |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | enclosingFunctionBody | foo $ ajax url foo bar |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | enclosingFunctionName | effectiveSinkAndNotASink |
| index.js:83:10:85:3 | {\\n " ... ar,\\n } | receiverName | $ |
| index.js:84:12:84:18 | foo.bar | argumentIndex | |
| index.js:84:12:84:18 | foo.bar | calleeAccessPath | |
| index.js:84:12:84:18 | foo.bar | calleeAccessPathWithStructuralInfo | |
| index.js:84:12:84:18 | foo.bar | calleeApiName | |
| index.js:84:12:84:18 | foo.bar | calleeName | |
| index.js:84:12:84:18 | foo.bar | enclosingFunctionBody | foo $ ajax url foo bar |
| index.js:84:12:84:18 | foo.bar | enclosingFunctionName | effectiveSinkAndNotASink |
| index.js:84:12:84:18 | foo.bar | receiverName | |

View File

@@ -77,3 +77,10 @@ function veryLongFunctionBody() {
// We should name the anonymous function here that's passed as an argument to `identity`.
identity(() => console.log("someData"));
}
function effectiveSinkAndNotASink(foo) {
$.ajax({
"url": foo.bar,
});
}

View File

@@ -27,3 +27,133 @@ module Handlebars {
SafeString() { this = any(Handlebars h).getAConstructorInvocation("SafeString") }
}
}
/** Provides logic for taint steps for the handlebars library. */
private module HandlebarsTaintSteps {
/**
* Gets a reference to a compiled Handlebars template.
*/
private DataFlow::SourceNode compiledTemplate(DataFlow::CallNode compileCall) {
result = compiledTemplate(DataFlow::TypeTracker::end(), compileCall)
}
private DataFlow::SourceNode compiledTemplate(
DataFlow::TypeTracker t, DataFlow::CallNode compileCall
) {
t.start() and
result = any(Handlebars::Handlebars hb).getAMethodCall(["compile", "template"]) and
result = compileCall
or
exists(DataFlow::TypeTracker t2 | result = compiledTemplate(t2, compileCall).track(t2, t))
}
/**
* Gets a reference to a parameter of a registered Handlebars helper.
*
* ```javascript
* function loudHelper(text) {
* return text.toUpperCase();
* }
*
* hb.registerHelper("loud", loudHelper);
* ```
* In this example, `getRegisteredHelperParameter("loud", func, 0)` will bind `func` to
* the `FunctionNode` representing `function loudHelper`, and return its parameter `text`.
*/
private DataFlow::ParameterNode getRegisteredHelperParam(
string helperName, DataFlow::FunctionNode helperFunction, int paramIndex
) {
exists(DataFlow::CallNode registerHelperCall |
registerHelperCall = any(Handlebars::Handlebars hb).getAMemberCall("registerHelper") and
registerHelperCall.getArgument(0).mayHaveStringValue(helperName) and
helperFunction = registerHelperCall.getArgument(1).getAFunctionValue() and
result = helperFunction.getParameter(paramIndex)
)
}
/**
* Gets a `call` (which is a block wrapped inside curly braces inside the template) from `templateText`.
*
* For example, `getAHelperCallFromTemplate("Hello {{loud customer}}")` will return `"loud customer"`.
*/
bindingset[templateText]
private string getAHelperCallFromTemplate(string templateText) {
result = templateText.regexpFind("\\{\\{[^}]+\\}\\}", _, _).regexpReplaceAll("[{}]", "").trim() and
result.regexpMatch(".*\\s.*")
}
/**
* Holds for calls to helpers from handlebars templates.
*
* ```javascript
* hb.compile("contents of file {{path}} are: {{catFile path}} {{echo p1 p2}}");
* ```
*
* In the example, the predicate will hold for:
*
* * helperName="catFile", argIdx=1, arg="path"
* * helperName="echo", argIdx=1, arg="p1"
* * helperName="echo", argIdx=2, arg="p2"
*
* The initial `{{path}}` will not be considered, as it has no arguments.
*/
bindingset[templateText]
private predicate isTemplateHelperCallArg(
string templateText, string helperName, int argIdx, string argVal
) {
exists(string call | call = getAHelperCallFromTemplate(templateText) |
helperName = call.regexpFind("[^\\s]+", 0, _) and
argIdx >= 0 and
argVal = call.regexpFind("[^\\s]+", argIdx + 1, _)
)
}
/**
* Holds if there's a step from `pred` to `succ` due to templating data being
* passed from a templating call to a registered helper via a parameter.
*
* To establish the step, we look at the template passed to `compile`, and will
* only track steps from templates to helpers they actually reference.
*
* ```javascript
* function loudHelper(text) {
* // ^^^^ succ
* return text.toUpperCase();
* }
*
* hb.registerHelper("loud", loudHelper);
*
* const template = hb.compile("Hello, {{loud name}}!");
*
* template({name: "user"});
* // ^^^^^^ pred
* ```
*/
private predicate isHandlebarsArgStep(DataFlow::Node pred, DataFlow::Node succ) {
exists(
string helperName, DataFlow::CallNode templatingCall, DataFlow::CallNode compileCall,
DataFlow::FunctionNode helperFunction
|
templatingCall = compiledTemplate(compileCall).getACall() and
exists(string templateText, string paramName, int argIdx |
compileCall.getArgument(0).mayHaveStringValue(templateText)
|
pred = templatingCall.getArgument(0).getALocalSource().getAPropertyWrite(paramName).getRhs() and
isTemplateHelperCallArg(templateText, helperName, argIdx, paramName) and
succ = getRegisteredHelperParam(helperName, helperFunction, argIdx)
)
)
}
/**
* A shared flow step from passing data to a handlebars template with
* helpers registered.
*/
class HandlebarsStep extends DataFlow::SharedFlowStep {
DataFlow::CallNode templatingCall;
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
isHandlebarsArgStep(pred, succ)
}
}
}

View File

@@ -850,7 +850,7 @@ module TaintedPath {
/**
* Holds if we should include a step from `src -> dst` with labels `srclabel -> dstlabel`, and the
* standard taint step `src -> dst` should be suppresesd.
* standard taint step `src -> dst` should be suppressed.
*/
private predicate isPosixPathStep(
DataFlow::Node src, DataFlow::Node dst, Label::PosixPath srclabel, Label::PosixPath dstlabel

View File

@@ -162,6 +162,11 @@ module XssThroughDom {
}
}
/** The `files` property of an `<input />` element */
class FilesSource extends Source {
FilesSource() { this = DOM::domValueRef().getAPropertyRead("files") }
}
/**
* A module for form inputs seen as sources for xss-through-dom.
*/

View File

@@ -36,6 +36,11 @@ class Configuration extends TaintTracking::Configuration {
DomBasedXss::isOptionallySanitizedEdge(pred, succ)
}
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
succ = DataFlow::globalVarRef("URL").getAMemberCall("createObjectURL") and
pred = succ.(DataFlow::InvokeNode).getArgument(0)
}
override predicate hasFlowPath(DataFlow::SourcePathNode src, DataFlow::SinkPathNode sink) {
super.hasFlowPath(src, sink) and
// filtering away readings of `src` that end in a URL sink.

View File

@@ -1541,6 +1541,34 @@ nodes
| express.js:8:20:8:32 | req.query.bar |
| express.js:8:20:8:32 | req.query.bar |
| express.js:8:20:8:32 | req.query.bar |
| handlebars.js:10:51:10:58 | filePath |
| handlebars.js:10:51:10:58 | filePath |
| handlebars.js:10:51:10:58 | filePath |
| handlebars.js:10:51:10:58 | filePath |
| handlebars.js:11:32:11:39 | filePath |
| handlebars.js:11:32:11:39 | filePath |
| handlebars.js:11:32:11:39 | filePath |
| handlebars.js:11:32:11:39 | filePath |
| handlebars.js:11:32:11:39 | filePath |
| handlebars.js:13:73:13:80 | filePath |
| handlebars.js:13:73:13:80 | filePath |
| handlebars.js:13:73:13:80 | filePath |
| handlebars.js:13:73:13:80 | filePath |
| handlebars.js:15:25:15:32 | filePath |
| handlebars.js:15:25:15:32 | filePath |
| handlebars.js:15:25:15:32 | filePath |
| handlebars.js:15:25:15:32 | filePath |
| handlebars.js:15:25:15:32 | filePath |
| handlebars.js:29:46:29:60 | req.params.path |
| handlebars.js:29:46:29:60 | req.params.path |
| handlebars.js:29:46:29:60 | req.params.path |
| handlebars.js:29:46:29:60 | req.params.path |
| handlebars.js:29:46:29:60 | req.params.path |
| handlebars.js:43:15:43:29 | req.params.path |
| handlebars.js:43:15:43:29 | req.params.path |
| handlebars.js:43:15:43:29 | req.params.path |
| handlebars.js:43:15:43:29 | req.params.path |
| handlebars.js:43:15:43:29 | req.params.path |
| normalizedPaths.js:11:7:11:27 | path |
| normalizedPaths.js:11:7:11:27 | path |
| normalizedPaths.js:11:7:11:27 | path |
@@ -6414,6 +6442,38 @@ edges
| TaintedPath.js:211:24:211:30 | req.url | TaintedPath.js:211:14:211:37 | url.par ... , true) |
| TaintedPath.js:211:24:211:30 | req.url | TaintedPath.js:211:14:211:37 | url.par ... , true) |
| express.js:8:20:8:32 | req.query.bar | express.js:8:20:8:32 | req.query.bar |
| handlebars.js:10:51:10:58 | filePath | handlebars.js:11:32:11:39 | filePath |
| handlebars.js:10:51:10:58 | filePath | handlebars.js:11:32:11:39 | filePath |
| handlebars.js:10:51:10:58 | filePath | handlebars.js:11:32:11:39 | filePath |
| handlebars.js:10:51:10:58 | filePath | handlebars.js:11:32:11:39 | filePath |
| handlebars.js:10:51:10:58 | filePath | handlebars.js:11:32:11:39 | filePath |
| handlebars.js:10:51:10:58 | filePath | handlebars.js:11:32:11:39 | filePath |
| handlebars.js:10:51:10:58 | filePath | handlebars.js:11:32:11:39 | filePath |
| handlebars.js:10:51:10:58 | filePath | handlebars.js:11:32:11:39 | filePath |
| handlebars.js:13:73:13:80 | filePath | handlebars.js:15:25:15:32 | filePath |
| handlebars.js:13:73:13:80 | filePath | handlebars.js:15:25:15:32 | filePath |
| handlebars.js:13:73:13:80 | filePath | handlebars.js:15:25:15:32 | filePath |
| handlebars.js:13:73:13:80 | filePath | handlebars.js:15:25:15:32 | filePath |
| handlebars.js:13:73:13:80 | filePath | handlebars.js:15:25:15:32 | filePath |
| handlebars.js:13:73:13:80 | filePath | handlebars.js:15:25:15:32 | filePath |
| handlebars.js:13:73:13:80 | filePath | handlebars.js:15:25:15:32 | filePath |
| handlebars.js:13:73:13:80 | filePath | handlebars.js:15:25:15:32 | filePath |
| handlebars.js:29:46:29:60 | req.params.path | handlebars.js:10:51:10:58 | filePath |
| handlebars.js:29:46:29:60 | req.params.path | handlebars.js:10:51:10:58 | filePath |
| handlebars.js:29:46:29:60 | req.params.path | handlebars.js:10:51:10:58 | filePath |
| handlebars.js:29:46:29:60 | req.params.path | handlebars.js:10:51:10:58 | filePath |
| handlebars.js:29:46:29:60 | req.params.path | handlebars.js:10:51:10:58 | filePath |
| handlebars.js:29:46:29:60 | req.params.path | handlebars.js:10:51:10:58 | filePath |
| handlebars.js:29:46:29:60 | req.params.path | handlebars.js:10:51:10:58 | filePath |
| handlebars.js:29:46:29:60 | req.params.path | handlebars.js:10:51:10:58 | filePath |
| handlebars.js:43:15:43:29 | req.params.path | handlebars.js:13:73:13:80 | filePath |
| handlebars.js:43:15:43:29 | req.params.path | handlebars.js:13:73:13:80 | filePath |
| handlebars.js:43:15:43:29 | req.params.path | handlebars.js:13:73:13:80 | filePath |
| handlebars.js:43:15:43:29 | req.params.path | handlebars.js:13:73:13:80 | filePath |
| handlebars.js:43:15:43:29 | req.params.path | handlebars.js:13:73:13:80 | filePath |
| handlebars.js:43:15:43:29 | req.params.path | handlebars.js:13:73:13:80 | filePath |
| handlebars.js:43:15:43:29 | req.params.path | handlebars.js:13:73:13:80 | filePath |
| handlebars.js:43:15:43:29 | req.params.path | handlebars.js:13:73:13:80 | filePath |
| normalizedPaths.js:11:7:11:27 | path | normalizedPaths.js:13:19:13:22 | path |
| normalizedPaths.js:11:7:11:27 | path | normalizedPaths.js:13:19:13:22 | path |
| normalizedPaths.js:11:7:11:27 | path | normalizedPaths.js:13:19:13:22 | path |
@@ -9844,6 +9904,8 @@ edges
| TaintedPath.js:213:45:213:48 | path | TaintedPath.js:211:24:211:30 | req.url | TaintedPath.js:213:45:213:48 | path | This path depends on $@. | TaintedPath.js:211:24:211:30 | req.url | a user-provided value |
| TaintedPath.js:214:35:214:38 | path | TaintedPath.js:211:24:211:30 | req.url | TaintedPath.js:214:35:214:38 | path | This path depends on $@. | TaintedPath.js:211:24:211:30 | req.url | a user-provided value |
| express.js:8:20:8:32 | req.query.bar | express.js:8:20:8:32 | req.query.bar | express.js:8:20:8:32 | req.query.bar | This path depends on $@. | express.js:8:20:8:32 | req.query.bar | a user-provided value |
| handlebars.js:11:32:11:39 | filePath | handlebars.js:29:46:29:60 | req.params.path | handlebars.js:11:32:11:39 | filePath | This path depends on $@. | handlebars.js:29:46:29:60 | req.params.path | a user-provided value |
| handlebars.js:15:25:15:32 | filePath | handlebars.js:43:15:43:29 | req.params.path | handlebars.js:15:25:15:32 | filePath | This path depends on $@. | handlebars.js:43:15:43:29 | req.params.path | a user-provided value |
| normalizedPaths.js:13:19:13:22 | path | normalizedPaths.js:11:14:11:27 | req.query.path | normalizedPaths.js:13:19:13:22 | path | This path depends on $@. | normalizedPaths.js:11:14:11:27 | req.query.path | a user-provided value |
| normalizedPaths.js:14:19:14:29 | './' + path | normalizedPaths.js:11:14:11:27 | req.query.path | normalizedPaths.js:14:19:14:29 | './' + path | This path depends on $@. | normalizedPaths.js:11:14:11:27 | req.query.path | a user-provided value |
| normalizedPaths.js:15:19:15:38 | path + '/index.html' | normalizedPaths.js:11:14:11:27 | req.query.path | normalizedPaths.js:15:19:15:38 | path + '/index.html' | This path depends on $@. | normalizedPaths.js:11:14:11:27 | req.query.path | a user-provided value |

View File

@@ -0,0 +1,52 @@
const express = require('express');
const hb = require("handlebars");
const fs = require("fs");
const app = express();
const data = {};
function init() {
hb.registerHelper("catFile", function catFile(filePath) {
return fs.readFileSync(filePath); // SINK (reads file)
});
hb.registerHelper("prependToLines", function prependToLines(prefix, filePath) {
return fs
.readFileSync(filePath)
.split("\n")
.map((line) => prefix + line)
.join("\n");
});
data.compiledFileAccess = hb.compile("contents of file {{path}} are: {{catFile path}}")
data.compiledBenign = hb.compile("hello, {{name}}");
data.compiledUnknown = hb.compile(fs.readFileSync("greeting.template"));
data.compiledMixed = hb.compile("helpers may have several args, like here: {{prependToLines prefix path}}");
}
init();
app.get('/some/path1', function (req, res) {
res.send(data.compiledFileAccess({ path: req.params.path })); // NOT ALLOWED (template uses vulnerable catFile)
});
app.get('/some/path2', function (req, res) {
res.send(data.compiledBenign({ name: req.params.name })); // ALLOWED (this template does not use catFile)
});
app.get('/some/path3', function (req, res) {
res.send(data.compiledUnknown({ name: req.params.name })); // ALLOWED (could be using a vulnerable helper, but we'll assume it's ok)
});
app.get('/some/path4', function (req, res) {
res.send(data.compiledMixed({
prefix: ">>> ",
path: req.params.path // NOT ALLOWED (template uses vulnerable helper)
}));
});
app.get('/some/path5', function (req, res) {
res.send(data.compiledMixed({
prefix: req.params.prefix, // ALLOWED (this parameter is safe)
path: "data/path-5.txt"
}));
});

View File

@@ -129,6 +129,16 @@ nodes
| xss-through-dom.js:115:16:115:18 | src |
| xss-through-dom.js:117:26:117:28 | src |
| xss-through-dom.js:117:26:117:28 | src |
| xss-through-dom.js:120:23:120:37 | ev.target.files |
| xss-through-dom.js:120:23:120:37 | ev.target.files |
| xss-through-dom.js:120:23:120:40 | ev.target.files[0] |
| xss-through-dom.js:120:23:120:45 | ev.targ ... 0].name |
| xss-through-dom.js:120:23:120:45 | ev.targ ... 0].name |
| xss-through-dom.js:122:33:122:71 | URL.cre ... les[0]) |
| xss-through-dom.js:122:33:122:71 | URL.cre ... les[0]) |
| xss-through-dom.js:122:53:122:67 | ev.target.files |
| xss-through-dom.js:122:53:122:67 | ev.target.files |
| xss-through-dom.js:122:53:122:70 | ev.target.files[0] |
edges
| forms.js:8:23:8:28 | values | forms.js:9:31:9:36 | values |
| forms.js:8:23:8:28 | values | forms.js:9:31:9:36 | values |
@@ -207,6 +217,14 @@ edges
| xss-through-dom.js:114:11:114:52 | src | xss-through-dom.js:117:26:117:28 | src |
| xss-through-dom.js:114:17:114:52 | documen ... k").src | xss-through-dom.js:114:11:114:52 | src |
| xss-through-dom.js:114:17:114:52 | documen ... k").src | xss-through-dom.js:114:11:114:52 | src |
| xss-through-dom.js:120:23:120:37 | ev.target.files | xss-through-dom.js:120:23:120:40 | ev.target.files[0] |
| xss-through-dom.js:120:23:120:37 | ev.target.files | xss-through-dom.js:120:23:120:40 | ev.target.files[0] |
| xss-through-dom.js:120:23:120:40 | ev.target.files[0] | xss-through-dom.js:120:23:120:45 | ev.targ ... 0].name |
| xss-through-dom.js:120:23:120:40 | ev.target.files[0] | xss-through-dom.js:120:23:120:45 | ev.targ ... 0].name |
| xss-through-dom.js:122:53:122:67 | ev.target.files | xss-through-dom.js:122:53:122:70 | ev.target.files[0] |
| xss-through-dom.js:122:53:122:67 | ev.target.files | xss-through-dom.js:122:53:122:70 | ev.target.files[0] |
| xss-through-dom.js:122:53:122:70 | ev.target.files[0] | xss-through-dom.js:122:33:122:71 | URL.cre ... les[0]) |
| xss-through-dom.js:122:53:122:70 | ev.target.files[0] | xss-through-dom.js:122:33:122:71 | URL.cre ... les[0]) |
#select
| forms.js:9:31:9:40 | values.foo | forms.js:8:23:8:28 | values | forms.js:9:31:9:40 | values.foo | $@ is reinterpreted as HTML without escaping meta-characters. | forms.js:8:23:8:28 | values | DOM text |
| forms.js:12:31:12:40 | values.bar | forms.js:11:24:11:29 | values | forms.js:12:31:12:40 | values.bar | $@ is reinterpreted as HTML without escaping meta-characters. | forms.js:11:24:11:29 | values | DOM text |
@@ -242,3 +260,5 @@ edges
| xss-through-dom.js:96:17:96:47 | $("#foo ... ].value | xss-through-dom.js:96:17:96:47 | $("#foo ... ].value | xss-through-dom.js:96:17:96:47 | $("#foo ... ].value | $@ is reinterpreted as HTML without escaping meta-characters. | xss-through-dom.js:96:17:96:47 | $("#foo ... ].value | DOM text |
| xss-through-dom.js:109:31:109:70 | "<a src ... oo</a>" | xss-through-dom.js:109:45:109:55 | this.el.src | xss-through-dom.js:109:31:109:70 | "<a src ... oo</a>" | $@ is reinterpreted as HTML without escaping meta-characters. | xss-through-dom.js:109:45:109:55 | this.el.src | DOM text |
| xss-through-dom.js:115:16:115:18 | src | xss-through-dom.js:114:17:114:52 | documen ... k").src | xss-through-dom.js:115:16:115:18 | src | $@ is reinterpreted as HTML without escaping meta-characters. | xss-through-dom.js:114:17:114:52 | documen ... k").src | DOM text |
| xss-through-dom.js:120:23:120:45 | ev.targ ... 0].name | xss-through-dom.js:120:23:120:37 | ev.target.files | xss-through-dom.js:120:23:120:45 | ev.targ ... 0].name | $@ is reinterpreted as HTML without escaping meta-characters. | xss-through-dom.js:120:23:120:37 | ev.target.files | DOM text |
| xss-through-dom.js:122:33:122:71 | URL.cre ... les[0]) | xss-through-dom.js:122:53:122:67 | ev.target.files | xss-through-dom.js:122:33:122:71 | URL.cre ... les[0]) | $@ is reinterpreted as HTML without escaping meta-characters. | xss-through-dom.js:122:53:122:67 | ev.target.files | DOM text |

View File

@@ -115,4 +115,10 @@ class Sub extends Super {
$("#id").html(src); // NOT OK.
$("#id").attr("src", src); // OK
$("input.foo")[0].onchange = function (ev) {
$("#id").html(ev.target.files[0].name); // NOT OK.
$("img#id").attr("src", URL.createObjectURL(ev.target.files[0])); // NOT OK
}
})();

View File

@@ -0,0 +1,558 @@
/**
* INTERNAL: Do not use.
*
* Points-to based call-graph.
*/
private import python
private import DataFlowPublic
private import semmle.python.SpecialMethods
/** A parameter position represented by an integer. */
class ParameterPosition extends int {
ParameterPosition() { exists(any(DataFlowCallable c).getParameter(this)) }
}
/** An argument position represented by an integer. */
class ArgumentPosition extends int {
ArgumentPosition() { exists(any(DataFlowCall c).getArg(this)) }
}
/** Holds if arguments at position `apos` match parameters at position `ppos`. */
pragma[inline]
predicate parameterMatch(ParameterPosition ppos, ArgumentPosition apos) { ppos = apos }
/**
* Computes routing of arguments to parameters
*
* When a call contains more positional arguments than there are positional parameters,
* the extra positional arguments are passed as a tuple to a starred parameter. This is
* achieved by synthesizing a node `TPosOverflowNode(call, callable)`
* that represents the tuple of extra positional arguments. There is a store step from each
* extra positional argument to this node.
*
* CURRENTLY NOT SUPPORTED:
* When a call contains an iterable unpacking argument, such as `func(*args)`, it is expanded into positional arguments.
*
* CURRENTLY NOT SUPPORTED:
* If a call contains an iterable unpacking argument, such as `func(*args)`, and the callee contains a starred argument, any extra
* positional arguments are passed to the starred argument.
*
* When a call contains keyword arguments that do not correspond to keyword parameters, these
* extra keyword arguments are passed as a dictionary to a doubly starred parameter. This is
* achieved by synthesizing a node `TKwOverflowNode(call, callable)`
* that represents the dictionary of extra keyword arguments. There is a store step from each
* extra keyword argument to this node.
*
* When a call contains a dictionary unpacking argument, such as `func(**kwargs)`, with entries corresponding to a keyword parameter,
* the value at such a key is unpacked and passed to the parameter. This is achieved
* by synthesizing an argument node `TKwUnpacked(call, callable, name)` representing the unpacked
* value. This node is used as the argument passed to the matching keyword parameter. There is a read
* step from the dictionary argument to the synthesized argument node.
*
* When a call contains a dictionary unpacking argument, such as `func(**kwargs)`, and the callee contains a doubly starred parameter,
* entries which are not unpacked are passed to the doubly starred parameter. This is achieved by
* adding a dataflow step from the dictionary argument to `TKwOverflowNode(call, callable)` and a
* step to clear content of that node at any unpacked keys.
*
* ## Examples:
* Assume that we have the callable
* ```python
* def f(x, y, *t, **d):
* pass
* ```
* Then the call
* ```python
* f(0, 1, 2, a=3)
* ```
* will be modeled as
* ```python
* f(0, 1, [*t], [**d])
* ```
* where `[` and `]` denotes synthesized nodes, so `[*t]` is the synthesized tuple argument
* `TPosOverflowNode` and `[**d]` is the synthesized dictionary argument `TKwOverflowNode`.
* There will be a store step from `2` to `[*t]` at pos `0` and one from `3` to `[**d]` at key
* `a`.
*
* For the call
* ```python
* f(0, **{"y": 1, "a": 3})
* ```
* no tuple argument is synthesized. It is modeled as
* ```python
* f(0, [y=1], [**d])
* ```
* where `[y=1]` is the synthesized unpacked argument `TKwUnpacked` (with `name` = `y`). There is
* a read step from `**{"y": 1, "a": 3}` to `[y=1]` at key `y` to get the value passed to the parameter
* `y`. There is a dataflow step from `**{"y": 1, "a": 3}` to `[**d]` to transfer the content and
* a clearing of content at key `y` for node `[**d]`, since that value has been unpacked.
*/
module ArgumentPassing {
/**
* Holds if `call` represents a `DataFlowCall` to a `DataFlowCallable` represented by `callable`.
*
* It _may not_ be the case that `call = callable.getACall()`, i.e. if `call` represents a `ClassCall`.
*
* Used to limit the size of predicates.
*/
predicate connects(CallNode call, CallableValue callable) {
exists(DataFlowCall c |
call = c.getNode() and
callable = c.getCallable().getCallableValue()
)
}
/**
* Gets the `n`th parameter of `callable`.
* If the callable has a starred parameter, say `*tuple`, that is matched with `n=-1`.
* If the callable has a doubly starred parameter, say `**dict`, that is matched with `n=-2`.
* Note that, unlike other languages, we do _not_ use -1 for the position of `self` in Python,
* as it is an explicit parameter at position 0.
*/
NameNode getParameter(CallableValue callable, int n) {
// positional parameter
result = callable.getParameter(n)
or
// starred parameter, `*tuple`
exists(Function f |
f = callable.getScope() and
n = -1 and
result = f.getVararg().getAFlowNode()
)
or
// doubly starred parameter, `**dict`
exists(Function f |
f = callable.getScope() and
n = -2 and
result = f.getKwarg().getAFlowNode()
)
}
/**
* A type representing a mapping from argument indices to parameter indices.
* We currently use two mappings: NoShift, the identity, used for ordinary
* function calls, and ShiftOneUp which is used for calls where an extra argument
* is inserted. These include method calls, constructor calls and class calls.
* In these calls, the argument at index `n` is mapped to the parameter at position `n+1`.
*/
newtype TArgParamMapping =
TNoShift() or
TShiftOneUp()
/** A mapping used for parameter passing. */
abstract class ArgParamMapping extends TArgParamMapping {
/** Gets the index of the parameter that corresponds to the argument at index `argN`. */
bindingset[argN]
abstract int getParamN(int argN);
/** Gets a textual representation of this element. */
abstract string toString();
}
/** A mapping that passes argument `n` to parameter `n`. */
class NoShift extends ArgParamMapping, TNoShift {
NoShift() { this = TNoShift() }
override string toString() { result = "NoShift [n -> n]" }
bindingset[argN]
override int getParamN(int argN) { result = argN }
}
/** A mapping that passes argument `n` to parameter `n+1`. */
class ShiftOneUp extends ArgParamMapping, TShiftOneUp {
ShiftOneUp() { this = TShiftOneUp() }
override string toString() { result = "ShiftOneUp [n -> n+1]" }
bindingset[argN]
override int getParamN(int argN) { result = argN + 1 }
}
/**
* Gets the node representing the argument to `call` that is passed to the parameter at
* (zero-based) index `paramN` in `callable`. If this is a positional argument, it must appear
* at an index, `argN`, in `call` wich satisfies `paramN = mapping.getParamN(argN)`.
*
* `mapping` will be the identity for function calls, but not for method- or constructor calls,
* where the first parameter is `self` and the first positional argument is passed to the second positional parameter.
* Similarly for classmethod calls, where the first parameter is `cls`.
*
* NOT SUPPORTED: Keyword-only parameters.
*/
Node getArg(CallNode call, ArgParamMapping mapping, CallableValue callable, int paramN) {
connects(call, callable) and
(
// positional argument
exists(int argN |
paramN = mapping.getParamN(argN) and
result = TCfgNode(call.getArg(argN))
)
or
// keyword argument
// TODO: Since `getArgName` have no results for keyword-only parameters,
// these are currently not supported.
exists(Function f, string argName |
f = callable.getScope() and
f.getArgName(paramN) = argName and
result = TCfgNode(call.getArgByName(unbind_string(argName)))
)
or
// a synthezised argument passed to the starred parameter (at position -1)
callable.getScope().hasVarArg() and
paramN = -1 and
result = TPosOverflowNode(call, callable)
or
// a synthezised argument passed to the doubly starred parameter (at position -2)
callable.getScope().hasKwArg() and
paramN = -2 and
result = TKwOverflowNode(call, callable)
or
// argument unpacked from dict
exists(string name |
call_unpacks(call, mapping, callable, name, paramN) and
result = TKwUnpackedNode(call, callable, name)
)
)
}
/** Currently required in `getArg` in order to prevent a bad join. */
bindingset[result, s]
private string unbind_string(string s) { result <= s and s <= result }
/** Gets the control flow node that is passed as the `n`th overflow positional argument. */
ControlFlowNode getPositionalOverflowArg(CallNode call, CallableValue callable, int n) {
connects(call, callable) and
exists(Function f, int posCount, int argNr |
f = callable.getScope() and
f.hasVarArg() and
posCount = f.getPositionalParameterCount() and
result = call.getArg(argNr) and
argNr >= posCount and
argNr = posCount + n
)
}
/** Gets the control flow node that is passed as the overflow keyword argument with key `key`. */
ControlFlowNode getKeywordOverflowArg(CallNode call, CallableValue callable, string key) {
connects(call, callable) and
exists(Function f |
f = callable.getScope() and
f.hasKwArg() and
not exists(f.getArgByName(key)) and
result = call.getArgByName(key)
)
}
/**
* Holds if `call` unpacks a dictionary argument in order to pass it via `name`.
* It will then be passed to the parameter of `callable` at index `paramN`.
*/
predicate call_unpacks(
CallNode call, ArgParamMapping mapping, CallableValue callable, string name, int paramN
) {
connects(call, callable) and
exists(Function f |
f = callable.getScope() and
not exists(int argN | paramN = mapping.getParamN(argN) | exists(call.getArg(argN))) and // no positional argument available
name = f.getArgName(paramN) and
// not exists(call.getArgByName(name)) and // only matches keyword arguments not preceded by **
// TODO: make the below logic respect control flow splitting (by not going to the AST).
not call.getNode().getANamedArg().(Keyword).getArg() = name and // no keyword argument available
paramN >= 0 and
paramN < f.getPositionalParameterCount() + f.getKeywordOnlyParameterCount() and
exists(call.getNode().getKwargs()) // dict argument available
)
}
}
import ArgumentPassing
/**
* IPA type for DataFlowCallable.
*
* A callable is either a function value, a class value, or a module (for enclosing `ModuleVariableNode`s).
* A module has no calls.
*/
newtype TDataFlowCallable =
TCallableValue(CallableValue callable) {
callable instanceof FunctionValue and
not callable.(FunctionValue).isLambda()
or
callable instanceof ClassValue
} or
TLambda(Function lambda) { lambda.isLambda() } or
TModule(Module m)
/** A callable. */
abstract class DataFlowCallable extends TDataFlowCallable {
/** Gets a textual representation of this element. */
abstract string toString();
/** Gets a call to this callable. */
abstract CallNode getACall();
/** Gets the scope of this callable */
abstract Scope getScope();
/** Gets the specified parameter of this callable */
abstract NameNode getParameter(int n);
/** Gets the name of this callable. */
abstract string getName();
/** Gets a callable value for this callable, if one exists. */
abstract CallableValue getCallableValue();
}
/** A class representing a callable value. */
class DataFlowCallableValue extends DataFlowCallable, TCallableValue {
CallableValue callable;
DataFlowCallableValue() { this = TCallableValue(callable) }
override string toString() { result = callable.toString() }
override CallNode getACall() { result = callable.getACall() }
override Scope getScope() { result = callable.getScope() }
override NameNode getParameter(int n) { result = getParameter(callable, n) }
override string getName() { result = callable.getName() }
override CallableValue getCallableValue() { result = callable }
}
/** A class representing a callable lambda. */
class DataFlowLambda extends DataFlowCallable, TLambda {
Function lambda;
DataFlowLambda() { this = TLambda(lambda) }
override string toString() { result = lambda.toString() }
override CallNode getACall() { result = this.getCallableValue().getACall() }
override Scope getScope() { result = lambda.getEvaluatingScope() }
override NameNode getParameter(int n) { result = getParameter(this.getCallableValue(), n) }
override string getName() { result = "Lambda callable" }
override FunctionValue getCallableValue() {
result.getOrigin().getNode() = lambda.getDefinition()
}
}
/** A class representing the scope in which a `ModuleVariableNode` appears. */
class DataFlowModuleScope extends DataFlowCallable, TModule {
Module mod;
DataFlowModuleScope() { this = TModule(mod) }
override string toString() { result = mod.toString() }
override CallNode getACall() { none() }
override Scope getScope() { result = mod }
override NameNode getParameter(int n) { none() }
override string getName() { result = mod.getName() }
override CallableValue getCallableValue() { none() }
}
/**
* IPA type for DataFlowCall.
*
* Calls corresponding to `CallNode`s are either to callable values or to classes.
* The latter is directed to the callable corresponding to the `__init__` method of the class.
*
* An `__init__` method can also be called directly, so that the callable can be targeted by
* different types of calls. In that case, the parameter mappings will be different,
* as the class call will synthesize an argument node to be mapped to the `self` parameter.
*
* A call corresponding to a special method call is handled by the corresponding `SpecialMethodCallNode`.
*
* TODO: Add `TClassMethodCall` mapping `cls` appropriately.
*/
newtype TDataFlowCall =
TFunctionCall(CallNode call) { call = any(FunctionValue f).getAFunctionCall() } or
/** Bound methods need to make room for the explicit self parameter */
TMethodCall(CallNode call) { call = any(FunctionValue f).getAMethodCall() } or
TClassCall(CallNode call) { call = any(ClassValue c | not c.isAbsent()).getACall() } or
TSpecialCall(SpecialMethodCallNode special)
/** A call. */
abstract class DataFlowCall extends TDataFlowCall {
/** Gets a textual representation of this element. */
abstract string toString();
/** Get the callable to which this call goes. */
abstract DataFlowCallable getCallable();
/**
* Gets the argument to this call that will be sent
* to the `n`th parameter of the callable.
*/
abstract Node getArg(int n);
/** Get the control flow node representing this call. */
abstract ControlFlowNode getNode();
/** Gets the enclosing callable of this call. */
abstract DataFlowCallable getEnclosingCallable();
/** Gets the location of this dataflow call. */
Location getLocation() { result = this.getNode().getLocation() }
}
/**
* A call to a function/lambda.
* This excludes calls to bound methods, classes, and special methods.
* Bound method calls and class calls insert an argument for the explicit
* `self` parameter, and special method calls have special argument passing.
*/
class FunctionCall extends DataFlowCall, TFunctionCall {
CallNode call;
DataFlowCallable callable;
FunctionCall() {
this = TFunctionCall(call) and
call = callable.getACall()
}
override string toString() { result = call.toString() }
override Node getArg(int n) { result = getArg(call, TNoShift(), callable.getCallableValue(), n) }
override ControlFlowNode getNode() { result = call }
override DataFlowCallable getCallable() { result = callable }
override DataFlowCallable getEnclosingCallable() { result.getScope() = call.getNode().getScope() }
}
/**
* Represents a call to a bound method call.
* The node representing the instance is inserted as argument to the `self` parameter.
*/
class MethodCall extends DataFlowCall, TMethodCall {
CallNode call;
FunctionValue bm;
MethodCall() {
this = TMethodCall(call) and
call = bm.getACall()
}
private CallableValue getCallableValue() { result = bm }
override string toString() { result = call.toString() }
override Node getArg(int n) {
n > 0 and result = getArg(call, TShiftOneUp(), this.getCallableValue(), n)
or
n = 0 and result = TCfgNode(call.getFunction().(AttrNode).getObject())
}
override ControlFlowNode getNode() { result = call }
override DataFlowCallable getCallable() { result = TCallableValue(this.getCallableValue()) }
override DataFlowCallable getEnclosingCallable() { result.getScope() = call.getScope() }
}
/**
* Represents a call to a class.
* The pre-update node for the call is inserted as argument to the `self` parameter.
* That makes the call node be the post-update node holding the value of the object
* after the constructor has run.
*/
class ClassCall extends DataFlowCall, TClassCall {
CallNode call;
ClassValue c;
ClassCall() {
this = TClassCall(call) and
call = c.getACall()
}
private CallableValue getCallableValue() { c.getScope().getInitMethod() = result.getScope() }
override string toString() { result = call.toString() }
override Node getArg(int n) {
n > 0 and result = getArg(call, TShiftOneUp(), this.getCallableValue(), n)
or
n = 0 and result = TSyntheticPreUpdateNode(TCfgNode(call))
}
override ControlFlowNode getNode() { result = call }
override DataFlowCallable getCallable() { result = TCallableValue(this.getCallableValue()) }
override DataFlowCallable getEnclosingCallable() { result.getScope() = call.getScope() }
}
/** A call to a special method. */
class SpecialCall extends DataFlowCall, TSpecialCall {
SpecialMethodCallNode special;
SpecialCall() { this = TSpecialCall(special) }
override string toString() { result = special.toString() }
override Node getArg(int n) { result = TCfgNode(special.(SpecialMethod::Potential).getArg(n)) }
override ControlFlowNode getNode() { result = special }
override DataFlowCallable getCallable() {
result = TCallableValue(special.getResolvedSpecialMethod())
}
override DataFlowCallable getEnclosingCallable() {
result.getScope() = special.getNode().getScope()
}
}
/** Gets a viable run-time target for the call `call`. */
DataFlowCallable viableCallable(DataFlowCall call) { result = call.getCallable() }
private newtype TReturnKind = TNormalReturnKind()
/**
* A return kind. A return kind describes how a value can be returned
* from a callable. For Python, this is simply a method return.
*/
class ReturnKind extends TReturnKind {
/** Gets a textual representation of this element. */
string toString() { result = "return" }
}
/** A data flow node that represents a value returned by a callable. */
class ReturnNode extends CfgNode {
Return ret;
// See `TaintTrackingImplementation::returnFlowStep`
ReturnNode() { node = ret.getValue().getAFlowNode() }
/** Gets the kind of this return node. */
ReturnKind getKind() { any() }
}
/** A data flow node that represents the output of a call. */
class OutNode extends CfgNode {
OutNode() { node instanceof CallNode }
}
/**
* Gets a node that can read the value returned from `call` with return kind
* `kind`.
*/
OutNode getAnOutNode(DataFlowCall call, ReturnKind kind) {
call.getNode() = result.getNode() and
kind = TNormalReturnKind()
}

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -0,0 +1,396 @@
/**
* The unpacking assignment takes the general form
* ```python
* sequence = iterable
* ```
* where `sequence` is either a tuple or a list and it can contain wildcards.
* The iterable can be any iterable, which means that (CodeQL modeling of) content
* will need to change type if it should be transferred from the LHS to the RHS.
*
* Note that (CodeQL modeling of) content does not have to change type on data-flow
* paths _inside_ the LHS, as the different allowed syntaxes here are merely a convenience.
* Consequently, we model all LHS sequences as tuples, which have the more precise content
* model, making flow to the elements more precise. If an element is a starred variable,
* we will have to mutate the content type to be list content.
*
* We may for instance have
* ```python
* (a, b) = ["a", SOURCE] # RHS has content `ListElementContent`
* ```
* Due to the abstraction for list content, we do not know whether `SOURCE`
* ends up in `a` or in `b`, so we want to overapproximate and see it in both.
*
* Using wildcards we may have
* ```python
* (a, *b) = ("a", "b", SOURCE) # RHS has content `TupleElementContent(2)`
* ```
* Since the starred variables are always assigned (Python-)type list, `*b` will be
* `["b", SOURCE]`, and we will again overapproximate and assign it
* content corresponding to anything found in the RHS.
*
* For a precise transfer
* ```python
* (a, b) = ("a", SOURCE) # RHS has content `TupleElementContent(1)`
* ```
* we wish to keep the precision, so only `b` receives the tuple content at index 1.
*
* Finally, `sequence` is actually a pattern and can have a more complicated structure,
* such as
* ```python
* (a, [b, *c]) = ("a", ["b", SOURCE]) # RHS has content `TupleElementContent(1); ListElementContent`
* ```
* where `a` should not receive content, but `b` and `c` should. `c` will be `[SOURCE]` so
* should have the content transferred, while `b` should read it.
*
* To transfer content from RHS to the elements of the LHS in the expression `sequence = iterable`,
* we use two synthetic nodes:
*
* - `TIterableSequence(sequence)` which captures the content-modeling the entire `sequence` will have
* (essentially just a copy of the content-modeling the RHS has)
*
* - `TIterableElement(sequence)` which captures the content-modeling that will be assigned to an element.
* Note that an empty access path means that the value we are tracking flows directly to the element.
*
*
* The `TIterableSequence(sequence)` is at this point superflous but becomes useful when handling recursive
* structures in the LHS, where `sequence` is some internal sequence node. We can have a uniform treatment
* by always having these two synthetic nodes. So we transfer to (or, in the recursive case, read into)
* `TIterableSequence(sequence)`, from which we take a read step to `TIterableElement(sequence)` and then a
* store step to `sequence`.
*
* This allows the unknown content from the RHS to be read into `TIterableElement(sequence)` and tuple content
* to then be stored into `sequence`. If the content is already tuple content, this inderection creates crosstalk
* between indices. Therefore, tuple content is never read into `TIterableElement(sequence)`; it is instead
* transferred directly from `TIterableSequence(sequence)` to `sequence` via a flow step. Such a flow step will
* also transfer other content, but only tuple content is further read from `sequence` into its elements.
*
* The strategy is then via several read-, store-, and flow steps:
* 1. a) [Flow] Content is transferred from `iterable` to `TIterableSequence(sequence)` via a
* flow step. From here, everything happens on the LHS.
*
* b) [Read] If the unpacking happens inside a for as in
* ```python
* for sequence in iterable
* ```
* then content is read from `iterable` to `TIterableSequence(sequence)`.
*
* 2. [Flow] Content is transferred from `TIterableSequence(sequence)` to `sequence` via a
* flow step. (Here only tuple content is relevant.)
*
* 3. [Read] Content is read from `TIterableSequence(sequence)` into `TIterableElement(sequence)`.
* As `sequence` is modeled as a tuple, we will not read tuple content as that would allow
* crosstalk.
*
* 4. [Store] Content is stored from `TIterableElement(sequence)` to `sequence`.
* Content type is `TupleElementContent` with indices taken from the syntax.
* For instance, if `sequence` is `(a, *b, c)`, content is written to index 0, 1, and 2.
* This is adequate as the route through `TIterableElement(sequence)` does not transfer precise content.
*
* 5. [Read] Content is read from `sequence` to its elements.
* a) If the element is a plain variable, the target is the corresponding essa node.
*
* b) If the element is itself a sequence, with control-flow node `seq`, the target is `TIterableSequence(seq)`.
*
* c) If the element is a starred variable, with control-flow node `v`, the target is `TIterableElement(v)`.
*
* 6. [Store] Content is stored from `TIterableElement(v)` to the essa variable for `v`, with
* content type `ListElementContent`.
*
* 7. [Flow, Read, Store] Steps 2 through 7 are repeated for all recursive elements which are sequences.
*
*
* We illustrate the above steps on the assignment
*
* ```python
* (a, b) = ["a", SOURCE]
* ```
*
* Looking at the content propagation to `a`:
* `["a", SOURCE]`: [ListElementContent]
*
* --Step 1a-->
*
* `TIterableSequence((a, b))`: [ListElementContent]
*
* --Step 3-->
*
* `TIterableElement((a, b))`: []
*
* --Step 4-->
*
* `(a, b)`: [TupleElementContent(0)]
*
* --Step 5a-->
*
* `a`: []
*
* Meaning there is data-flow from the RHS to `a` (an over approximation). The same logic would be applied to show there is data-flow to `b`. Note that _Step 3_ and _Step 4_ would not have been needed if the RHS had been a tuple (since that would have been able to use _Step 2_ instead).
*
* Another, more complicated example:
* ```python
* (a, [b, *c]) = ["a", [SOURCE]]
* ```
* where the path to `c` is
*
* `["a", [SOURCE]]`: [ListElementContent; ListElementContent]
*
* --Step 1a-->
*
* `TIterableSequence((a, [b, *c]))`: [ListElementContent; ListElementContent]
*
* --Step 3-->
*
* `TIterableElement((a, [b, *c]))`: [ListElementContent]
*
* --Step 4-->
*
* `(a, [b, *c])`: [TupleElementContent(1); ListElementContent]
*
* --Step 5b-->
*
* `TIterableSequence([b, *c])`: [ListElementContent]
*
* --Step 3-->
*
* `TIterableElement([b, *c])`: []
*
* --Step 4-->
*
* `[b, *c]`: [TupleElementContent(1)]
*
* --Step 5c-->
*
* `TIterableElement(c)`: []
*
* --Step 6-->
*
* `c`: [ListElementContent]
*/
private import python
private import DataFlowPublic
/**
* The target of a `for`, e.g. `x` in `for x in list` or in `[42 for x in list]`.
* This class also records the source, which in both above cases is `list`.
* This class abstracts away the differing representations of comprehensions and
* for statements.
*/
class ForTarget extends ControlFlowNode {
Expr source;
ForTarget() {
exists(For for |
source = for.getIter() and
this.getNode() = for.getTarget() and
not for = any(Comp comp).getNthInnerLoop(0)
)
or
exists(Comp comp |
source = comp.getIterable() and
this.getNode() = comp.getNthInnerLoop(0).getTarget()
)
}
Expr getSource() { result = source }
}
/** The LHS of an assignment, it also records the assigned value. */
class AssignmentTarget extends ControlFlowNode {
Expr value;
AssignmentTarget() {
exists(Assign assign | this.getNode() = assign.getATarget() | value = assign.getValue())
}
Expr getValue() { result = value }
}
/** A direct (or top-level) target of an unpacking assignment. */
class UnpackingAssignmentDirectTarget extends ControlFlowNode {
Expr value;
UnpackingAssignmentDirectTarget() {
this instanceof SequenceNode and
(
value = this.(AssignmentTarget).getValue()
or
value = this.(ForTarget).getSource()
)
}
Expr getValue() { result = value }
}
/** A (possibly recursive) target of an unpacking assignment. */
class UnpackingAssignmentTarget extends ControlFlowNode {
UnpackingAssignmentTarget() {
this instanceof UnpackingAssignmentDirectTarget
or
this = any(UnpackingAssignmentSequenceTarget parent).getAnElement()
}
}
/** A (possibly recursive) target of an unpacking assignment which is also a sequence. */
class UnpackingAssignmentSequenceTarget extends UnpackingAssignmentTarget instanceof SequenceNode {
ControlFlowNode getElement(int i) { result = super.getElement(i) }
ControlFlowNode getAnElement() { result = this.getElement(_) }
}
/**
* Step 1a
* Data flows from `iterable` to `TIterableSequence(sequence)`
*/
predicate iterableUnpackingAssignmentFlowStep(Node nodeFrom, Node nodeTo) {
exists(AssignmentTarget target |
nodeFrom.asExpr() = target.getValue() and
nodeTo = TIterableSequenceNode(target)
)
}
/**
* Step 1b
* Data is read from `iterable` to `TIterableSequence(sequence)`
*/
predicate iterableUnpackingForReadStep(CfgNode nodeFrom, Content c, Node nodeTo) {
exists(ForTarget target |
nodeFrom.asExpr() = target.getSource() and
target instanceof SequenceNode and
nodeTo = TIterableSequenceNode(target)
) and
(
c instanceof ListElementContent
or
c instanceof SetElementContent
)
}
/**
* Step 2
* Data flows from `TIterableSequence(sequence)` to `sequence`
*/
predicate iterableUnpackingTupleFlowStep(Node nodeFrom, Node nodeTo) {
exists(UnpackingAssignmentSequenceTarget target |
nodeFrom = TIterableSequenceNode(target) and
nodeTo.asCfgNode() = target
)
}
/**
* Step 3
* Data flows from `TIterableSequence(sequence)` into `TIterableElement(sequence)`.
* As `sequence` is modeled as a tuple, we will not read tuple content as that would allow
* crosstalk.
*/
predicate iterableUnpackingConvertingReadStep(Node nodeFrom, Content c, Node nodeTo) {
exists(UnpackingAssignmentSequenceTarget target |
nodeFrom = TIterableSequenceNode(target) and
nodeTo = TIterableElementNode(target) and
(
c instanceof ListElementContent
or
c instanceof SetElementContent
// TODO: dict content in iterable unpacking not handled
)
)
}
/**
* Step 4
* Data flows from `TIterableElement(sequence)` to `sequence`.
* Content type is `TupleElementContent` with indices taken from the syntax.
* For instance, if `sequence` is `(a, *b, c)`, content is written to index 0, 1, and 2.
*/
predicate iterableUnpackingConvertingStoreStep(Node nodeFrom, Content c, Node nodeTo) {
exists(UnpackingAssignmentSequenceTarget target |
nodeFrom = TIterableElementNode(target) and
nodeTo.asCfgNode() = target and
exists(int index | exists(target.getElement(index)) |
c.(TupleElementContent).getIndex() = index
)
)
}
/**
* Step 5
* For a sequence node inside an iterable unpacking, data flows from the sequence to its elements. There are
* three cases for what `toNode` should be:
* a) If the element is a plain variable, `toNode` is the corresponding essa node.
*
* b) If the element is itself a sequence, with control-flow node `seq`, `toNode` is `TIterableSequence(seq)`.
*
* c) If the element is a starred variable, with control-flow node `v`, `toNode` is `TIterableElement(v)`.
*/
predicate iterableUnpackingElementReadStep(Node nodeFrom, Content c, Node nodeTo) {
exists(
UnpackingAssignmentSequenceTarget target, int index, ControlFlowNode element, int starIndex
|
target.getElement(starIndex) instanceof StarredNode
or
not exists(target.getAnElement().(StarredNode)) and
starIndex = -1
|
nodeFrom.asCfgNode() = target and
element = target.getElement(index) and
(
if starIndex = -1 or index < starIndex
then c.(TupleElementContent).getIndex() = index
else
// This could get big if big tuples exist
if index = starIndex
then c.(TupleElementContent).getIndex() >= index
else c.(TupleElementContent).getIndex() >= index - 1
) and
(
if element instanceof SequenceNode
then
// Step 5b
nodeTo = TIterableSequenceNode(element)
else
if element instanceof StarredNode
then
// Step 5c
nodeTo = TIterableElementNode(element)
else
// Step 5a
nodeTo.asVar().getDefinition().(MultiAssignmentDefinition).getDefiningNode() = element
)
)
}
/**
* Step 6
* Data flows from `TIterableElement(v)` to the essa variable for `v`, with
* content type `ListElementContent`.
*/
predicate iterableUnpackingStarredElementStoreStep(Node nodeFrom, Content c, Node nodeTo) {
exists(ControlFlowNode starred | starred.getNode() instanceof Starred |
nodeFrom = TIterableElementNode(starred) and
nodeTo.asVar().getDefinition().(MultiAssignmentDefinition).getDefiningNode() = starred and
c instanceof ListElementContent
)
}
/** All read steps associated with unpacking assignment. */
predicate iterableUnpackingReadStep(Node nodeFrom, Content c, Node nodeTo) {
iterableUnpackingForReadStep(nodeFrom, c, nodeTo)
or
iterableUnpackingElementReadStep(nodeFrom, c, nodeTo)
or
iterableUnpackingConvertingReadStep(nodeFrom, c, nodeTo)
}
/** All store steps associated with unpacking assignment. */
predicate iterableUnpackingStoreStep(Node nodeFrom, Content c, Node nodeTo) {
iterableUnpackingStarredElementStoreStep(nodeFrom, c, nodeTo)
or
iterableUnpackingConvertingStoreStep(nodeFrom, c, nodeTo)
}
/** All flow steps associated with unpacking assignment. */
predicate iterableUnpackingFlowStep(Node nodeFrom, Node nodeTo) {
iterableUnpackingAssignmentFlowStep(nodeFrom, nodeTo)
or
iterableUnpackingTupleFlowStep(nodeFrom, nodeTo)
}

View File

@@ -0,0 +1,311 @@
/**
* There are a number of patterns available for the match statement.
* Each one transfers data and content differently to its parts.
*
* Furthermore, given a successful match, we can infer some data about
* the subject. Consider the example:
* ```python
* match choice:
* case 'Y':
* ...body
* ```
* Inside `body`, we know that `choice` has the value `'Y'`.
*
* A similar thing happens with the "as pattern". Consider the example:
* ```python
* match choice:
* case ('y'|'Y') as c:
* ...body
* ```
* By the binding rules, there is data flow from `choice` to `c`. But we
* can infer the value of `c` to be either `'y'` or `'Y'` if the match succeeds.
*
* We will treat such inferences separately as guards. First we will model the data flow
* stemming from the bindings and the matching of shape. Below, 'subject' is not necessarily the
* top-level subject of the match, but rather the part recursively matched by the current pattern.
* For instance, in the example:
* ```python
* match command:
* case ('quit' as c) | ('go', ('up'|'down') as c):
* ...body
* ```
* `command` is the subject of first the as-pattern, while the second component of `command`
* is the subject of the second as-pattern. As such, 'subject' refers to the pattern under evaluation.
*
* - as pattern: subject flows to alias as well as to the interior pattern
* - or pattern: subject flows to each alternative
* - literal pattern: flow from the literal to the pattern, to add information
* - capture pattern: subject flows to the variable
* - wildcard pattern: no flow
* - value pattern: flow from the value to the pattern, to add information
* - sequence pattern: each element reads from subject at the associated index
* - star pattern: subject flows to the variable, possibly via a conversion
* - mapping pattern: each value reads from subject at the associated key
* - double star pattern: subject flows to the variable, possibly via a conversion
* - key-value pattern: the value reads from the subject at the key (see mapping pattern)
* - class pattern: all keywords read the appropriate attribute from the subject
* - keyword pattern: the appropriate attribute is read from the subject (see class pattern)
*
* Inside the class pattern, we also find positional arguments. They are converted to
* keyword arguments using the `__match_args__` attribute on the class. We do not
* currently model this.
*/
private import python
private import DataFlowPublic
/**
* Holds when there is flow from the subject `nodeFrom` to the (top-level) pattern `nodeTo` of a `match` statement.
*
* The subject of a match flows to each top-level pattern
* (a pattern directly under a `case` statement).
*
* We could consider a model closer to use-use-flow, where the subject
* only flows to the first top-level pattern and from there to the
* following ones.
*/
predicate matchSubjectFlowStep(Node nodeFrom, Node nodeTo) {
exists(MatchStmt match, Expr subject, Pattern target |
subject = match.getSubject() and
target = match.getCase(_).(Case).getPattern()
|
nodeFrom.asExpr() = subject and
nodeTo.asCfgNode().getNode() = target
)
}
/**
* as pattern: subject flows to alias as well as to the interior pattern
* syntax (toplevel): `case pattern as alias:`
*/
predicate matchAsFlowStep(Node nodeFrom, Node nodeTo) {
exists(MatchAsPattern subject, Name alias | alias = subject.getAlias() |
// We make the subject flow to the interior pattern via the alias.
// That way, information can propagate from the interior pattern to the alias.
//
// the subject flows to the interior pattern
nodeFrom.asCfgNode().getNode() = subject and
nodeTo.asCfgNode().getNode() = subject.getPattern()
or
// the interior pattern flows to the alias
nodeFrom.asCfgNode().getNode() = subject.getPattern() and
nodeTo.asVar().getDefinition().(PatternAliasDefinition).getDefiningNode().getNode() = alias
)
}
/**
* or pattern: subject flows to each alternative
* syntax (toplevel): `case alt1 | alt2:`
*/
predicate matchOrFlowStep(Node nodeFrom, Node nodeTo) {
exists(MatchOrPattern subject, Pattern pattern | pattern = subject.getAPattern() |
nodeFrom.asCfgNode().getNode() = subject and
nodeTo.asCfgNode().getNode() = pattern
)
}
/**
* literal pattern: flow from the literal to the pattern, to add information
* syntax (toplevel): `case literal:`
*/
predicate matchLiteralFlowStep(Node nodeFrom, Node nodeTo) {
exists(MatchLiteralPattern pattern, Expr literal | literal = pattern.getLiteral() |
nodeFrom.asExpr() = literal and
nodeTo.asCfgNode().getNode() = pattern
)
}
/**
* capture pattern: subject flows to the variable
* syntax (toplevel): `case var:`
*/
predicate matchCaptureFlowStep(Node nodeFrom, Node nodeTo) {
exists(MatchCapturePattern capture, Name var | capture.getVariable() = var |
nodeFrom.asCfgNode().getNode() = capture and
nodeTo.asVar().getDefinition().(PatternCaptureDefinition).getDefiningNode().getNode() = var
)
}
/**
* value pattern: flow from the value to the pattern, to add information
* syntax (toplevel): `case Dotted.value:`
*/
predicate matchValueFlowStep(Node nodeFrom, Node nodeTo) {
exists(MatchValuePattern pattern, Expr value | value = pattern.getValue() |
nodeFrom.asExpr() = value and
nodeTo.asCfgNode().getNode() = pattern
)
}
/**
* sequence pattern: each element reads from subject at the associated index
* syntax (toplevel): `case [a, b]:`
*/
predicate matchSequenceReadStep(Node nodeFrom, Content c, Node nodeTo) {
exists(MatchSequencePattern subject, int index, Pattern element |
element = subject.getPattern(index)
|
nodeFrom.asCfgNode().getNode() = subject and
nodeTo.asCfgNode().getNode() = element and
(
// tuple content
c.(TupleElementContent).getIndex() = index
or
// list content
c instanceof ListElementContent
// set content is excluded from sequence patterns,
// see https://www.python.org/dev/peps/pep-0635/#sequence-patterns
)
)
}
/**
* star pattern: subject flows to the variable, possibly via a conversion
* syntax (toplevel): `case *var:`
*
* We decompose this flow into a read step and a store step. The read step
* reads both tuple and list content, the store step only stores list content.
* This way, we convert all content to list content.
*
* This is the read step.
*/
predicate matchStarReadStep(Node nodeFrom, Content c, Node nodeTo) {
exists(MatchSequencePattern subject, int index, MatchStarPattern star |
star = subject.getPattern(index)
|
nodeFrom.asCfgNode().getNode() = subject and
nodeTo = TStarPatternElementNode(star) and
(
// tuple content
c.(TupleElementContent).getIndex() >= index
or
// list content
c instanceof ListElementContent
// set content is excluded from sequence patterns,
// see https://www.python.org/dev/peps/pep-0635/#sequence-patterns
)
)
}
/**
* star pattern: subject flows to the variable, possibly via a conversion
* syntax (toplevel): `case *var:`
*
* We decompose this flow into a read step and a store step. The read step
* reads both tuple and list content, the store step only stores list content.
* This way, we convert all content to list content.
*
* This is the store step.
*/
predicate matchStarStoreStep(Node nodeFrom, Content c, Node nodeTo) {
exists(MatchStarPattern star |
nodeFrom = TStarPatternElementNode(star) and
nodeTo.asCfgNode().getNode() = star.getTarget() and
c instanceof ListElementContent
)
}
/**
* mapping pattern: each value reads from subject at the associated key
* syntax (toplevel): `case {"color": c, "height": x}:`
*/
predicate matchMappingReadStep(Node nodeFrom, Content c, Node nodeTo) {
exists(
MatchMappingPattern subject, MatchKeyValuePattern keyValue, MatchLiteralPattern key,
Pattern value
|
keyValue = subject.getAMapping() and
key = keyValue.getKey() and
value = keyValue.getValue()
|
nodeFrom.asCfgNode().getNode() = subject and
nodeTo.asCfgNode().getNode() = value and
c.(DictionaryElementContent).getKey() = key.getLiteral().(StrConst).getText()
)
}
/**
* double star pattern: subject flows to the variable, possibly via a conversion
* syntax (toplevel): `case {**var}:`
*
* Dictionary content flows to the double star, but all mentioned keys in the
* mapping pattern should be cleared.
*/
predicate matchMappingFlowStep(Node nodeFrom, Node nodeTo) {
exists(MatchMappingPattern subject, MatchDoubleStarPattern dstar | dstar = subject.getAMapping() |
nodeFrom.asCfgNode().getNode() = subject and
nodeTo.asCfgNode().getNode() = dstar.getTarget()
)
}
/**
* Bindings that are mentioned in a mapping pattern will not be available
* to a double star pattern in the same mapping pattern.
*/
predicate matchMappingClearStep(Node n, Content c) {
exists(
MatchMappingPattern subject, MatchKeyValuePattern keyValue, MatchLiteralPattern key,
MatchDoubleStarPattern dstar
|
keyValue = subject.getAMapping() and
key = keyValue.getKey() and
dstar = subject.getAMapping()
|
n.asCfgNode().getNode() = dstar.getTarget() and
c.(DictionaryElementContent).getKey() = key.getLiteral().(StrConst).getText()
)
}
/**
* class pattern: all keywords read the appropriate attribute from the subject
* syntax (toplevel): `case ClassName(attr = val):`
*/
predicate matchClassReadStep(Node nodeFrom, Content c, Node nodeTo) {
exists(MatchClassPattern subject, MatchKeywordPattern keyword, Name attr, Pattern value |
keyword = subject.getKeyword(_) and
attr = keyword.getAttribute() and
value = keyword.getValue()
|
nodeFrom.asCfgNode().getNode() = subject and
nodeTo.asCfgNode().getNode() = value and
c.(AttributeContent).getAttribute() = attr.getId()
)
}
/** All flow steps associated with match. */
predicate matchFlowStep(Node nodeFrom, Node nodeTo) {
matchSubjectFlowStep(nodeFrom, nodeTo)
or
matchAsFlowStep(nodeFrom, nodeTo)
or
matchOrFlowStep(nodeFrom, nodeTo)
or
matchLiteralFlowStep(nodeFrom, nodeTo)
or
matchCaptureFlowStep(nodeFrom, nodeTo)
or
matchValueFlowStep(nodeFrom, nodeTo)
or
matchMappingFlowStep(nodeFrom, nodeTo)
}
/** All read steps associated with match. */
predicate matchReadStep(Node nodeFrom, Content c, Node nodeTo) {
matchClassReadStep(nodeFrom, c, nodeTo)
or
matchSequenceReadStep(nodeFrom, c, nodeTo)
or
matchMappingReadStep(nodeFrom, c, nodeTo)
or
matchStarReadStep(nodeFrom, c, nodeTo)
}
/** All store steps associated with match. */
predicate matchStoreStep(Node nodeFrom, Content c, Node nodeTo) {
matchStarStoreStep(nodeFrom, c, nodeTo)
}
/**
* All clear steps associated with match
*/
predicate matchClearStep(Node n, Content c) { matchMappingClearStep(n, c) }

View File

@@ -1,138 +0,0 @@
# Using the shared dataflow library
## File organisation
The files currently live in `experimental` (whereas the existing implementation lives in `semmle\python\dataflow`).
In there is found `DataFlow.qll`, `DataFlow2.qll` etc. which refer to `internal\DataFlowImpl`, `internal\DataFlowImpl2` etc. respectively. The `DataFlowImplN`-files are all identical copies to avoid mutual recursion. They start off by including two files `internal\DataFlowImplCommon` and `internal\DataFlowImplSpecific`. The former contains all the language-agnostic definitions, while the latter is where we describe our favorite language. `Sepcific` simply forwards to two other files `internal\DataFlowPrivate.qll` and `internal\DataFlowPublic.qll`. Definitions in the former will be hidden behind a `private` modifier, while those in the latter can be referred to in data flow queries. For instance, the definition of `DataFlow::Node` should likely be in `DataFlowPublic.qll`.
## Define the dataflow graph
In order to use the dataflow library, we need to define the dataflow graph,
that is define the nodes and the edges.
### Define the nodes
The nodes are defined in the type `DataFlow::Node` (found in `DataFlowPublic.qll`).
This should likely be an IPA type, so we can extend it as needed.
Typical cases needed to construct the call graph include
- argument node
- parameter node
- return node
Typical extensions include
- postupdate nodes
- implicit `this`-nodes
### Define the edges
The edges split into local flow (within a function) and global flow (the call graph, between functions/procedures).
Extra flow, such as reading from and writing to global variables, can be captured in `jumpStep`.
The local flow should be obtainalble from an SSA computation.
Local flow nodes are generally either control flow nodes or SSA variables.
Flow from control flow nodes to SSA variables comes from SSA variable definitions, while flow from SSA variables to control flow nodes comes from def-use pairs.
The global flow should be obtainable from a `PointsTo` analysis. It is specified via `viableCallable` and
`getAnOutNode`. Consider making `ReturnKind` a singleton IPA type as in java.
Global flow includes local flow within a consistent call context. Thus, for local flow to count as global flow, all relevant nodes should implement `getEnclosingCallable`.
If complicated dispatch needs to be modelled, try using the `[reduced|pruned]viable*` predicates.
## Field flow
To track flow through fields we need to provide a model of fields, that is the `Content` class.
Field access is specified via `read_step` and `store_step`.
Work is being done to make field flow handle lists and dictionaries and the like.
`PostUpdateNode`s become important when field flow is used, as they track modifications to fields resulting from function calls.
## Type pruning
If type information is available, flows can be discarded on the grounds of type mismatch.
Tracked types are given by the class `DataFlowType` and the predicate `getTypeBound`, and compatibility is recorded in the predicate `compatibleTypes`.
If type pruning is not used, `compatibleTypes` should be implemented as `any`; if it is implemented, say, as `none`, all flows will be pruned.
Further, possible casts are given by the class `CastNode`.
---
# Plan
## Stage I, data flow
### Phase 0, setup
Define minimal IPA type for `DataFlow::Node`
Define all required predicates empty (via `none()`),
except `compatibleTypes` which should be `any()`.
Define `ReturnKind`, `DataFlowType`, and `Content` as singleton IPA types.
### Phase 1, local flow
Implement `simpleLocalFlowStep` based on the existing SSA computation
### Phase 2, local flow
Implement `viableCallable` and `getAnOutNode` based on the existing predicate `PointsTo`.
### Phase 3, field flow
Redefine `Content` and implement `read_step` and `store_step`.
Review use of post-update nodes.
### Phase 4, type pruning
Use type trackers to obtain relevant type information and redefine `DataFlowType` to contain appropriate cases. Record the type information in `getTypeBound`.
Implement `compatibleTypes` (perhaps simply as the identity).
If necessary, re-implement `getErasedRepr` and `ppReprType`.
If necessary, redefine `CastNode`.
### Phase 5, bonus
Review possible use of `[reduced|pruned]viable*` predicates.
Review need for more elaborate `ReturnKind`.
Review need for non-empty `jumpStep`.
Review need for non-empty `isUnreachableInCall`.
## Stage II, taint tracking
# Phase 0, setup
Implement all predicates empty.
# Phase 1, experiments
Try recovering an existing taint tracking query by implementing sources, sinks, sanitizers, and barriers.
---
# Status
## Achieved
- Copy of shared library; implemented enough predicates to make it compile.
- Simple flow into, out of, and through functions.
- Some tests, in particular a sceleton for something comprehensive.
## TODO
- Implementation has largely been done by finding a plausibly-sounding predicate in the python library to refer to. We should review that we actually have the intended semantics in all places.
- Comprehensive testing.
- The regression tests track the value of guards in order to eliminate impossible data flow. We currently have regressions because of this. We cannot readily replicate the existing method, as it uses the interdefinedness of data flow and taint tracking (there is a boolean taint kind). C++ [does something similar](https://github.com/github/codeql/blob/master/cpp/ql/src/semmle/code/cpp/controlflow/internal/ConstantExprs.qll#L27-L36) for eliminating impossible control flow, which we might be able to replicate (they infer values of "interesting" control flow nodes, which are those needed to determine values of guards).
- Flow for some syntactic constructs are done via extra taint steps in the existing implementation, we should find a way to get data flow for it. Some of this should be covered by field flow.
- A document is being written about proper use of the shared data flow library, this should be adhered to. In particular, we should consider replacing def-use with def-to-first-use and use-to-next-use in local flow.
- We seem to get duplicated results for global flow, as well as flow with and without type (so four times the "unique" results).
- We currently consider control flow nodes like exit nodes for functions, we should probably filter down which ones are of interest.
- We should probably override ToString for a number of data flow nodes.
- Test flow through classes, constructors and methods.
- What happens with named arguments? What does C# do?
- What should the enclosable callable for global variables be? C++ [makes it the variable itself](https://github.com/github/codeql/blob/master/cpp/ql/src/semmle/code/cpp/ir/dataflow/internal/DataFlowUtil.qll#L417), C# seems to not have nodes for these but only for their reads and writes.
- Is `yield` another return type? If not, how is it handled?
- Should `OutNode` include magic function calls?
- Consider creating an internal abstract class for nodes as C# does. Among other things, this can help the optimizer by stating that `getEnclosingCallable` [is functional](https://github.com/github/codeql/blob/master/csharp/ql/src/semmle/code/csharp/dataflow/internal/DataFlowPublic.qll#L62).

View File

@@ -79,6 +79,7 @@ class AstNode extends TAstNode {
/** Gets an annotation of this AST node. */
Annotation getAnAnnotation() {
not this instanceof Annotation and // avoid cyclic parent-child relationship
toQL(this).getParent() = pragma[only_bind_out](toQL(result)).getParent()
}
@@ -125,6 +126,9 @@ class TopLevel extends TTopLevel, AstNode {
/** Gets a `newtype` defined at the top-level of this module. */
NewType getANewType() { result = this.getAMember() }
/** Gets a `select` clause in the top-level of this module. */
Select getASelect() { result = this.getAMember() }
override ModuleMember getAChild(string pred) {
pred = directMember("getAnImport") and result = this.getAnImport()
or
@@ -137,6 +141,8 @@ class TopLevel extends TTopLevel, AstNode {
pred = directMember("getANewType") and result = this.getANewType()
or
pred = directMember("getQLDoc") and result = this.getQLDoc()
or
pred = directMember("getASelect") and result = this.getASelect()
}
QLDoc getQLDocFor(ModuleMember m) {
@@ -547,6 +553,9 @@ class VarDef extends TVarDef, AstNode {
Type getType() { none() }
/** Gets a variable access to this `VarDef` */
VarAccess getAnAccess() { result.getDeclaration() = this }
override string getAPrimaryQlClass() { result = "VarDef" }
override string toString() { result = this.getName() }

View File

@@ -0,0 +1,38 @@
private import codeql_ql.ast.Ast
pragma[inline]
private predicate isNumberedNode(AstNode node) {
// these are not nested in the location of the parent node, so we can't use their location to order them
not node instanceof Annotation and
not node instanceof QLDoc
}
private int getNodeDepth(AstNode node) {
node instanceof TopLevel and
result = 0
or
isNumberedNode(node) and
result = 1 + getNodeDepth(node.getParent())
}
/**
* Gets the pre-order ID for the given `node`, that is, its visit position
* in a pre-order traversal of all nodes.
*
* The children of a node are ordered left-to-right as they appear in the source code.
*
* The ID is globally unique for this AST node, also across files.
*
* At the moment this predicate is only defined for node which are:
* - reachable via `getAChild` edges from a `TopLevel`, and
* - is not a comment or annotation
*/
cached
int getPreOrderId(AstNode node) {
node =
rank[result](AstNode n, Location loc, int depth |
depth = getNodeDepth(n) and loc = n.getLocation()
|
n order by loc.getFile().getAbsolutePath(), loc.getStartLine(), loc.getStartColumn(), depth
)
}

View File

@@ -204,7 +204,7 @@ QL::AstNode toQL(AST::AstNode n) {
class TPredicate =
TCharPred or TClasslessPredicate or TClassPredicate or TDBRelation or TNewTypeBranch;
class TPredOrBuiltin = TPredicate or TNewTypeBranch or TBuiltin;
class TPredOrBuiltin = TPredicate or TBuiltin;
class TBuiltin = TBuiltinClassless or TBuiltinMember;

View File

@@ -0,0 +1,391 @@
/**
* Experimental library for reasoning about data flow.
*
* Current limitations:
* - Global flow does not reason about subclassing, overriding, and dispatch
* - `this`, `result`, and local field variables are treated less precisely
* than regular variables (see VarScoping.qll)
* - Polarity is not tracked, that is, global flow does not care about negation at all.
*/
private import codeql_ql.ast.Ast
private import internal.NodesInternal
private import internal.DataFlowNumbering
private import internal.LocalFlow as LocalFlow
private import internal.GlobalFlow as GlobalFlow
/**
* An expression or variable in a formula, including some additional nodes
* that are not part of the AST.
*
* Nodes that are locally bound together by equalities are clustered into a "super node",
* which can be accessed using `getSuperNode()`. There is usually no reason to use `Node` directly
* other than to reason about what kind of node is contained in a super node.
*
* To reason about global data flow, use `SuperNode.track()`.
*/
class Node extends TNode {
/** Gets a string representation of this element. */
string toString() { none() } // overridden in subclasses
/** Gets the location of element. */
Location getLocation() { none() } // overridden in subclasses
/**
* Gets the underlying `Expr` or `VarDef` node, if this is an `AstNodeNode`.
*/
AstNode asAstNode() { astNode(result) = this }
/**
* Gets the predicate containing this data-flow node.
*
* All data-flow nodes belong in exactly one predicate.
* TODO: select clauses
*/
Predicate getEnclosingPredicate() { none() } // overridden in subclasses
/**
* Gets the collection of data-flow nodes locally bound by equalities, represented
* by a "super node".
*
* Super nodes are the medium through which to propagate data-flow information globally.
*/
SuperNode getSuperNode() { result.getANode() = this }
}
/**
* A data-flow node based an `Expr` or `VarDef` AST node.
*/
class AstNodeNode extends Node, MkAstNodeNode {
private AstNode ast;
AstNodeNode() { this = MkAstNodeNode(ast) }
override string toString() { result = ast.toString() }
override Location getLocation() { result = ast.getLocation() }
/** Gets the AST node. */
AstNode getAstNode() { result = ast }
override Predicate getEnclosingPredicate() { result = ast.getEnclosingPredicate() }
}
/**
* Gets the data-flow node correspoinding to the given AST node.
*/
pragma[inline]
Node astNode(AstNode node) { result = MkAstNodeNode(node) }
/**
* A data-flow node representing a variable within a specific scope.
*/
class ScopedVariableNode extends Node, MkScopedVariable {
private VarDef var;
private AstNode scope;
ScopedVariableNode() { this = MkScopedVariable(var, scope) }
override string toString() {
result =
"Variable '" + var.getName() + "' scoped to " + scope.getLocation().getStartLine() + ":" +
scope.getLocation().getStartColumn()
}
override Location getLocation() { result = scope.getLocation() }
/** Gets the variable being refined to a specific scope. */
VarDef getVariable() { result = var }
/** Gets the scope to which this variable has been refined. */
AstNode getScope() { result = scope }
override Predicate getEnclosingPredicate() { result = var.getEnclosingPredicate() }
}
/**
* Gets the data-flow node corresponding to `var` restricted to `scope`.
*/
pragma[inline]
Node scopedVariable(VarDef var, AstNode scope) { result = MkScopedVariable(var, scope) }
/**
* A data-flow node representing `this` within a class predicate, charpred, or newtype branch.
*/
class ThisNode extends Node, MkThisNode {
private Predicate pred;
ThisNode() { this = MkThisNode(pred) }
override string toString() { result = "'this' in " + pred.getName() }
override Location getLocation() { result = pred.getLocation() }
/** Gets the class predicate, charpred, or newtype branch whose 'this' parameter is represented by this node. */
Predicate getPredicate() { result = pred }
override Predicate getEnclosingPredicate() { result = pred }
}
/**
* Gets the data-flow node representing `this` within the given class predicate, charpred, or newtype branch.
*/
pragma[inline]
Node thisNode(Predicate pred) { result = MkThisNode(pred) }
/**
* A data-flow node representing `result` within a predicate that has a result.
*/
class ResultNode extends Node, MkResultNode {
private Predicate pred;
ResultNode() { this = MkResultNode(pred) }
override string toString() { result = "'result' in " + pred.getName() }
override Location getLocation() { result = pred.getLocation() }
/** Gets the predicate whose 'result' parameter is represented by this node. */
Predicate getPredicate() { result = pred }
override Predicate getEnclosingPredicate() { result = pred }
}
/**
* Gets the data-flow node representing `result` within the given predicate.
*/
pragma[inline]
Node resultNode(Predicate pred) { result = MkResultNode(pred) }
/**
* A data-flow node representing the view of a field in the enclosing class, as seen
* from a charpred or class predicate.
*/
class FieldNode extends Node, MkFieldNode {
private Predicate pred;
private FieldDecl fieldDecl;
FieldNode() { this = MkFieldNode(pred, fieldDecl) }
/** Gets the member predicate or charpred for which this node represents access to the field. */
Predicate getPredicate() { result = pred }
/** Gets the declaration of the field. */
FieldDecl getFieldDeclaration() { result = fieldDecl }
/** Gets the name of the field. */
string getFieldName() { result = fieldDecl.getName() }
override string toString() { result = "'" + this.getFieldName() + "' in " + pred.getName() }
override Location getLocation() { result = pred.getLocation() }
override Predicate getEnclosingPredicate() { result = pred }
}
/**
* Gets the data-flow node representing the given predicate's view of the given field
* in the enclosing class.
*/
pragma[inline]
Node fieldNode(Predicate pred, FieldDecl fieldDecl) { result = MkFieldNode(pred, fieldDecl) }
/**
* A collection of data-flow nodes in the same predicate, locally bound by equalities.
*
* To reason about global data flow, use `SuperNode.track()`.
*/
class SuperNode extends LocalFlow::TSuperNode {
private int repr;
SuperNode() { this = LocalFlow::MkSuperNode(repr) }
/** Gets a data-flow node that is part of this super node. */
Node getANode() { LocalFlow::getRepr(result) = repr }
/** Gets an AST node from any of the nodes in this super node. */
AstNode asAstNode() { result = getANode().asAstNode() }
/**
* Gets a single node from this super node.
*
* The node is arbitrary and the caller should not rely on how the node is chosen.
* The node is currently chosen such that:
* - An `AstNodeNode` is preferred over other nodes.
* - A node occuring earlier is preferred over one occurring later.
*/
Node getArbitraryRepr() { result = min(Node n | n = getANode() | n order by getInternalId(n)) }
/**
* Gets the predicate containing all nodes that are part of this super node.
*/
Predicate getEnclosingPredicate() { result = getANode().getEnclosingPredicate() }
/** Gets a string representation of this super node. */
string toString() {
exists(int c |
c = strictcount(getANode()) and
result = "Super node of " + c + " nodes in " + getEnclosingPredicate().getName()
)
}
/** Gets the location of an arbitrary node in this super node. */
Location getLocation() { result = getArbitraryRepr().getLocation() }
/** Gets any member call whose receiver is in the same super node. */
MemberCall getALocalMemberCall() { superNode(result.getBase()) = this }
/** Gets any member call whose receiver is in the same super node. */
MemberCall getALocalMemberCall(string name) {
result = this.getALocalMemberCall() and
result.getMemberName() = name
}
/**
* Gets a node that this node may "flow to" after one step.
*
* Basic usage of `track()` to track some expressions looks like this:
* ```
* DataFlow::SuperNode myThing(DataFlow::Tracker t) {
* t.start() and
* result = DataFlow::superNode(< some ast node >)
* or
* exists (DataFlow::Tracker t2 |
* result = myThing(t2).track(t2, t)
* )
* }
*
* DataFlow::SuperNode myThing() { result = myThing(DataFlow::Tracker::end()) }
* ```
*/
pragma[inline]
SuperNode track(Tracker t1, Tracker t2) {
// Return state -> return state
// Store the return edge in t2
not t1.hasCall() and
GlobalFlow::directedEdgeSuper(result, this, t2)
or
// Call state or initial state -> call state
t1.hasCallOrIsStart() and
t2.hasCall() and
GlobalFlow::directedEdgeSuper(this, result, _)
or
// Return state -> call state
// The last-used return edge must not be used as the initial call edge
// (doing so would allow returning out of a disjunction and into another branch of that disjunction)
not t1.hasCall() and
t2.hasCall() and
exists(GlobalFlow::EdgeLabel edge |
GlobalFlow::directedEdgeSuper(this, result, edge) and
edge != t1
)
}
/**
* Gets node containing a string flowing to this node via `t`.
*/
cached
private string getAStringValue(Tracker t) {
t.start() and
result = asAstNode().(String).getValue()
or
exists(SuperNode pred, Tracker t2 |
this = pred.track(t2, t) and
result = pred.getAStringValue(t2)
)
or
// Step through calls to a few built-ins that don't cause a blow-up
exists(SuperNode pred, string methodName, string oldValue |
this.asAstNode() = pred.getALocalMemberCall(methodName) and
oldValue = pred.getAStringValue(t)
|
methodName = "toLowerCase" and
result = oldValue.toLowerCase()
or
methodName = "toUpperCase" and
result = oldValue.toUpperCase()
)
}
/** Gets a string constant that may flow here (possibly from a caller context). */
pragma[inline]
string getAStringValue() { result = this.getAStringValue(Tracker::end()) }
/** Gets a string constant that may flow here, possibly out of callees, but not from caller contexts. */
pragma[inline]
string getAStringValueNoCall() { result = this.getAStringValue(Tracker::endNoCall()) }
/**
* Gets a string constant that may flow here, which can safely be combined with another
* value that was tracked here with `otherT`.
*
* This is under-approximate and will fail to accept valid matches when both values
* came in from the same chain of calls.
*/
bindingset[otherT]
string getAStringValueForContext(Tracker otherT) {
exists(Tracker stringT |
result = this.getAStringValue(stringT) and
otherT.isSafeToCombineWith(stringT)
)
}
}
/** Gets the super node for the given AST node. */
pragma[inline]
SuperNode superNode(AstNode node) { result = astNode(node).getSuperNode() }
/**
* A summary of the steps needed to reach a node in the global data flow graph,
* to be used in combination with `SuperNode.track`.
*/
class Tracker extends GlobalFlow::TEdgeLabelOrTrackerState {
/** Holds if this is the starting point, that is, the summary of the empty path. */
predicate start() { this = GlobalFlow::MkNoEdge() }
/** Holds if a call step has been used (possibly preceeded by return steps). */
predicate hasCall() { this = GlobalFlow::MkHasCall() }
/** Holds if either `start()` or `hasCall()` holds */
predicate hasCallOrIsStart() { this.start() or this.hasCall() }
/**
* Holds if the two trackers are safe to combine, in the sense that
* they don't make contradictory assumptions what context they're in.
*
* This is approximate and will reject any pair of trackers that have
* both used a call or locally came from the same disjunction.
*/
pragma[inline]
predicate isSafeToCombineWith(Tracker other) {
not (
// Both values came from a call, they could come from different call sites.
this.hasCall() and
other.hasCall()
or
// Both values came from the same disjunction, they could come from different branches.
this = other and
this instanceof GlobalFlow::MkDisjunction
)
}
/** Gets a string representation of this element. */
string toString() {
this instanceof GlobalFlow::MkNoEdge and
result = "Tracker in initial state"
or
this instanceof GlobalFlow::MkHasCall and
result = "Tracker with calls"
or
this instanceof GlobalFlow::EdgeLabel and
result = "Tracker with return step out of " + this.(GlobalFlow::EdgeLabel).toString()
}
}
module Tracker {
/** Gets a valid end-point for tracking. */
Tracker end() { any() }
/** Gets a valid end-point for tracking where no calls were used. */
Tracker endNoCall() { not result.hasCall() }
}

View File

@@ -0,0 +1,51 @@
private import codeql_ql.ast.Ast
private import codeql_ql.ast.internal.AstNodeNumbering
private import NodesInternal
/** An arbitrary total ordering of data-flow nodes. */
private predicate internalOrderingKey(TNode node, int tag, int field1, int field2) {
exists(AstNode ast |
node = MkAstNodeNode(ast) and
tag = 0 and
field1 = getPreOrderId(ast) and
field2 = 0
)
or
exists(VarDef var, Formula scope |
node = MkScopedVariable(var, scope) and
tag = 1 and
field1 = getPreOrderId(var) and
field2 = getPreOrderId(scope)
)
or
exists(Predicate pred |
node = MkThisNode(pred) and
tag = 2 and
field1 = getPreOrderId(pred) and
field2 = 0
)
or
exists(Predicate pred |
node = MkResultNode(pred) and
tag = 3 and
field1 = getPreOrderId(pred) and
field2 = 0
)
or
exists(Predicate pred, FieldDecl fieldDecl |
node = MkFieldNode(pred, fieldDecl) and
tag = 4 and
field1 = getPreOrderId(pred) and
field2 = getPreOrderId(fieldDecl)
)
}
/** Gets an integer unique to `node`. */
int getInternalId(TNode node) {
node =
rank[result](TNode n, int tag, int field1, int field2 |
internalOrderingKey(n, tag, field1, field2)
|
n order by tag, field1, field2
)
}

View File

@@ -0,0 +1,242 @@
/**
* Models global flow, that is, flow between predicates and across disjunctions.
*
* We say an expression `A` "flows to" another expression `B` if some series of transformations
* would move/copy them into the same conjunction, where they are bound by equality (or are the same variable).
*
* The transformations permitted are:
* - Apply distributive law to lift a disjunction out: `A and (B or C) -> (A and B) or (A and C)`
* - Inline a predicate call.
*
* Both of these tend to copy expressions, and we just ask if some copy of `A` and some copy of `B` could
* end up in the same place.
*
* For example, `A` flows to both `B` and `C`, but `B` does not flow to `C`:
* ```
* x = A and (x = B or x = C)
* -->
* (x = A and x = B) or (x = A and x = C)
* ```
*
* We don't actually perform these exponential-cost transformations, we just use them to specify what "flow" means.
*
* The problem of determining which expression flows where is reduced to a graph problem.
* The edges in the graph represent inlining of a predicate or lifting of a disjunction.
* The resulting graph is similar to the call/return edges from procedural languages, but with a
* few tweaks -- see `EdgeLabel`.
*
* Note that the "flows to" relation is symmetric, but variations like "flows to using call steps" are not.
* So we use the term "flows to" rather than a more naturally symmetric term like "unifiable with".
*/
private import codeql_ql.ast.Ast
private import codeql_ql.dataflow.DataFlow
private import VarScoping
cached
private module Cached {
/**
* An edge or a `Tracker` state.
*
* An edge represents a call site or a disjunction, and a `Tracker` state is an
* edge or one of the special values `NoEdge` or `HasCall`.
*
* The tracker state could be defined as
* ```
* newtype TTracker = MkReturn(TEdgeLabel edge) or MkNoEdge() or MkHasCall()
* ```
* but for efficiency, `TTracker` and `TEdgeLabel` have been merged.
*/
cached
newtype TEdgeLabelOrTrackerState =
MkCall(Call call) or
MkMemberToCharPred(ClassPredicate p) or // implicit call to charpred in a member predicate
MkTypeToCharPred(TypeExpr t) or // implicit call to charpred from a type annotation
MkDisjunction(DisjunctionOperator disj) or // "call" into a disjunction branch
MkNoEdge() or
MkHasCall()
/**
* Label for a directed edge (see `EdgeLabel` class).
*/
class TEdgeLabel = MkCall or MkMemberToCharPred or MkTypeToCharPred or MkDisjunction;
/**
* Holds if `argument` is passed to `parameter` via an edge of the given `edge`.
*/
cached
predicate directedEdge(Node argument, Node parameter, EdgeLabel edge) {
// Argument-passing via an explicit call
exists(Call call, Predicate target |
call.getTarget() = target and
edge = EdgeLabel::call(call)
|
exists(int i |
argument = astNode(call.getArgument(i)) and
parameter = astNode(target.getParameter(i))
)
or
argument = astNode(call.(MemberCall).getBase()) and
parameter = thisNode(target)
or
argument = astNode(call) and
parameter = resultNode(target)
)
or
// Implicit charpred call in member predicate: passes `this` and each field to the charpred
exists(Class cls, ClassPredicate p |
p = cls.getAClassPredicate() and
edge = EdgeLabel::memberToCharPred(p)
|
argument = thisNode(p) and
parameter = thisNode(cls.getCharPred())
or
exists(FieldDecl fieldDecl |
argument = fieldNode(p, fieldDecl) and
parameter = fieldNode(cls.getCharPred(), fieldDecl)
)
)
or
// Type test passes the tested value to the charpred of the type
exists(TypeExpr type |
edge = EdgeLabel::typeToCharPred(type) and
parameter = thisNode(type.getResolvedType().getDeclaration().(Class).getCharPred())
|
exists(VarDecl var |
argument = [scopedVariable(var, _), astNode(var)] and
type = var.getTypeExpr()
)
or
exists(Predicate p |
argument = resultNode(p) and
type = p.getReturnTypeExpr()
)
or
exists(InlineCast cast |
argument = astNode(cast.getBase()) and
type = cast.getTypeExpr()
)
or
exists(InstanceOf expr |
argument = astNode(expr.getExpr()) and
type = expr.getType()
)
)
or
// Flow between differently-scoped copies of the same variable, going into a disjunction
exists(VarDef var, AstNode inner, AstNode outer, DisjunctionOperator disjunction |
isRefinement(var, inner, outer) and
disjunction.getAnOperand() = inner and
edge = EdgeLabel::disjunction(disjunction)
|
// Scoped variable to an inner scoped variable
argument = scopedVariable(var, outer) and
parameter = scopedVariable(var, inner) and
// Avoid loop edge when a VarAccess is also a disjunct (because it's an element of a set literal)
argument != parameter
or
// VarDef to a scoped variable
outer = getVarDefScope(var) and
argument = astNode(var) and
parameter = scopedVariable(var, inner)
)
or
// Flow into a set literal, similar to flow into disjunctions.
// If we consider a desugared set literal, `[x,y] -> any(T v | v = x or v = y)`, this edge
// corresponds to the edges going from `T v` to its scoped variables.
exists(Set set |
edge = EdgeLabel::disjunction(set) and
argument = astNode(set) and
parameter = astNode(set.getAnElement())
)
}
/**
* Holds if `argument` is passed to `parameter` via an edge of the given `kind`.
*
* This is identical to `directedEdge` where the operands are mapped to their super nodes.
*/
cached
predicate directedEdgeSuper(SuperNode argument, SuperNode parameter, EdgeLabel edge) {
directedEdge(argument.getANode(), parameter.getANode(), edge)
}
}
import Cached
/**
* Label for a directed edge.
*
* This is either a call (implicit or explicit) or a disjunction.
*
* All edges are considered "call" edges in their default orientation (even disjunction edges).
* Flipping an edge turns it into a "return" edge (there is no separate label for return edges).
*
* Data flow allows any number of return edges followed by any number of call edges,
* with this additional rule:
*
* - The first call edge must not have the same label as the last return edge.
*
* The above rule is the reason this class exists.
*
* - The rule ensures flow cannot step out of a disjunction, and then
* into another branch of the same disjunction (because they'd use the same label).
*
* - A byproduct of the rule is that we cannot step directly into the call edge we just came from.
* We lose no real flow from this, as the flow direction can just flip one step earlier.
*
* - It would not be enough to only enforce this for disjunction edges, because it would
* allow the path to "reset" its last-used edge by stepping out of a call and
* immediately back in again (the kind of flow mentioned above).
*/
class EdgeLabel extends TEdgeLabel {
/** Gets a location associated with this edge label. */
Location getLocation() {
exists(Call call | this = EdgeLabel::call(call) | result = call.getLocation())
or
exists(ClassPredicate p | this = EdgeLabel::memberToCharPred(p) | result = p.getLocation())
or
exists(TypeExpr t | this = EdgeLabel::typeToCharPred(t) | result = t.getLocation())
or
exists(DisjunctionOperator disj | this = EdgeLabel::disjunction(disj) |
result = disj.getLocation()
)
}
/** Gets a string representation of this edge label. */
string toString() {
exists(Call call | this = EdgeLabel::call(call) |
result =
call.toString() + ":" + call.getLocation().getStartLine() + ":" +
call.getLocation().getStartColumn()
)
or
exists(ClassPredicate p | this = EdgeLabel::memberToCharPred(p) |
result = "MemberToCharPred call from " + p.getName()
)
or
exists(TypeExpr t | this = EdgeLabel::typeToCharPred(t) |
result = "TypeToCharPred call from " + t.getResolvedType().getName()
)
or
exists(DisjunctionOperator disj | this = EdgeLabel::disjunction(disj) |
result =
disj.toString() + ":" + disj.getLocation().getStartLine() + ":" +
disj.getLocation().getStartColumn()
)
}
}
module EdgeLabel {
/** Gets the edge label representing the given explicit call. */
EdgeLabel call(Call call) { result = MkCall(call) }
/** Gets the edge label for the implicit call to the charpred in a member predicate. */
EdgeLabel memberToCharPred(ClassPredicate p) { result = MkMemberToCharPred(p) }
/** Gets the edge label for the implicit call to the charpred from a type annotation. */
EdgeLabel typeToCharPred(TypeExpr t) { result = MkTypeToCharPred(t) }
/** Gets the edge label for the edge stepping into the given disjunction. */
EdgeLabel disjunction(DisjunctionOperator disj) { result = MkDisjunction(disj) }
}

View File

@@ -0,0 +1,102 @@
/**
* Models local flow edges. Each equivalence class in the local flow relation becomes a super node.
*/
private import codeql_ql.dataflow.DataFlow
private import codeql_ql.ast.Ast
private import codeql_ql.ast.internal.AstNodeNumbering
private import NodesInternal
private import VarScoping
private import DataFlowNumbering
private module Cached {
/**
* Holds if `x` and `y` are bound by an equality (intra-predicate only).
*
* This edge has no orientation, and is used to construct the equivalence relation.
* Each equivalence class becomes a `SuperNode`.
*/
private predicate localEdge(Node x, Node y) {
exists(AstNode a, AstNode b |
x = astNode(a) and
y = astNode(b)
|
// x ~ any(x)
a = b.(Any).getExpr(0)
or
// v ~ any(T v)
a = b.(Any).getArgument(0)
or
// x ~ x as VAR
a = b.(AsExpr).getInnerExpr()
or
// x ~ x.(Type)
a = b.(InlineCast).getBase()
or
// x = y ==> x ~ y
exists(ComparisonFormula compare |
compare.getOperator() = "=" and
a = compare.getLeftOperand() and
b = compare.getRightOperand()
)
)
or
// VarAccess -> ScopedVariable
exists(VarDef var, VarAccess access, VarAccessOrDisjunct scope |
isRefinement(var, access, scope) and
x = astNode(access) and
y = scopedVariable(var, scope)
)
or
// VarAccess -> VarDef (if no refinement exists)
exists(VarDef var, VarAccess access |
isRefinement(var, access, getVarDefScope(var)) and
x = astNode(access) and
y = astNode(var)
)
or
// result ~ enclosing 'result' node
x = resultNode(y.(AstNodeNode).getAstNode().(ResultAccess).getEnclosingPredicate())
or
// this ~ enclosing 'this' node
x = thisNode(y.(AstNodeNode).getAstNode().(ThisAccess).getEnclosingPredicate())
or
// f ~ enclosing field node for 'f'
exists(FieldAccess access |
x = astNode(access) and
y = fieldNode(access.getEnclosingPredicate(), access.getDeclaration())
)
or
// field declaration ~ field node in the charpred
exists(FieldDecl field, Class cls |
cls.getAField() = field and
x = astNode(field.getVarDecl()) and
y = fieldNode(cls.getCharPred(), field)
)
}
/** Like `localEdge` but the parameters are mapped to their internal ID. */
private predicate rawLocalEdge(int x, int y) {
exists(Node a, Node b |
localEdge(a, b) and
x = getInternalId(a) and
y = getInternalId(b)
)
or
// Ensure a representative is generated for singleton components
x = getInternalId(_) and
y = x
}
/** Gets the representative for the equivalence class containing the node with ID `x`. */
private int getRawRepr(int x) = equivalenceRelation(rawLocalEdge/2)(x, result)
/** Gets the ID for the equivalence class containing `node`. */
cached
int getRepr(Node node) { result = getRawRepr(getInternalId(node)) }
cached
newtype TSuperNode = MkSuperNode(int repr) { repr = getRepr(_) }
}
import Cached

View File

@@ -0,0 +1,24 @@
private import codeql_ql.ast.Ast
private import VarScoping
newtype TNode =
MkAstNodeNode(AstNode node) {
node instanceof Expr or
node instanceof VarDef
} or
MkScopedVariable(VarDef var, AstNode scope) {
isRefinement(var, _, scope) and
not scope = getVarDefScope(var)
} or
MkThisNode(Predicate pred) {
pred instanceof ClassPredicate or
pred instanceof CharPred or
pred instanceof NewTypeBranch
} or
MkResultNode(Predicate pred) { exists(pred.getReturnTypeExpr()) } or
MkFieldNode(Predicate pred, FieldDecl fieldDecl) {
// TODO: should this be omitted when the field is not referenced?
fieldDecl.getVarDecl() = pred.(ClassPredicate).getDeclaringType().getField(_)
or
fieldDecl.getVarDecl() = pred.(CharPred).getDeclaringType().getField(_)
}

View File

@@ -0,0 +1,115 @@
/**
* Computes scopes in which it is safe to unify all uses of a given variable.
*
* It is not accurate to unify variables across a disjunction, so the scope of a variable
* is restricted to its nearest enclosing disjunction operand ("disjunct").
* At such a disjunct, we introduce a "refinement" of the variable, which is seen as a
* redefinition of the variable within that disjunct.
*
* In principle this should also be done for `this`, `result`, and local field variables
* but currently it is not.
*/
private import codeql_ql.ast.Ast
private import codeql_ql.ast.internal.AstNodeNumbering
/** Gets the disjunction immediately containing another disjunction `inner`. */
private Disjunction getOuterDisjunction(Disjunction inner) { result.getAnOperand() = inner }
/**
* Get the root of a disjunction tree containing `f`, if any.
*/
private Disjunction getRootDisjunction(Disjunction f) {
not exists(getOuterDisjunction(result)) and
result = getOuterDisjunction(f)
or
result = getRootDisjunction(getOuterDisjunction(f))
}
/** Get the root disjunction for `f` if there is one, other gets `f` itself. */
pragma[inline]
private AstNode tryGetRootDisjunction(AstNode f) {
result = getRootDisjunction(f)
or
not exists(getRootDisjunction(f)) and
result = f
}
AstNode getADisjunctionOperand(AstNode disjunction) {
exists(Disjunction d |
result = d.getAnOperand() and
// skip intermediate nodes in large disjunctions
disjunction = tryGetRootDisjunction(d) and
not result instanceof Disjunction
)
or
result = disjunction.(Implication).getAChild()
or
result = disjunction.(IfFormula).getThenPart()
or
result = disjunction.(IfFormula).getElsePart()
or
exists(Forall all |
disjunction = all and
exists(all.getFormula()) and
exists(all.getRange()) and
result = [all.getRange(), all.getFormula()]
)
or
result = disjunction.(Set).getAnElement()
}
/**
* A node that acts as a disjunction:
* - The root in a tree of `or` operators, or
* - An `implies`, `if`, `forall`, or set literal.
*/
class DisjunctionOperator extends AstNode {
DisjunctionOperator() { exists(getADisjunctionOperand(this)) }
AstNode getAnOperand() { result = getADisjunctionOperand(this) }
}
/**
* Gets the scope of `var`, such as the predicate or `exists` clause that binds it.
*/
AstNode getVarDefScope(VarDef var) {
// TODO: not valid for `as` expressions
result = var.getParent()
}
/** A `VarAccess` or disjunct, representing the input to refinement of a variable. */
class VarAccessOrDisjunct = AstNode;
/**
* Walks upwards from an access to `varDef` until encountering either the scope of `varDef`
* or a disjunct. When a disjunct is found, the disjunct becomes the new `access`, representing
* a refinement we intend to insert there.
*/
private AstNode getVarScope(VarDef varDef, VarAccessOrDisjunct access) {
access.(VarAccess).getDeclaration() = varDef and
result = access
or
exists(AstNode scope | scope = getVarScope(varDef, access) |
not scope = getADisjunctionOperand(_) and
not scope = getVarDefScope(varDef) and
result = scope.getParent()
)
or
isRefinement(varDef, _, access) and
result = tryGetRootDisjunction(access.getParent())
}
/**
* Holds if `inner` should be seen as a refinement of `outer`.
*
* `outer` is always a disjunct, and `inner` is either a `VarAccess` or another disjunct.
*/
predicate isRefinement(VarDef varDef, VarAccessOrDisjunct inner, VarAccessOrDisjunct outer) {
getVarScope(varDef, inner) = outer and
(
outer = getADisjunctionOperand(_)
or
outer = getVarDefScope(varDef)
)
}

View File

@@ -0,0 +1,104 @@
/**
* An experimental and incomplete query for measuring framework coverage
* for models implemented in CodeQL.
*
* Currently only supports JavaScript models, and simply lists the package names
* alongside the named features accessed on such a package.
*/
import ql
import codeql_ql.ast.internal.AstNodes
import codeql_ql.dataflow.DataFlow as DataFlow
predicate isExcludedFile(File file) {
file.getAbsolutePath().matches(["%ql/experimental/%", "%ql/test/%"])
}
class PackageImportCall extends PredicateCall {
PackageImportCall() {
this.getQualifier().getName() = ["API", "DataFlow"] and
this.getPredicateName() = ["moduleImport", "moduleMember"] and
not isExcludedFile(getLocation().getFile())
}
/** Gets the name of a package referenced by this call */
string getAPackageName() { result = DataFlow::superNode(getArgument(0)).getAStringValueNoCall() }
}
/** Gets a reference to `package` or any transitive member thereof. */
DataFlow::SuperNode getADerivedRef(string package, DataFlow::Tracker t) {
t.start() and
result.asAstNode().(PackageImportCall).getAPackageName() = package
or
exists(DataFlow::Tracker t2 | result = getADerivedRef(package, t2).track(t2, t))
or
result.asAstNode() = getADerivedCall(package, t)
}
/** Gets a call which models some aspect of `package`. */
MemberCall getADerivedCall(string package, DataFlow::Tracker t) {
result = getADerivedRef(package, t).getALocalMemberCall() and
not result.(Expr).getType().getName() = ["int", "string"]
}
/**
* Gets an expression whose string-value is the name of a member accessed from `package`,
* where the underlying package node was tracked here using `t`.
*/
Expr getAFeatureUse(string package, DataFlow::Tracker t) {
exists(MemberCall call | call = getADerivedCall(package, t) |
call.getMemberName() =
[
"getMember", "getAPropertyRead", "getAPropertyWrite", "getAPropertyReference",
"getAPropertySource", "getAMethodCall", "getAMemberCall"
] and
result = call.getArgument(0)
or
call.getMemberName() = "getOptionArgument" and
result = call.getArgument(1)
)
or
t.start() and
exists(PackageImportCall call |
call.getAPackageName() = package and
call.getPredicateName() = "moduleMember" and
result = call.getArgument(1)
)
}
/**
* Gets the name of a feature accessed as `use`.
*/
string getAFeatureName(string package, Expr use) {
exists(DataFlow::Tracker t |
use = getAFeatureUse(package, t) and
result = DataFlow::superNode(use).getAStringValueForContext(t)
)
}
query predicate packageFeatures(string package, string features) {
// TODO: 'express' still missing features from request objects - likely subclassing-related
package = any(PackageImportCall call).getAPackageName() and
features = concat(getAFeatureName(package, _), ", ")
}
/** Holds if `cls` extends an abstract class from another file. */
predicate isCrossFileContribution(Class cls) {
exists(Class sup |
cls.getASuperType().getResolvedType().getDeclaration() = sup and
sup.isAbstract() and
sup.getLocation().getFile() != cls.getLocation().getFile()
)
}
query predicate packageConcepts(string package, Class concept) {
package = any(PackageImportCall call).getAPackageName() and
getADerivedRef(package, DataFlow::Tracker::endNoCall()).getANode() =
DataFlow::thisNode(concept.getCharPred()) and
isCrossFileContribution(concept)
}
query predicate importWithoutPackageName(PackageImportCall call, string path) {
not exists(call.getAPackageName()) and
path = call.getLocation().getFile().getRelativePath()
}

View File

@@ -0,0 +1,365 @@
/**
* Provides a library for writing QL tests whose success or failure is based on expected results
* embedded in the test source code as comments, rather than the contents of an `.expected` file
* (in that the `.expected` file should always be empty).
*
* To add this framework to a new language:
* - Add a file `InlineExpectationsTestPrivate.qll` that defines a `ExpectationComment` class. This class
* must support a `getContents` method that returns the contents of the given comment, _excluding_
* the comment indicator itself. It should also define `toString` and `getLocation` as usual.
*
* To create a new inline expectations test:
* - Declare a class that extends `InlineExpectationsTest`. In the characteristic predicate of the
* new class, bind `this` to a unique string (usually the name of the test).
* - Override the `hasActualResult()` predicate to produce the actual results of the query. For each
* result, specify a `Location`, a text description of the element for which the result was
* reported, a short string to serve as the tag to identify expected results for this test, and the
* expected value of the result.
* - Override `getARelevantTag()` to return the set of tags that can be produced by
* `hasActualResult()`. Often this is just a single tag.
*
* Example:
* ```ql
* class ConstantValueTest extends InlineExpectationsTest {
* ConstantValueTest() { this = "ConstantValueTest" }
*
* override string getARelevantTag() {
* // We only use one tag for this test.
* result = "const"
* }
*
* override predicate hasActualResult(
* Location location, string element, string tag, string value
* ) {
* exists(Expr e |
* tag = "const" and // The tag for this test.
* value = e.getValue() and // The expected value. Will only hold for constant expressions.
* location = e.getLocation() and // The location of the result to be reported.
* element = e.toString() // The display text for the result.
* )
* }
* }
* ```
*
* There is no need to write a `select` clause or query predicate. All of the differences between
* expected results and actual results will be reported in the `failures()` query predicate.
*
* To annotate the test source code with an expected result, place a comment starting with a `$` on the
* same line as the expected result, with text of the following format as the body of the comment:
*
* `tag=expected-value`
*
* Where `tag` is the value of the `tag` parameter from `hasActualResult()`, and `expected-value` is
* the value of the `value` parameter from `hasActualResult()`. The `=expected-value` portion may be
* omitted, in which case `expected-value` is treated as the empty string. Multiple expectations may
* be placed in the same comment. Any actual result that
* appears on a line that does not contain a matching expected result comment will be reported with
* a message of the form "Unexpected result: tag=value". Any expected result comment for which there
* is no matching actual result will be reported with a message of the form
* "Missing result: tag=expected-value".
*
* Example:
* ```cpp
* int i = x + 5; // $ const=5
* int j = y + (7 - 3) // $ const=7 const=3 const=4 // The result of the subtraction is a constant.
* ```
*
* For tests that contain known missing and spurious results, it is possible to further
* annotate that a particular expected result is known to be spurious, or that a particular
* missing result is known to be missing:
*
* `$ SPURIOUS: tag=expected-value` // Spurious result
* `$ MISSING: tag=expected-value` // Missing result
*
* A spurious expectation is treated as any other expected result, except that if there is no
* matching actual result, the message will be of the form "Fixed spurious result: tag=value". A
* missing expectation is treated as if there were no expected result, except that if a
* matching expected result is found, the message will be of the form
* "Fixed missing result: tag=value".
*
* A single line can contain all the expected, spurious and missing results of that line. For instance:
* `$ tag1=value1 SPURIOUS: tag2=value2 MISSING: tag3=value3`.
*
* If the same result value is expected for two or more tags on the same line, there is a shorthand
* notation available:
*
* `tag1,tag2=expected-value`
*
* is equivalent to:
*
* `tag1=expected-value tag2=expected-value`
*/
private import InlineExpectationsTestPrivate
/**
* The base class for tests with inline expectations. The test extends this class to provide the actual
* results of the query, which are then compared with the expected results in comments to produce a
* list of failure messages that point out where the actual results differ from the expected
* results.
*/
abstract class InlineExpectationsTest extends string {
bindingset[this]
InlineExpectationsTest() { any() }
/**
* Returns all tags that can be generated by this test. Most tests will only ever produce a single
* tag. Any expected result comments for a tag that is not returned by the `getARelevantTag()`
* predicate for an active test will be ignored. This makes it possible to write multiple tests in
* different `.ql` files that all query the same source code.
*/
abstract string getARelevantTag();
/**
* Returns the actual results of the query that is being tested. Each result consist of the
* following values:
* - `location` - The source code location of the result. Any expected result comment must appear
* on the start line of this location.
* - `element` - Display text for the element on which the result is reported.
* - `tag` - The tag that marks this result as coming from this test. This must be one of the tags
* returned by `getARelevantTag()`.
* - `value` - The value of the result, which will be matched against the value associated with
* `tag` in any expected result comment on that line.
*/
abstract predicate hasActualResult(Location location, string element, string tag, string value);
/**
* Holds if there is an optional result on the specified location.
*
* This is similar to `hasActualResult`, but returns results that do not require a matching annotation.
* A failure will still arise if there is an annotation that does not match any results, but not vice versa.
* Override this predicate to specify optional results.
*/
predicate hasOptionalResult(Location location, string element, string tag, string value) {
none()
}
final predicate hasFailureMessage(FailureLocatable element, string message) {
exists(ActualResult actualResult |
actualResult.getTest() = this and
element = actualResult and
(
exists(FalseNegativeExpectation falseNegative |
falseNegative.matchesActualResult(actualResult) and
message = "Fixed missing result:" + falseNegative.getExpectationText()
)
or
not exists(ValidExpectation expectation | expectation.matchesActualResult(actualResult)) and
message = "Unexpected result: " + actualResult.getExpectationText() and
not actualResult.isOptional()
)
)
or
exists(ValidExpectation expectation |
not exists(ActualResult actualResult | expectation.matchesActualResult(actualResult)) and
expectation.getTag() = getARelevantTag() and
element = expectation and
(
expectation instanceof GoodExpectation and
message = "Missing result:" + expectation.getExpectationText()
or
expectation instanceof FalsePositiveExpectation and
message = "Fixed spurious result:" + expectation.getExpectationText()
)
)
or
exists(InvalidExpectation expectation |
element = expectation and
message = "Invalid expectation syntax: " + expectation.getExpectation()
)
}
}
/**
* RegEx pattern to match a comment containing one or more expected results. The comment must have
* `$` as its first non-whitespace character. Any subsequent character
* is treated as part of the expected results, except that the comment may contain a `//` sequence
* to treat the remainder of the line as a regular (non-interpreted) comment.
*/
private string expectationCommentPattern() { result = "\\s*\\$((?:[^/]|/[^/])*)(?://.*)?" }
/**
* The possible columns in an expectation comment. The `TDefaultColumn` branch represents the first
* column in a comment. This column is not precedeeded by a name. `TNamedColumn(name)` represents a
* column containing expected results preceeded by the string `name:`.
*/
private newtype TColumn =
TDefaultColumn() or
TNamedColumn(string name) { name = ["MISSING", "SPURIOUS"] }
bindingset[start, content]
private int getEndOfColumnPosition(int start, string content) {
result =
min(string name, int cand |
exists(TNamedColumn(name)) and
cand = content.indexOf(name + ":") and
cand >= start
|
cand
)
or
not exists(string name |
exists(TNamedColumn(name)) and
content.indexOf(name + ":") >= start
) and
result = content.length()
}
private predicate getAnExpectation(
ExpectationComment comment, TColumn column, string expectation, string tags, string value
) {
exists(string content |
content = comment.getContents().regexpCapture(expectationCommentPattern(), 1) and
(
column = TDefaultColumn() and
exists(int end |
end = getEndOfColumnPosition(0, content) and
expectation = content.prefix(end).regexpFind(expectationPattern(), _, _).trim()
)
or
exists(string name, int start, int end |
column = TNamedColumn(name) and
start = content.indexOf(name + ":") + name.length() + 1 and
end = getEndOfColumnPosition(start, content) and
expectation = content.substring(start, end).regexpFind(expectationPattern(), _, _).trim()
)
)
) and
tags = expectation.regexpCapture(expectationPattern(), 1) and
if exists(expectation.regexpCapture(expectationPattern(), 2))
then value = expectation.regexpCapture(expectationPattern(), 2)
else value = ""
}
private string getColumnString(TColumn column) {
column = TDefaultColumn() and result = ""
or
column = TNamedColumn(result)
}
/**
* RegEx pattern to match a single expected result, not including the leading `$`. It consists of one or
* more comma-separated tags containing only letters, digits, `-` and `_` (note that the first character
* must not be a digit), optionally followed by `=` and the expected value.
*/
private string expectationPattern() {
exists(string tag, string tags, string value |
tag = "[A-Za-z-_][A-Za-z-_0-9]*" and
tags = "((?:" + tag + ")(?:\\s*,\\s*" + tag + ")*)" and
// In Python, we allow both `"` and `'` for strings, as well as the prefixes `bru`.
// For example, `b"foo"`.
value = "((?:[bru]*\"[^\"]*\"|[bru]*'[^']*'|\\S+)*)" and
result = tags + "(?:=" + value + ")?"
)
}
private newtype TFailureLocatable =
TActualResult(
InlineExpectationsTest test, Location location, string element, string tag, string value,
boolean optional
) {
test.hasActualResult(location, element, tag, value) and
optional = false
or
test.hasOptionalResult(location, element, tag, value) and optional = true
} or
TValidExpectation(ExpectationComment comment, string tag, string value, string knownFailure) {
exists(TColumn column, string tags |
getAnExpectation(comment, column, _, tags, value) and
tag = tags.splitAt(",") and
knownFailure = getColumnString(column)
)
} or
TInvalidExpectation(ExpectationComment comment, string expectation) {
getAnExpectation(comment, _, expectation, _, _) and
not expectation.regexpMatch(expectationPattern())
}
class FailureLocatable extends TFailureLocatable {
string toString() { none() }
Location getLocation() { none() }
final string getExpectationText() { result = getTag() + "=" + getValue() }
string getTag() { none() }
string getValue() { none() }
}
class ActualResult extends FailureLocatable, TActualResult {
InlineExpectationsTest test;
Location location;
string element;
string tag;
string value;
boolean optional;
ActualResult() { this = TActualResult(test, location, element, tag, value, optional) }
override string toString() { result = element }
override Location getLocation() { result = location }
InlineExpectationsTest getTest() { result = test }
override string getTag() { result = tag }
override string getValue() { result = value }
predicate isOptional() { optional = true }
}
abstract private class Expectation extends FailureLocatable {
ExpectationComment comment;
override string toString() { result = comment.toString() }
override Location getLocation() { result = comment.getLocation() }
}
private class ValidExpectation extends Expectation, TValidExpectation {
string tag;
string value;
string knownFailure;
ValidExpectation() { this = TValidExpectation(comment, tag, value, knownFailure) }
override string getTag() { result = tag }
override string getValue() { result = value }
string getKnownFailure() { result = knownFailure }
predicate matchesActualResult(ActualResult actualResult) {
getLocation().getStartLine() = actualResult.getLocation().getStartLine() and
getLocation().getFile() = actualResult.getLocation().getFile() and
getTag() = actualResult.getTag() and
getValue() = actualResult.getValue()
}
}
/* Note: These next three classes correspond to all the possible values of type `TColumn`. */
class GoodExpectation extends ValidExpectation {
GoodExpectation() { getKnownFailure() = "" }
}
class FalsePositiveExpectation extends ValidExpectation {
FalsePositiveExpectation() { getKnownFailure() = "SPURIOUS" }
}
class FalseNegativeExpectation extends ValidExpectation {
FalseNegativeExpectation() { getKnownFailure() = "MISSING" }
}
class InvalidExpectation extends Expectation, TInvalidExpectation {
string expectation;
InvalidExpectation() { this = TInvalidExpectation(comment, expectation) }
string getExpectation() { result = expectation }
}
query predicate failures(FailureLocatable element, string message) {
exists(InlineExpectationsTest test | test.hasFailureMessage(element, message))
}

View File

@@ -0,0 +1,22 @@
import ql
private import codeql_ql.ast.internal.TreeSitter
private newtype TExpectationComment = MkExpectationComment(QL::LineComment comment)
/**
* Represents a line comment.
*/
class ExpectationComment extends TExpectationComment {
QL::LineComment comment;
ExpectationComment() { this = MkExpectationComment(comment) }
/** Returns the contents of the given comment, _without_ the preceding comment marker (`//`). */
string getContents() { result = comment.getValue().suffix(2) }
/** Gets a textual representation of this element. */
string toString() { result = comment.toString() }
/** Gets the location of this comment. */
Location getLocation() { result = comment.getLocation() }
}

View File

@@ -0,0 +1,79 @@
predicate isLeft(string s) { none() }
predicate isRight(string s) { none() }
predicate disjunct() {
exists(string x |
x = "left_1" and
isLeft(x) // $ getAStringValue=left_1
or
x = "right_1" and
isRight(x) // $ getAStringValue=right_1
)
}
string sourceLeft2() { result = "left_2" }
predicate sinkLeft2(string x) {
isLeft(x) // $ getAStringValue=left_2
}
string sourceRight2() { result = "right_2" }
predicate sinkRight2(string x) {
isRight(x) // $ getAStringValue=right_2
}
predicate disjunctViaCall() {
exists(string x |
x = sourceLeft2() and
sinkLeft2(x) // $ getAStringValue=left_2
or
x = sourceRight2() and
sinkRight2(x) // $ getAStringValue=right_2
)
}
predicate distribute() {
exists(string x |
x = "left_3"
or
x = "right_3"
|
isLeft(x) // $ getAStringValue=left_3 getAStringValue=right_3
or
isRight(x) // $ getAStringValue=left_3 getAStringValue=right_3
)
}
predicate distributeSet() {
exists(string x | x = ["left_4", "right_4"] |
isLeft(x) // $ getAStringValue=left_4 getAStringValue=right_4
or
isRight(x) // $ getAStringValue=left_4 getAStringValue=right_4
)
}
predicate noFlowBackOut() {
exists(string x |
isLeft(x) // no value
)
}
class StringClass extends string {
StringClass() { this = "StringClass" }
}
class FieldClass extends int {
StringClass field;
FieldClass() { this = 0 }
StringClass getField() { result = field }
}
predicate isStringClass(string s) { none() }
predicate test() {
isStringClass(any(FieldClass f).getField()) // $ getAStringValue=StringClass
}

View File

@@ -0,0 +1,19 @@
import ql
import codeql_ql.dataflow.DataFlow
import TestUtilities.InlineExpectationsTest
class GetAStringValueTest extends InlineExpectationsTest {
GetAStringValueTest() { this = "getAStringValue" }
override string getARelevantTag() { result = "getAStringValue" }
override predicate hasActualResult(Location location, string element, string tag, string value) {
exists(Expr e |
e = any(Call c).getAnArgument() and
tag = "getAStringValue" and
value = superNode(e).getAStringValue() and
location = e.getLocation() and
element = e.toString()
)
}
}

View File

@@ -265,8 +265,6 @@ edges
| Foo.qll:6:23:6:36 | ComparisonFormula | Foo.qll:6:23:6:28 | result | semmle.order | 11 |
| Foo.qll:6:23:6:36 | ComparisonFormula | Foo.qll:6:32:6:36 | String | semmle.label | getRightOperand() |
| Foo.qll:6:23:6:36 | ComparisonFormula | Foo.qll:6:32:6:36 | String | semmle.order | 13 |
| Foo.qll:9:1:9:5 | annotation | Foo.qll:9:1:9:5 | annotation | semmle.label | getAnAnnotation() |
| Foo.qll:9:1:9:5 | annotation | Foo.qll:9:1:9:5 | annotation | semmle.order | 14 |
| Foo.qll:9:7:11:1 | ClasslessPredicate foo | Foo.qll:9:1:9:5 | annotation | semmle.label | getAnAnnotation() |
| Foo.qll:9:7:11:1 | ClasslessPredicate foo | Foo.qll:9:1:9:5 | annotation | semmle.order | 14 |
| Foo.qll:9:7:11:1 | ClasslessPredicate foo | Foo.qll:9:21:9:25 | f | semmle.label | getParameter(_) |

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -5047,6 +5047,7 @@ private module FlowExploration {
)
}
pragma[nomagic]
private predicate revPartialPathStep(
PartialPathNodeRev mid, NodeEx node, FlowState state, TRevSummaryCtx1 sc1, TRevSummaryCtx2 sc2,
TRevSummaryCtx3 sc3, RevPartialAccessPath ap, Configuration config

View File

@@ -820,24 +820,13 @@ string ppReprType(DataFlowType t) { result = t.toString() }
pragma[inline]
predicate compatibleTypes(DataFlowType t1, DataFlowType t2) { any() }
/**
* A node associated with an object after an operation that might have
* changed its state.
*
* This can be either the argument to a callable after the callable returns
* (which might have mutated the argument), or the qualifier of a field after
* an update to the field.
*
* Nodes corresponding to AST elements, for example `ExprNode`, usually refer
* to the value before the update.
*/
abstract class PostUpdateNode extends Node {
abstract class PostUpdateNodeImpl extends Node {
/** Gets the node before the state update. */
abstract Node getPreUpdateNode();
}
private module PostUpdateNodes {
class ExprPostUpdateNode extends PostUpdateNode, NodeImpl, TExprPostUpdateNode {
class ExprPostUpdateNode extends PostUpdateNodeImpl, NodeImpl, TExprPostUpdateNode {
private CfgNodes::ExprCfgNode e;
ExprPostUpdateNode() { this = TExprPostUpdateNode(e) }
@@ -851,7 +840,7 @@ private module PostUpdateNodes {
override string toStringImpl() { result = "[post] " + e.toString() }
}
private class SummaryPostUpdateNode extends SummaryNode, PostUpdateNode {
private class SummaryPostUpdateNode extends SummaryNode, PostUpdateNodeImpl {
private Node pre;
SummaryPostUpdateNode() { FlowSummaryImpl::Private::summaryPostUpdateNode(this, pre) }

View File

@@ -132,6 +132,22 @@ class LocalSourceNode extends Node {
LocalSourceNode backtrack(TypeBackTracker t2, TypeBackTracker t) { t2 = t.step(result, this) }
}
/**
* A node associated with an object after an operation that might have
* changed its state.
*
* This can be either the argument to a callable after the callable returns
* (which might have mutated the argument), or the qualifier of a field after
* an update to the field.
*
* Nodes corresponding to AST elements, for example `ExprNode`, usually refer
* to the value before the update.
*/
class PostUpdateNode extends Node instanceof PostUpdateNodeImpl {
/** Gets the node before the state update. */
Node getPreUpdateNode() { result = super.getPreUpdateNode() }
}
cached
private predicate hasLocalSource(Node sink, Node source) {
// Declaring `source` to be a `SourceNode` currently causes a redundant check in the

View File

@@ -0,0 +1,91 @@
/**
* Provides predicates for reasoning about insecure dependency configurations.
*/
private import ruby
/**
* A method call in a Gemfile.
*/
private class GemfileMethodCall extends MethodCall {
GemfileMethodCall() { this.getLocation().getFile().getBaseName() = "Gemfile" }
}
/**
* Method calls that configure gem dependencies and can specify (possibly insecure) URLs.
*/
abstract private class RelevantGemCall extends GemfileMethodCall {
abstract Expr getAUrlPart();
}
/**
* A call to `source`.
*/
private class SourceCall extends RelevantGemCall {
SourceCall() { this.getMethodName() = "source" }
override Expr getAUrlPart() { result = this.getAnArgument() }
}
/**
* A call to `git_source`.
*/
private class GitSourceCall extends RelevantGemCall {
GitSourceCall() { this.getMethodName() = "git_source" }
override Expr getAUrlPart() { result = this.getBlock().getLastStmt() }
}
/**
* A call to `gem`.
*/
private class GemCall extends RelevantGemCall {
GemCall() { this.getMethodName() = "gem" }
override Expr getAUrlPart() { result = this.getKeywordArgument(["source", "git"]) }
}
/**
* Holds if `s` is a URL with an insecure protocol. `proto` is the protocol.
*/
bindingset[s]
private predicate hasInsecureProtocol(string s, string proto) {
proto = s.regexpCapture("^(http|ftp):.+", 1).toUpperCase()
}
/**
* Holds if `e` is a string containing a URL that uses the insecure protocol `proto`.
*/
private predicate containsInsecureUrl(Expr e, string proto) {
// Handle cases where the string as a whole has no constant value (due to interpolations)
// but has a known prefix. E.g. "http://#{foo}"
exists(StringComponent c | c = e.(StringlikeLiteral).getComponent(0) |
hasInsecureProtocol(c.getConstantValue().getString(), proto)
)
or
hasInsecureProtocol(e.getConstantValue().getString(), proto)
}
/**
* Returns the suggested protocol to use in place of the insecure protocol `proto`.
*/
bindingset[proto]
private string suggestedProtocol(string proto) {
proto = "HTTP" and result = "HTTPS"
or
proto = "FTP" and result = "FTPS or SFTP"
}
/**
* Holds if `url` is a string containing a URL that uses an insecure protocol.
* `msg` is the alert message that will be displayed to the user.
*/
predicate insecureDependencyUrl(Expr url, string msg) {
exists(RelevantGemCall call, string proto |
url = call.getAUrlPart() and
containsInsecureUrl(url, proto) and
msg =
"Dependency source URL uses the unencrypted protocol " + proto + ". Use " +
suggestedProtocol(proto) + " instead."
)
}

View File

@@ -147,7 +147,7 @@ predicate basicStoreStep(Node nodeFrom, Node nodeTo, string content) {
// TODO: support SetterMethodCall inside TuplePattern
exists(ExprNodes::MethodCallCfgNode call |
content = getSetterCallAttributeName(call.getExpr()) and
nodeTo.(DataFlowPrivate::PostUpdateNode).getPreUpdateNode().asExpr() = call.getReceiver() and
nodeTo.(DataFlowPublic::PostUpdateNode).getPreUpdateNode().asExpr() = call.getReceiver() and
call.getExpr() instanceof AST::SetterMethodCall and
call.getArgument(call.getNumberOfArguments() - 1) =
nodeFrom.(DataFlowPublic::ExprNode).getExprNode()

View File

@@ -0,0 +1,4 @@
---
category: newQuery
---
* Added a new query, `rb/insecure-dependency`. The query finds cases where Ruby gems may be downloaded over an insecure communication channel.

View File

@@ -0,0 +1,54 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Using an insecure protocol like HTTP or FTP to download dependencies makes the build process vulnerable to a
man-in-the-middle (MITM) attack.
</p>
<p>
This can allow attackers to inject malicious code into the downloaded dependencies, and thereby
infect the build artifacts and execute arbitrary code on the machine building the artifacts.
</p>
</overview>
<recommendation>
<p>Always use a secure protocol, such as HTTPS or SFTP, when downloading artifacts from a URL.</p>
</recommendation>
<example>
<p>
The below example shows a <code>Gemfile</code> that specifies a gem source using the insecure HTTP protocol.
</p>
<sample src="examples/bad_gemfile.rb" />
<p>
The fix is to change the protocol to HTTPS.
</p>
<sample src="examples/good_gemfile.rb" />
</example>
<references>
<li>
Jonathan Leitschuh:
<a href="https://infosecwriteups.com/want-to-take-over-the-java-ecosystem-all-you-need-is-a-mitm-1fc329d898fb">
Want to take over the Java ecosystem? All you need is a MITM!
</a>
</li>
<li>
Max Veytsman:
<a href="https://max.computer/blog/how-to-take-over-the-computer-of-any-java-or-clojure-or-scala-developer/">
How to take over the computer of any Java (or Clojure or Scala) Developer.
</a>
</li>
<li>
Wikipedia: <a href="https://en.wikipedia.org/wiki/Supply_chain_attack">Supply chain attack.</a>
</li>
<li>
Wikipedia: <a href="https://en.wikipedia.org/wiki/Man-in-the-middle_attack">Man-in-the-middle attack.</a>
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,22 @@
/**
* @name Dependency download using unencrypted communication channel
* @description Using unencrypted protocols to fetch dependencies can leave an application
* open to man-in-the-middle attacks.
* @kind problem
* @problem.severity warning
* @security-severity 8.1
* @precision high
* @id rb/insecure-dependency
* @tags security
* external/cwe/cwe-300
* external/cwe/cwe-319
* external/cwe/cwe-494
* external/cwe/cwe-829
*/
import ruby
import codeql.ruby.security.InsecureDependencyQuery
from Expr url, string msg
where insecureDependencyUrl(url, msg)
select url, msg

View File

@@ -0,0 +1,3 @@
source "http://rubygems.org"
gem "my-gem-a", "1.2.3"

View File

@@ -0,0 +1,3 @@
source "https://rubygems.org"
gem "my-gem-a", "1.2.3"

View File

@@ -0,0 +1,56 @@
source "https://rubygems.org" # GOOD
source "http://rubygems.org" # $result=BAD
source "ftp://rubygems.org" # $result=BAD
source "ftps://rubygems.org" # GOOD
source "unknown://rubygems.org" # GOOD
git_source(:a) { "https://github.com" } # GOOD
git_source(:b) { "http://github.com" } # $result=BAD
git_source(:c) { "ftp://github.com" } # $result=BAD
git_source(:d) { "ftps://github.com" } # GOOD
git_source(:e) { "unknown://github.com" } # GOOD
git_source(:f) { |name| "https://github.com/#{name}" } # GOOD
git_source(:g) { |name| "http://github.com/#{name}" } # $result=BAD
git_source(:h) { |name| "ftp://github.com/#{name}" } # $result=BAD
git_source(:i) { |name| "ftps://github.com/#{name}" } # GOOD
git_source(:j) { |name| "unknown://github.com/#{name}" } # GOOD
git_source(:k) do |name|
foo
"https://github.com/#{name}" } # GOOD
end
git_source(:l) do |name|
foo
"http://github.com/#{name}" } # $result=BAD
end
git_source(:m) do |name|
foo
"ftp://github.com/#{name}" } # $result=BAD
end
git_source(:n) do |name|
foo
"ftps://github.com/#{name}" } # GOOD
end
git_source(:o) do |name|
foo
"unknown://github.com/#{name}" } # GOOD
end
gem "jwt", "1.2.3", git: "https://github.com/jwt/ruby-jwt" # GOOD
gem "jwt", "1.2.3", git: "http://github.com/jwt/ruby-jwt" # $result=BAD
gem "jwt", "1.2.3", git: "ftp://github.com/jwt/ruby-jwt" # $result=BAD
gem "jwt", "1.2.3", git: "ftps://github.com/jwt/ruby-jwt" # GOOD
gem "jwt", "1.2.3", git: "unknown://github.com/jwt/ruby-jwt" # GOOD
gem "jwt", "1.2.3", :git => "https://github.com/jwt/ruby-jwt" # GOOD
gem "jwt", "1.2.3", :git => "http://github.com/jwt/ruby-jwt" # $result=BAD
gem "jwt", "1.2.3", :git => "ftp://github.com/jwt/ruby-jwt" # $result=BAD
gem "jwt", "1.2.3", :git => "ftps://github.com/jwt/ruby-jwt" # GOOD
gem "jwt", "1.2.3", :git => "unknown://github.com/jwt/ruby-jwt" # GOOD
gem "jwt", "1.2.3", source: "https://rubygems.org" # GOOD
gem "jwt", "1.2.3", source: "http://rubygems.org" # $result=BAD
gem "jwt", "1.2.3", source: "ftp://rubygems.org" # $result=BAD
gem "jwt", "1.2.3", source: "ftps://rubygems.org" # GOOD
gem "jwt", "1.2.3", source: "unknown://rubygems.org" # GOOD

View File

@@ -0,0 +1,16 @@
failures
#select
| Gemfile:2:8:2:28 | "http://rubygems.org" | Dependency source URL uses the unencrypted protocol HTTP. Use HTTPS instead. |
| Gemfile:3:8:3:27 | "ftp://rubygems.org" | Dependency source URL uses the unencrypted protocol FTP. Use FTPS or SFTP instead. |
| Gemfile:8:18:8:36 | "http://github.com" | Dependency source URL uses the unencrypted protocol HTTP. Use HTTPS instead. |
| Gemfile:9:18:9:35 | "ftp://github.com" | Dependency source URL uses the unencrypted protocol FTP. Use FTPS or SFTP instead. |
| Gemfile:14:25:14:51 | "http://github.com/#{...}" | Dependency source URL uses the unencrypted protocol HTTP. Use HTTPS instead. |
| Gemfile:15:25:15:50 | "ftp://github.com/#{...}" | Dependency source URL uses the unencrypted protocol FTP. Use FTPS or SFTP instead. |
| Gemfile:25:5:25:31 | "http://github.com/#{...}" | Dependency source URL uses the unencrypted protocol HTTP. Use HTTPS instead. |
| Gemfile:29:5:29:30 | "ftp://github.com/#{...}" | Dependency source URL uses the unencrypted protocol FTP. Use FTPS or SFTP instead. |
| Gemfile:41:26:41:57 | "http://github.com/jwt/ruby-jwt" | Dependency source URL uses the unencrypted protocol HTTP. Use HTTPS instead. |
| Gemfile:42:26:42:56 | "ftp://github.com/jwt/ruby-jwt" | Dependency source URL uses the unencrypted protocol FTP. Use FTPS or SFTP instead. |
| Gemfile:47:29:47:60 | "http://github.com/jwt/ruby-jwt" | Dependency source URL uses the unencrypted protocol HTTP. Use HTTPS instead. |
| Gemfile:48:29:48:59 | "ftp://github.com/jwt/ruby-jwt" | Dependency source URL uses the unencrypted protocol FTP. Use FTPS or SFTP instead. |
| Gemfile:53:29:53:49 | "http://rubygems.org" | Dependency source URL uses the unencrypted protocol HTTP. Use HTTPS instead. |
| Gemfile:54:29:54:48 | "ftp://rubygems.org" | Dependency source URL uses the unencrypted protocol FTP. Use FTPS or SFTP instead. |

View File

@@ -0,0 +1,23 @@
import ruby
import TestUtilities.InlineExpectationsTest
import codeql.ruby.security.InsecureDependencyQuery
class InsecureDependencyTest extends InlineExpectationsTest {
InsecureDependencyTest() { this = "InsecureDependencyTest" }
override string getARelevantTag() { result = "BAD" }
override predicate hasActualResult(Location location, string element, string tag, string value) {
tag = "result" and
value = "BAD" and
exists(Expr e |
insecureDependencyUrl(e, _) and
location = e.getLocation() and
element = e.toString()
)
}
}
from Expr url, string msg
where insecureDependencyUrl(url, msg)
select url, msg

View File

@@ -0,0 +1,5 @@
# Calls to `gem` etc. outside of the Gemfile should be ignored, since they may not be configuring dependencies.
gem "foo", git: "http://foo.com"
git_source :a { |x| "http://foo.com" }
source "http://foo.com"