Merge branch 'main' into rdmarsh/cpp/use-taint-configuration-dtt

This commit is contained in:
Mathias Vorreiter Pedersen
2021-03-15 09:42:32 +01:00
36 changed files with 826 additions and 42 deletions

View File

@@ -38,7 +38,7 @@ If you have an idea for a query that you would like to share with other CodeQL u
- The queries and libraries must be autoformatted, for example using the "Format Document" command in [CodeQL for Visual Studio Code](https://help.semmle.com/codeql/codeql-for-vscode/procedures/about-codeql-for-vscode.html).
If you prefer, you can use this [pre-commit hook](misc/scripts/pre-commit) that automatically checks whether your files are correctly formatted. See the [pre-commit hook installation guide](docs/install-pre-commit-hook.md) for instructions on how to install the hook.
If you prefer, you can use this [pre-commit hook](misc/scripts/pre-commit) that automatically checks whether your files are correctly formatted. See the [pre-commit hook installation guide](docs/pre-commit-hook-setup.md) for instructions on how to install the hook.
4. **Compilation**

View File

@@ -0,0 +1,2 @@
codescanning
* Added cpp/diagnostics/failed-extractions. This query gives information about which extractions did not run to completion.

View File

@@ -0,0 +1,22 @@
/**
* @name Failed extractions
* @description Gives the command-line of compilations for which extraction did not run to completion.
* @kind diagnostic
* @id cpp/diagnostics/failed-extractions
*/
import cpp
class AnonymousCompilation extends Compilation {
override string toString() { result = "<compilation>" }
}
string describe(Compilation c) {
if c.getArgument(1) = "--mimic"
then result = "compiler invocation " + concat(int i | i > 1 | c.getArgument(i), " " order by i)
else result = "extractor invocation " + concat(int i | | c.getArgument(i), " " order by i)
}
from Compilation c
where not c.normalTermination()
select c, "Extraction failed for " + describe(c), 2

View File

@@ -276,7 +276,10 @@ class File extends Container, @file {
c.getAFileCompiled() = this and
(
c.getAnArgument() = "--microsoft" or
c.getAnArgument().toLowerCase().replaceAll("\\", "/").matches("%/cl.exe")
c.getAnArgument()
.toLowerCase()
.replaceAll("\\", "/")
.matches(["%/cl.exe", "%/clang-cl.exe"])
)
)
or

View File

@@ -12,7 +12,7 @@ This topic offers some simple tips on how to avoid common problems that can affe
Before reading the tips below, it is worth reiterating a few important points about CodeQL and the QL language:
- CodeQL :ref:`predicates <predicates>` and :ref:`classes <classes>` are evaluated to database `tables <https://en.wikipedia.org/wiki/Table_(database)>`__. Large predicates generate large tables with many rows, and are therefore expensive to compute.
- The QL language is implemented using standard database operations and `relational algebra <https://en.wikipedia.org/wiki/Relational_algebra>`__ (such as join, projection, and union). For more information about query languages and databases, see ":ref:`About the QL language <about-the-ql-language>`.
- The QL language is implemented using standard database operations and `relational algebra <https://en.wikipedia.org/wiki/Relational_algebra>`__ (such as join, projection, and union). For more information about query languages and databases, see ":ref:`About the QL language <about-the-ql-language>`."
- Queries are evaluated *bottom-up*, which means that a predicate is not evaluated until *all* of the predicates that it depends on are evaluated. For more information on query evaluation, see ":ref:`Evaluation of QL programs <evaluation-of-ql-programs>`."
Performance tips

View File

@@ -0,0 +1,2 @@
lgtm,codescanning
* Added models for `ObjectUtils` methods in the Apache Commons Lang library. This may lead to more results from any dataflow query where traversal of `ObjectUtils` methods means we can now complete a path from a source of tainted data to a corresponding sink.

View File

@@ -144,7 +144,7 @@ class OpensDirective extends Directive, @opens {
/**
* Gets a module specified in the `to` clause of this
* `exports` directive, if any.
* `opens` directive, if any.
*/
Module getATargetModule() { opensTo(this, result) }

View File

@@ -46,7 +46,8 @@ predicate localAdditionalTaintStep(DataFlow::Node src, DataFlow::Node sink) {
localAdditionalTaintUpdateStep(src.asExpr(),
sink.(DataFlow::PostUpdateNode).getPreUpdateNode().asExpr())
or
summaryStep(src, sink, "taint")
summaryStep(src, sink, "taint") and
not summaryStep(src, sink, "value")
or
exists(Argument arg |
src.asExpr() = arg and

View File

@@ -396,3 +396,30 @@ private class ApacheRegExUtilsModel extends SummaryModelCsv {
]
}
}
/**
* Taint-propagating models for `ObjectUtils`.
*/
private class ApacheObjectUtilsModel extends SummaryModelCsv {
override predicate row(string row) {
row =
[
// Note all the functions annotated with `taint` flow really should have `value` flow,
// but we don't support value-preserving varargs functions at the moment.
"org.apache.commons.lang3;ObjectUtils;false;clone;;;Argument;ReturnValue;value",
"org.apache.commons.lang3;ObjectUtils;false;cloneIfPossible;;;Argument;ReturnValue;value",
"org.apache.commons.lang3;ObjectUtils;false;CONST;;;Argument;ReturnValue;value",
"org.apache.commons.lang3;ObjectUtils;false;CONST_BYTE;;;Argument;ReturnValue;value",
"org.apache.commons.lang3;ObjectUtils;false;CONST_SHORT;;;Argument;ReturnValue;value",
"org.apache.commons.lang3;ObjectUtils;false;defaultIfNull;;;Argument;ReturnValue;value",
"org.apache.commons.lang3;ObjectUtils;false;firstNonNull;;;Argument;ReturnValue;taint",
"org.apache.commons.lang3;ObjectUtils;false;getIfNull;;;Argument[0];ReturnValue;value",
"org.apache.commons.lang3;ObjectUtils;false;max;;;Argument;ReturnValue;taint",
"org.apache.commons.lang3;ObjectUtils;false;median;;;Argument;ReturnValue;taint",
"org.apache.commons.lang3;ObjectUtils;false;min;;;Argument;ReturnValue;taint",
"org.apache.commons.lang3;ObjectUtils;false;mode;;;Argument;ReturnValue;taint",
"org.apache.commons.lang3;ObjectUtils;false;requireNonEmpty;;;Argument[0];ReturnValue;value",
"org.apache.commons.lang3;ObjectUtils;false;toString;(Object,String);;Argument[1];ReturnValue;value"
]
}
}

View File

@@ -0,0 +1,41 @@
import org.apache.commons.lang3.ObjectUtils;
public class ObjectUtilsTest {
String taint() { return "tainted"; }
private static class IntSource {
static int taint() { return 0; }
}
void sink(Object o) {}
void test() throws Exception {
sink(ObjectUtils.clone(taint())); // $hasValueFlow
sink(ObjectUtils.cloneIfPossible(taint())); // $hasValueFlow
sink(ObjectUtils.CONST(taint())); // $hasValueFlow
sink(ObjectUtils.CONST_SHORT(IntSource.taint())); // $hasValueFlow
sink(ObjectUtils.CONST_BYTE(IntSource.taint())); // $hasValueFlow
sink(ObjectUtils.defaultIfNull(taint(), null)); // $hasValueFlow
sink(ObjectUtils.defaultIfNull(null, taint())); // $hasValueFlow
sink(ObjectUtils.firstNonNull(taint(), null, null)); // $hasTaintFlow $MISSING:hasValueFlow
sink(ObjectUtils.firstNonNull(null, taint(), null)); // $hasTaintFlow $MISSING:hasValueFlow
sink(ObjectUtils.firstNonNull(null, null, taint())); // $hasTaintFlow $MISSING:hasValueFlow
sink(ObjectUtils.getIfNull(taint(), null)); // $hasValueFlow
sink(ObjectUtils.max(taint(), null, null)); // $hasTaintFlow $MISSING:hasValueFlow
sink(ObjectUtils.max(null, taint(), null)); // $hasTaintFlow $MISSING:hasValueFlow
sink(ObjectUtils.max(null, null, taint())); // $hasTaintFlow $MISSING:hasValueFlow
sink(ObjectUtils.median(taint(), null, null)); // $hasTaintFlow $MISSING:hasValueFlow
sink(ObjectUtils.median((String)null, taint(), null)); // $hasTaintFlow $MISSING:hasValueFlow
sink(ObjectUtils.median((String)null, null, taint())); // $hasTaintFlow $MISSING:hasValueFlow
sink(ObjectUtils.min(taint(), null, null)); // $hasTaintFlow $MISSING:hasValueFlow
sink(ObjectUtils.min(null, taint(), null)); // $hasTaintFlow $MISSING:hasValueFlow
sink(ObjectUtils.min(null, null, taint())); // $hasTaintFlow $MISSING:hasValueFlow
sink(ObjectUtils.mode(taint(), null, null)); // $hasTaintFlow $MISSING:hasValueFlow
sink(ObjectUtils.mode(null, taint(), null)); // $hasTaintFlow $MISSING:hasValueFlow
sink(ObjectUtils.mode(null, null, taint())); // $hasTaintFlow $MISSING:hasValueFlow
sink(ObjectUtils.requireNonEmpty(taint(), "message")); // $hasValueFlow
sink(ObjectUtils.requireNonEmpty("not null", taint())); // GOOD (message doesn't propagate to the return)
sink(ObjectUtils.toString(taint(), "default string")); // GOOD (first argument is stringified)
sink(ObjectUtils.toString(null, taint())); // $hasValueFlow
}
}

View File

@@ -2,8 +2,20 @@ import java
import semmle.code.java.dataflow.TaintTracking
import TestUtilities.InlineExpectationsTest
class Conf extends TaintTracking::Configuration {
Conf() { this = "qltest:frameworks:apache-commons-lang3" }
class TaintFlowConf extends TaintTracking::Configuration {
TaintFlowConf() { this = "qltest:frameworks:apache-commons-lang3-taint-flow" }
override predicate isSource(DataFlow::Node n) {
n.asExpr().(MethodAccess).getMethod().hasName("taint")
}
override predicate isSink(DataFlow::Node n) {
exists(MethodAccess ma | ma.getMethod().hasName("sink") | n.asExpr() = ma.getAnArgument())
}
}
class ValueFlowConf extends DataFlow::Configuration {
ValueFlowConf() { this = "qltest:frameworks:apache-commons-lang3-value-flow" }
override predicate isSource(DataFlow::Node n) {
n.asExpr().(MethodAccess).getMethod().hasName("taint")
@@ -17,11 +29,19 @@ class Conf extends TaintTracking::Configuration {
class HasFlowTest extends InlineExpectationsTest {
HasFlowTest() { this = "HasFlowTest" }
override string getARelevantTag() { result = "hasTaintFlow" }
override string getARelevantTag() { result = ["hasTaintFlow", "hasValueFlow"] }
override predicate hasActualResult(Location location, string element, string tag, string value) {
tag = "hasTaintFlow" and
exists(DataFlow::Node src, DataFlow::Node sink, Conf conf | conf.hasFlow(src, sink) |
exists(DataFlow::Node src, DataFlow::Node sink, TaintFlowConf conf | conf.hasFlow(src, sink) |
not any(ValueFlowConf vconf).hasFlow(src, sink) and
sink.getLocation() = location and
element = sink.toString() and
value = ""
)
or
tag = "hasValueFlow" and
exists(DataFlow::Node src, DataFlow::Node sink, ValueFlowConf conf | conf.hasFlow(src, sink) |
sink.getLocation() = location and
element = sink.toString() and
value = ""

View File

@@ -0,0 +1,221 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.lang3;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.TreeSet;
import java.util.function.Supplier;
import org.apache.commons.lang3.text.StrBuilder;
@SuppressWarnings("deprecation") // deprecated class StrBuilder is imported
// because it is part of the signature of deprecated methods
public class ObjectUtils {
public static class Null implements Serializable {
}
public static final Null NULL = new Null();
public static boolean allNotNull(final Object... values) {
return false;
}
public static boolean allNull(final Object... values) {
return false;
}
public static boolean anyNotNull(final Object... values) {
return false;
}
public static boolean anyNull(final Object... values) {
return false;
}
public static <T> T clone(final T obj) {
return null;
}
public static <T> T cloneIfPossible(final T obj) {
return null;
}
public static <T extends Comparable<? super T>> int compare(final T c1, final T c2) {
return 0;
}
public static <T extends Comparable<? super T>> int compare(final T c1, final T c2, final boolean nullGreater) {
return 0;
}
public static boolean CONST(final boolean v) {
return false;
}
public static byte CONST(final byte v) {
return 0;
}
public static char CONST(final char v) {
return '\0';
}
public static double CONST(final double v) {
return 0;
}
public static float CONST(final float v) {
return 0;
}
public static int CONST(final int v) {
return 0;
}
public static long CONST(final long v) {
return 0;
}
public static short CONST(final short v) {
return 0;
}
public static <T> T CONST(final T v) {
return null;
}
public static byte CONST_BYTE(final int v) {
return 0;
}
public static short CONST_SHORT(final int v) {
return 0;
}
public static <T> T defaultIfNull(final T object, final T defaultValue) {
return null;
}
public static boolean equals(final Object object1, final Object object2) {
return false;
}
public static <T> T firstNonNull(final T... values) {
return null;
}
public static <T> T getFirstNonNull(final Supplier<T>... suppliers) {
return null;
}
public static <T> T getIfNull(final T object, final Supplier<T> defaultSupplier) {
return null;
}
public static int hashCode(final Object obj) {
return 0;
}
public static int hashCodeMulti(final Object... objects) {
return 0;
}
public static void identityToString(final Appendable appendable, final Object object) throws IOException {
}
public static String identityToString(final Object object) {
return null;
}
public static void identityToString(final StrBuilder builder, final Object object) {
}
public static void identityToString(final StringBuffer buffer, final Object object) {
}
public static void identityToString(final StringBuilder builder, final Object object) {
}
public static boolean isEmpty(final Object object) {
return false;
}
public static boolean isNotEmpty(final Object object) {
return false;
}
public static <T extends Comparable<? super T>> T max(final T... values) {
return null;
}
public static <T> T median(final Comparator<T> comparator, final T... items) {
return null;
}
public static <T extends Comparable<? super T>> T median(final T... items) {
return null;
}
public static <T extends Comparable<? super T>> T min(final T... values) {
return null;
}
public static <T> T mode(final T... items) {
return null;
}
public static boolean notEqual(final Object object1, final Object object2) {
return false;
}
public static <T> T requireNonEmpty(final T obj) {
return null;
}
public static <T> T requireNonEmpty(final T obj, final String message) {
return null;
}
public static String toString(final Object obj) {
return null;
}
public static String toString(final Object obj, final String nullStr) {
return null;
}
public static String toString(final Object obj, final Supplier<String> supplier) {
return null;
}
public static void wait(final Object obj, final Duration duration) throws InterruptedException {
}
public ObjectUtils() {
}
}

View File

@@ -0,0 +1,6 @@
lgtm,codescanning
* Support for `d3` has improved. The XSS queries now recognize HTML injection sinks
from the `d3` API.
Affected packages are
[d3](https://npmjs.com/package/d3),
[d3-selection](https://npmjs.com/package/d3-selection).

View File

@@ -80,6 +80,7 @@ import semmle.javascript.frameworks.ClosureLibrary
import semmle.javascript.frameworks.CookieLibraries
import semmle.javascript.frameworks.Credentials
import semmle.javascript.frameworks.CryptoLibraries
import semmle.javascript.frameworks.D3
import semmle.javascript.frameworks.DateFunctions
import semmle.javascript.frameworks.DigitalOcean
import semmle.javascript.frameworks.Electron

View File

@@ -0,0 +1,102 @@
/** Provides classes and predicates modelling aspects of the `d3` library. */
private import javascript
private import semmle.javascript.security.dataflow.Xss
/** Provides classes and predicates modelling aspects of the `d3` library. */
module D3 {
/** The global variable `d3` as an entry point for API graphs. */
private class D3GlobalEntry extends API::EntryPoint {
D3GlobalEntry() { this = "D3GlobalEntry" }
override DataFlow::SourceNode getAUse() { result = DataFlow::globalVarRef("d3") }
override DataFlow::Node getARhs() { none() }
}
/** Gets an API node referring to the `d3` module. */
API::Node d3() {
result = API::moduleImport("d3")
or
// recognize copies of d3 in a scope
result = API::moduleImport(any(string s | s.regexpMatch("@.*/d3(-\\w+)?")))
or
result = API::moduleImport("d3-node").getInstance().getMember("d3")
or
result = API::root().getASuccessor(any(D3GlobalEntry i))
}
/**
* Gets an API node referring to the given module, or to `d3`.
*
* Useful for accessing modules that are re-exported by `d3`.
*/
bindingset[name]
API::Node d3Module(string name) {
result = d3() // all d3 modules are re-exported as part of 'd3'
or
result = API::moduleImport(name)
}
/** Gets an API node referring to a `d3` selection object, such as `d3.selection()`. */
API::Node d3Selection() {
result = d3Module("d3-selection").getMember(["selection", "select", "selectAll"]).getReturn()
or
exists(API::CallNode call, string name |
call = d3Selection().getMember(name).getACall() and
result = call.getReturn()
|
name =
[
"select", "selectAll", "filter", "merge", "selectChild", "selectChildren", "selection",
"insert", "remove", "clone", "sort", "order", "raise", "lower", "append", "data", "join",
"enter", "exit", "call"
]
or
name = ["text", "html", "datum"] and
call.getNumArgument() > 0 // exclude 0-argument version, which returns the current value
or
name = ["attr", "classed", "style", "property", "on"] and
call.getNumArgument() > 1 // exclude 1-argument version, which returns the current value
or
// Setting multiple things at once
name = ["attr", "classed", "style", "property", "on"] and
call.getArgument(0).getALocalSource() instanceof DataFlow::ObjectLiteralNode
)
or
result = d3Selection().getMember("call").getParameter(0).getParameter(0)
}
private class D3XssSink extends DomBasedXss::Sink {
D3XssSink() {
exists(API::Node htmlArg |
htmlArg = d3Selection().getMember("html").getParameter(0) and
this = [htmlArg, htmlArg.getReturn()].getARhs()
)
}
}
private class D3DomValueSource extends DOM::DomValueSource::Range {
D3DomValueSource() {
this = d3Selection().getMember("each").getReceiver().getAnImmediateUse()
or
this = d3Selection().getMember("node").getReturn().getAnImmediateUse()
or
this = d3Selection().getMember("nodes").getReturn().getUnknownMember().getAnImmediateUse()
}
}
private class D3AttributeDefinition extends DOM::AttributeDefinition {
DataFlow::CallNode call;
D3AttributeDefinition() {
call = d3Selection().getMember("attr").getACall() and
call.getNumArgument() > 1 and
this = call.asExpr()
}
override string getName() { result = call.getArgument(0).getStringValue() }
override DataFlow::Node getValueNode() { result = call.getArgument(1) }
}
}

View File

@@ -479,8 +479,8 @@ module JQuery {
// either a reference to a global variable `$` or `jQuery`
this = DataFlow::globalVarRef(any(string jq | jq = "$" or jq = "jQuery"))
or
// or imported from a module named `jquery`
this = DataFlow::moduleImport("jquery")
// or imported from a module named `jquery` or `zepto`
this = DataFlow::moduleImport(["jquery", "zepto"])
or
this.hasUnderlyingType("JQueryStatic")
}

View File

@@ -27,6 +27,11 @@ module XssThroughDom {
result = ["name", "value", "title", "alt"]
}
/**
* Gets a DOM property name that could store user-controlled data.
*/
string unsafeDomPropertyName() { result = ["innerText", "textContent", "value", "name"] }
/**
* A source for text from the DOM from a JQuery method call.
*/
@@ -35,10 +40,16 @@ module XssThroughDom {
(
this.getMethodName() = ["text", "val"] and this.getNumArgument() = 0
or
this.getMethodName() = "attr" and
this.getNumArgument() = 1 and
forex(InferredType t | t = this.getArgument(0).analyze().getAType() | t = TTString()) and
this.getArgument(0).mayHaveStringValue(unsafeAttributeName())
exists(string methodName, string value |
this.getMethodName() = methodName and
this.getNumArgument() = 1 and
forex(InferredType t | t = this.getArgument(0).analyze().getAType() | t = TTString()) and
this.getArgument(0).mayHaveStringValue(value)
|
methodName = "attr" and value = unsafeAttributeName()
or
methodName = "prop" and value = unsafeDomPropertyName()
)
) and
// looks like a $("<p>" + ... ) source, which is benign for this query.
not exists(DataFlow::Node prefix |
@@ -51,6 +62,29 @@ module XssThroughDom {
}
}
/**
* A source for text from the DOM from a `d3` method call.
*/
class D3TextSource extends Source {
D3TextSource() {
exists(DataFlow::MethodCallNode call, string methodName |
this = call and
call = D3::d3Selection().getMember(methodName).getACall()
|
methodName = "attr" and
call.getNumArgument() = 1 and
call.getArgument(0).mayHaveStringValue(unsafeAttributeName())
or
methodName = "property" and
call.getNumArgument() = 1 and
call.getArgument(0).mayHaveStringValue(unsafeDomPropertyName())
or
methodName = "text" and
call.getNumArgument() = 0
)
}
}
/**
* A source for text from the DOM from a DOM property read or call to `getAttribute()`.
*/
@@ -58,7 +92,7 @@ module XssThroughDom {
DOMTextSource() {
exists(DataFlow::PropRead read | read = this |
read.getBase().getALocalSource() = DOM::domValueRef() and
read.mayHavePropertyName(["innerText", "textContent", "value", "name"])
read.mayHavePropertyName(unsafeDomPropertyName())
)
or
exists(DataFlow::MethodCallNode mcn | mcn = this |

View File

@@ -92,6 +92,16 @@ nodes
| classnames.js:15:47:15:63 | clsx(window.name) |
| classnames.js:15:52:15:62 | window.name |
| classnames.js:15:52:15:62 | window.name |
| d3.js:4:12:4:22 | window.name |
| d3.js:4:12:4:22 | window.name |
| d3.js:11:15:11:24 | getTaint() |
| d3.js:11:15:11:24 | getTaint() |
| d3.js:12:20:12:29 | getTaint() |
| d3.js:12:20:12:29 | getTaint() |
| d3.js:14:20:14:29 | getTaint() |
| d3.js:14:20:14:29 | getTaint() |
| d3.js:21:15:21:24 | getTaint() |
| d3.js:21:15:21:24 | getTaint() |
| dates.js:9:9:9:69 | taint |
| dates.js:9:17:9:69 | decodeU ... ing(1)) |
| dates.js:9:36:9:50 | window.location |
@@ -772,6 +782,22 @@ edges
| classnames.js:15:47:15:63 | clsx(window.name) | classnames.js:15:31:15:78 | `<span ... <span>` |
| classnames.js:15:52:15:62 | window.name | classnames.js:15:47:15:63 | clsx(window.name) |
| classnames.js:15:52:15:62 | window.name | classnames.js:15:47:15:63 | clsx(window.name) |
| d3.js:4:12:4:22 | window.name | d3.js:11:15:11:24 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:11:15:11:24 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:11:15:11:24 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:11:15:11:24 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:12:20:12:29 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:12:20:12:29 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:12:20:12:29 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:12:20:12:29 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:14:20:14:29 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:14:20:14:29 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:14:20:14:29 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:14:20:14:29 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:21:15:21:24 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:21:15:21:24 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:21:15:21:24 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:21:15:21:24 | getTaint() |
| dates.js:9:9:9:69 | taint | dates.js:11:63:11:67 | taint |
| dates.js:9:9:9:69 | taint | dates.js:12:66:12:70 | taint |
| dates.js:9:9:9:69 | taint | dates.js:13:59:13:63 | taint |
@@ -1338,6 +1364,10 @@ edges
| classnames.js:11:31:11:79 | `<span ... <span>` | classnames.js:10:45:10:55 | window.name | classnames.js:11:31:11:79 | `<span ... <span>` | Cross-site scripting vulnerability due to $@. | classnames.js:10:45:10:55 | window.name | user-provided value |
| classnames.js:13:31:13:83 | `<span ... <span>` | classnames.js:13:57:13:67 | window.name | classnames.js:13:31:13:83 | `<span ... <span>` | Cross-site scripting vulnerability due to $@. | classnames.js:13:57:13:67 | window.name | user-provided value |
| classnames.js:15:31:15:78 | `<span ... <span>` | classnames.js:15:52:15:62 | window.name | classnames.js:15:31:15:78 | `<span ... <span>` | Cross-site scripting vulnerability due to $@. | classnames.js:15:52:15:62 | window.name | user-provided value |
| d3.js:11:15:11:24 | getTaint() | d3.js:4:12:4:22 | window.name | d3.js:11:15:11:24 | getTaint() | Cross-site scripting vulnerability due to $@. | d3.js:4:12:4:22 | window.name | user-provided value |
| d3.js:12:20:12:29 | getTaint() | d3.js:4:12:4:22 | window.name | d3.js:12:20:12:29 | getTaint() | Cross-site scripting vulnerability due to $@. | d3.js:4:12:4:22 | window.name | user-provided value |
| d3.js:14:20:14:29 | getTaint() | d3.js:4:12:4:22 | window.name | d3.js:14:20:14:29 | getTaint() | Cross-site scripting vulnerability due to $@. | d3.js:4:12:4:22 | window.name | user-provided value |
| d3.js:21:15:21:24 | getTaint() | d3.js:4:12:4:22 | window.name | d3.js:21:15:21:24 | getTaint() | Cross-site scripting vulnerability due to $@. | d3.js:4:12:4:22 | window.name | user-provided value |
| dates.js:11:31:11:70 | `Time i ... aint)}` | dates.js:9:36:9:50 | window.location | dates.js:11:31:11:70 | `Time i ... aint)}` | Cross-site scripting vulnerability due to $@. | dates.js:9:36:9:50 | window.location | user-provided value |
| dates.js:12:31:12:73 | `Time i ... aint)}` | dates.js:9:36:9:50 | window.location | dates.js:12:31:12:73 | `Time i ... aint)}` | Cross-site scripting vulnerability due to $@. | dates.js:9:36:9:50 | window.location | user-provided value |
| dates.js:13:31:13:72 | `Time i ... time)}` | dates.js:9:36:9:50 | window.location | dates.js:13:31:13:72 | `Time i ... time)}` | Cross-site scripting vulnerability due to $@. | dates.js:9:36:9:50 | window.location | user-provided value |

View File

@@ -92,6 +92,16 @@ nodes
| classnames.js:15:47:15:63 | clsx(window.name) |
| classnames.js:15:52:15:62 | window.name |
| classnames.js:15:52:15:62 | window.name |
| d3.js:4:12:4:22 | window.name |
| d3.js:4:12:4:22 | window.name |
| d3.js:11:15:11:24 | getTaint() |
| d3.js:11:15:11:24 | getTaint() |
| d3.js:12:20:12:29 | getTaint() |
| d3.js:12:20:12:29 | getTaint() |
| d3.js:14:20:14:29 | getTaint() |
| d3.js:14:20:14:29 | getTaint() |
| d3.js:21:15:21:24 | getTaint() |
| d3.js:21:15:21:24 | getTaint() |
| dates.js:9:9:9:69 | taint |
| dates.js:9:17:9:69 | decodeU ... ing(1)) |
| dates.js:9:36:9:50 | window.location |
@@ -783,6 +793,22 @@ edges
| classnames.js:15:47:15:63 | clsx(window.name) | classnames.js:15:31:15:78 | `<span ... <span>` |
| classnames.js:15:52:15:62 | window.name | classnames.js:15:47:15:63 | clsx(window.name) |
| classnames.js:15:52:15:62 | window.name | classnames.js:15:47:15:63 | clsx(window.name) |
| d3.js:4:12:4:22 | window.name | d3.js:11:15:11:24 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:11:15:11:24 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:11:15:11:24 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:11:15:11:24 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:12:20:12:29 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:12:20:12:29 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:12:20:12:29 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:12:20:12:29 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:14:20:14:29 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:14:20:14:29 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:14:20:14:29 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:14:20:14:29 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:21:15:21:24 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:21:15:21:24 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:21:15:21:24 | getTaint() |
| d3.js:4:12:4:22 | window.name | d3.js:21:15:21:24 | getTaint() |
| dates.js:9:9:9:69 | taint | dates.js:11:63:11:67 | taint |
| dates.js:9:9:9:69 | taint | dates.js:12:66:12:70 | taint |
| dates.js:9:9:9:69 | taint | dates.js:13:59:13:63 | taint |

View File

@@ -0,0 +1,22 @@
const d3 = require('d3');
function getTaint() {
return window.name;
}
function doSomething() {
d3.select('#main')
.attr('width', 100)
.style('color', 'red')
.html(getTaint()) // NOT OK
.html(d => getTaint()) // NOT OK
.call(otherFunction)
.html(d => getTaint()); // NOT OK
}
function otherFunction(selection) {
selection
.attr('foo', 'bar')
.html(getTaint()); // NOT OK
}

View File

@@ -100,6 +100,9 @@ nodes
| xss-through-dom.js:79:4:79:34 | documen ... t.value |
| xss-through-dom.js:79:4:79:34 | documen ... t.value |
| xss-through-dom.js:79:4:79:34 | documen ... t.value |
| xss-through-dom.js:81:17:81:43 | $('#foo ... rText') |
| xss-through-dom.js:81:17:81:43 | $('#foo ... rText') |
| xss-through-dom.js:81:17:81:43 | $('#foo ... rText') |
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 |
@@ -157,6 +160,7 @@ edges
| xss-through-dom.js:73:20:73:41 | $("inpu ... 0).name | xss-through-dom.js:73:9:73:41 | selector |
| xss-through-dom.js:73:20:73:41 | $("inpu ... 0).name | xss-through-dom.js:73:9:73:41 | selector |
| xss-through-dom.js:79:4:79:34 | documen ... t.value | xss-through-dom.js:79:4:79:34 | documen ... t.value |
| xss-through-dom.js:81:17:81:43 | $('#foo ... rText') | xss-through-dom.js:81:17:81:43 | $('#foo ... rText') |
#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 |
@@ -185,3 +189,4 @@ edges
| xss-through-dom.js:71:11:71:32 | $("inpu ... 0).name | xss-through-dom.js:71:11:71:32 | $("inpu ... 0).name | xss-through-dom.js:71:11:71:32 | $("inpu ... 0).name | $@ is reinterpreted as HTML without escaping meta-characters. | xss-through-dom.js:71:11:71:32 | $("inpu ... 0).name | DOM text |
| xss-through-dom.js:77:4:77:11 | selector | xss-through-dom.js:73:20:73:41 | $("inpu ... 0).name | xss-through-dom.js:77:4:77:11 | selector | $@ is reinterpreted as HTML without escaping meta-characters. | xss-through-dom.js:73:20:73:41 | $("inpu ... 0).name | DOM text |
| xss-through-dom.js:79:4:79:34 | documen ... t.value | xss-through-dom.js:79:4:79:34 | documen ... t.value | xss-through-dom.js:79:4:79:34 | documen ... t.value | $@ is reinterpreted as HTML without escaping meta-characters. | xss-through-dom.js:79:4:79:34 | documen ... t.value | DOM text |
| xss-through-dom.js:81:17:81:43 | $('#foo ... rText') | xss-through-dom.js:81:17:81:43 | $('#foo ... rText') | xss-through-dom.js:81:17:81:43 | $('#foo ... rText') | $@ is reinterpreted as HTML without escaping meta-characters. | xss-through-dom.js:81:17:81:43 | $('#foo ... rText') | DOM text |

View File

@@ -77,4 +77,6 @@
$(selector); // NOT OK
$(document.my_form.my_input.value); // NOT OK
$("#id").html( $('#foo').prop('innerText') ); // NOT OK
})();

View File

@@ -0,0 +1,3 @@
lgtm,codescanning
* Updated _Binding a socket to all network interfaces_ (`py/bind-socket-all-network-interfaces`) query to use the new type-tracking approach instead of points-to analysis. You may see differences in the results found by the query, but overall this change should result in a more robust and accurate analysis.
* Updated _Binding a socket to all network interfaces_ (`py/bind-socket-all-network-interfaces`) to recognize binding to all interfaces in IPv6 with hostnames `::` and `::0`

View File

@@ -0,0 +1,2 @@
lgtm,codescanning
* API graphs now contain nodes for built-in functions and classes. For instance, `API::builtin("open")` is the API graph node corresponding to the built-in `open` function.

View File

@@ -11,28 +11,86 @@
*/
import python
import semmle.python.dataflow.new.DataFlow
import semmle.python.ApiGraphs
Value aSocket() { result.getClass() = Value::named("socket.socket") }
CallNode socketBindCall() {
result = aSocket().attr("bind").(CallableValue).getACall() and major_version() = 3
or
result.getFunction().(AttrNode).getObject("bind").pointsTo(aSocket()) and
major_version() = 2
/** Gets a hostname that can be used to bind to all interfaces. */
private string vulnerableHostname() {
result in [
// IPv4
"0.0.0.0", "",
// IPv6
"::", "::0"
]
}
string allInterfaces() { result = "0.0.0.0" or result = "" }
Value getTextValue(string address) {
result = Value::forUnicode(address) and major_version() = 3
/** Gets a reference to a hostname that can be used to bind to all interfaces. */
private DataFlow::LocalSourceNode vulnerableHostnameRef(DataFlow::TypeTracker t, string hostname) {
t.start() and
exists(StrConst allInterfacesStrConst | hostname = vulnerableHostname() |
allInterfacesStrConst.getText() = hostname and
result.asExpr() = allInterfacesStrConst
)
or
result = Value::forString(address) and major_version() = 2
// Due to bad performance when using normal setup with `vulnerableHostnameRef(t2, hostname).track(t2, t)`
// we have inlined that code and forced a join
exists(DataFlow::TypeTracker t2 |
exists(DataFlow::StepSummary summary |
vulnerableHostnameRef_first_join(t2, hostname, result, summary) and
t = t2.append(summary)
)
)
}
from CallNode call, TupleValue args, string address
pragma[nomagic]
private predicate vulnerableHostnameRef_first_join(
DataFlow::TypeTracker t2, string hostname, DataFlow::Node res, DataFlow::StepSummary summary
) {
DataFlow::StepSummary::step(vulnerableHostnameRef(t2, hostname), res, summary)
}
/** Gets a reference to a hostname that can be used to bind to all interfaces. */
DataFlow::Node vulnerableHostnameRef(string hostname) {
vulnerableHostnameRef(DataFlow::TypeTracker::end(), hostname).flowsTo(result)
}
/** Gets a reference to a tuple for which the first element is a hostname that can be used to bind to all interfaces. */
private DataFlow::LocalSourceNode vulnerableAddressTuple(DataFlow::TypeTracker t, string hostname) {
t.start() and
result.asExpr() = any(Tuple tup | tup.getElt(0) = vulnerableHostnameRef(hostname).asExpr())
or
// Due to bad performance when using normal setup with `vulnerableAddressTuple(t2, hostname).track(t2, t)`
// we have inlined that code and forced a join
exists(DataFlow::TypeTracker t2 |
exists(DataFlow::StepSummary summary |
vulnerableAddressTuple_first_join(t2, hostname, result, summary) and
t = t2.append(summary)
)
)
}
pragma[nomagic]
private predicate vulnerableAddressTuple_first_join(
DataFlow::TypeTracker t2, string hostname, DataFlow::Node res, DataFlow::StepSummary summary
) {
DataFlow::StepSummary::step(vulnerableAddressTuple(t2, hostname), res, summary)
}
/** Gets a reference to a tuple for which the first element is a hostname that can be used to bind to all interfaces. */
DataFlow::Node vulnerableAddressTuple(string hostname) {
vulnerableAddressTuple(DataFlow::TypeTracker::end(), hostname).flowsTo(result)
}
/**
* Gets an instance of `socket.socket` using _some_ address family.
*
* See https://docs.python.org/3/library/socket.html
*/
API::Node socketInstance() { result = API::moduleImport("socket").getMember("socket").getReturn() }
from DataFlow::CallCfgNode bindCall, DataFlow::Node addressArg, string hostname
where
call = socketBindCall() and
call.getArg(0).pointsTo(args) and
args.getItem(0) = getTextValue(address) and
address = allInterfaces()
select call.getNode(), "'" + address + "' binds a socket to all interfaces."
bindCall = socketInstance().getMember("bind").getACall() and
addressArg = bindCall.getArg(0) and
addressArg = vulnerableAddressTuple(hostname)
select bindCall.asExpr(), "'" + hostname + "' binds a socket to all interfaces."

View File

@@ -0,0 +1,33 @@
/**
* @name Binding a socket to all network interfaces
* @description Binding a socket to all interfaces opens it up to traffic from any IPv4 address
* and is therefore associated with security risks.
* @kind problem
*/
import python
Value aSocket() { result.getClass() = Value::named("socket.socket") }
CallNode socketBindCall() {
result = aSocket().attr("bind").(CallableValue).getACall() and major_version() = 3
or
result.getFunction().(AttrNode).getObject("bind").pointsTo(aSocket()) and
major_version() = 2
}
string allInterfaces() { result = "0.0.0.0" or result = "" }
Value getTextValue(string address) {
result = Value::forUnicode(address) and major_version() = 3
or
result = Value::forString(address) and major_version() = 2
}
from CallNode call, TupleValue args, string address
where
call = socketBindCall() and
call.getArg(0).pointsTo(args) and
args.getItem(0) = getTextValue(address) and
address = allInterfaces()
select call.getNode(), "'" + address + "' binds a socket to all interfaces."

View File

@@ -216,6 +216,9 @@ module API {
*/
Node moduleImport(string m) { result = Impl::MkModuleImport(m) }
/** Gets a node corresponding to the built-in with the given name, if any. */
Node builtin(string n) { result = moduleImport("builtins").getMember(n) }
/**
* Provides the actual implementation of API graphs, cached for performance.
*
@@ -300,11 +303,18 @@ module API {
MkRoot() or
/** An abstract representative for imports of the module called `name`. */
MkModuleImport(string name) {
imports(_, name)
// Ignore the following module name for Python 2, as we alias `__builtin__` to `builtins` elsewhere
(name != "__builtin__" or major_version() = 3) and
(
imports(_, name)
or
// When we `import foo.bar.baz` we want to create API graph nodes also for the prefixes
// `foo` and `foo.bar`:
name = any(ImportExpr e | not e.isRelative()).getAnImportedModuleName()
)
or
// When we `import foo.bar.baz` we want to create API graph nodes also for the prefixes
// `foo` and `foo.bar`:
name = any(ImportExpr e | not e.isRelative()).getAnImportedModuleName()
// The `builtins` module should always be implicitly available
name = "builtins"
} or
/** A use of an API member at the node `nd`. */
MkUse(DataFlow::Node nd) { use(_, _, nd) }
@@ -339,6 +349,24 @@ module API {
)
}
private import semmle.python.types.Builtins as Builtins
/**
* Gets a data flow node that is likely to refer to a built-in with the name `name`.
*
* Currently this is an over-approximation, and does not account for things like overwriting a
* built-in with a different value.
*/
private DataFlow::Node likely_builtin(string name) {
result.asCfgNode() =
any(NameNode n |
n.isGlobal() and
n.isLoad() and
name = n.getId() and
name = any(Builtins::Builtin b).getName()
)
}
/**
* Holds if `ref` is a use of a node that should have an incoming edge from `base` labeled
* `lbl` in the API graph.
@@ -369,6 +397,10 @@ module API {
ref.asExpr().(ClassExpr).getABase() = superclass.asExpr()
)
)
or
// Built-ins, treated as members of the module `builtins`
base = MkModuleImport("builtins") and
lbl = Label::member(any(string name | ref = likely_builtin(name)))
}
/**
@@ -381,6 +413,11 @@ module API {
imports(ref, name)
)
or
// Ensure the Python 2 `__builtin__` module gets the name of the Python 3 `builtins` module.
major_version() = 2 and
nd = MkModuleImport("builtins") and
imports(ref, "__builtin__")
or
nd = MkUse(ref)
}

View File

@@ -0,0 +1 @@
semmle-extractor-options: --lang=2 --max-import-depth=1

View File

@@ -0,0 +1,3 @@
def python2_style():
from __builtin__ import open #$ use=moduleImport("builtins").getMember("open")
open("hello.txt") #$ use=moduleImport("builtins").getMember("open").getReturn()

View File

@@ -0,0 +1,30 @@
import python
import semmle.python.dataflow.new.DataFlow
import TestUtilities.InlineExpectationsTest
import semmle.python.ApiGraphs
class ApiUseTest extends InlineExpectationsTest {
ApiUseTest() { this = "ApiUseTest" }
override string getARelevantTag() { result = "use" }
private predicate relevant_node(API::Node a, DataFlow::Node n, Location l) {
n = a.getAUse() and l = n.getLocation()
}
override predicate hasActualResult(Location location, string element, string tag, string value) {
exists(API::Node a, DataFlow::Node n | relevant_node(a, n, location) |
tag = "use" and
// Only report the longest path on this line:
value =
max(API::Node a2, Location l2 |
relevant_node(a2, _, l2) and
l2.getFile() = location.getFile() and
l2.getStartLine() = location.getStartLine()
|
a2.getPath()
) and
element = n.toString()
)
}
}

View File

@@ -100,3 +100,42 @@ def internal():
pass
int_instance = IntMyView() #$ use=moduleImport("pflask").getMember("views").getMember("View").getASubclass().getReturn()
# Built-ins
def use_of_builtins():
for x in range(5): #$ use=moduleImport("builtins").getMember("range").getReturn()
if x < len([]): #$ use=moduleImport("builtins").getMember("len").getReturn()
print("Hello") #$ use=moduleImport("builtins").getMember("print").getReturn()
raise Exception("Farewell") #$ use=moduleImport("builtins").getMember("Exception").getReturn()
def imported_builtins():
import builtins #$ use=moduleImport("builtins")
def open(f):
return builtins.open(f) #$ MISSING: use=moduleImport("builtins").getMember("open").getReturn()
def redefine_print():
def my_print(x):
import builtins #$ use=moduleImport("builtins")
builtins.print("I'm printing", x) #$ use=moduleImport("builtins").getMember("print").getReturn()
print = my_print
print("these words")
def local_redefine_range():
range = 5
return range
def global_redefine_range():
global range
range = 6
return range #$ SPURIOUS: use=moduleImport("builtins").getMember("range")
def obscured_print():
p = print #$ use=moduleImport("builtins").getMember("print")
p("Can you see me?") #$ use=moduleImport("builtins").getMember("print").getReturn()
def python2_style():
# In Python 3, `__builtin__` has no special meaning.
from __builtin__ import open #$ use=moduleImport("__builtin__").getMember("open")
open("hello.txt") #$ use=moduleImport("__builtin__").getMember("open").getReturn()

View File

@@ -2,5 +2,5 @@ import mypkg #$ use=moduleImport("mypkg")
print(mypkg.foo) #$ use=moduleImport("mypkg").getMember("foo") // 42
try:
print(mypkg.bar) #$ use=moduleImport("mypkg").getMember("bar")
except AttributeError as e:
print(e) # module 'mypkg' has no attribute 'bar'
except AttributeError as e: #$ use=moduleImport("builtins").getMember("AttributeError")
print(e) #$ use=moduleImport("builtins").getMember("print").getReturn() // module 'mypkg' has no attribute 'bar'

View File

@@ -3,8 +3,8 @@ import mypkg #$ use=moduleImport("mypkg")
print(mypkg.foo) #$ use=moduleImport("mypkg").getMember("foo") // 42
try:
print(mypkg.bar) #$ use=moduleImport("mypkg").getMember("bar")
except AttributeError as e:
print(e) # module 'mypkg' has no attribute 'bar'
except AttributeError as e: #$ use=moduleImport("builtins").getMember("AttributeError")
print(e) #$ use=moduleImport("builtins").getMember("print").getReturn() // module 'mypkg' has no attribute 'bar'
from mypkg import bar as _bar #$ use=moduleImport("mypkg").getMember("bar")
print(mypkg.bar) #$ use=moduleImport("mypkg").getMember("bar") // <module 'mypkg.bar' ...

View File

@@ -1,3 +1,5 @@
| BindToAllInterfaces_test.py:5:1:5:26 | Attribute() | '0.0.0.0' binds a socket to all interfaces. |
| BindToAllInterfaces_test.py:9:1:9:18 | Attribute() | '' binds a socket to all interfaces. |
| BindToAllInterfaces_test.py:17:1:17:26 | Attribute() | '0.0.0.0' binds a socket to all interfaces. |
| BindToAllInterfaces_test.py:21:1:21:11 | Attribute() | '0.0.0.0' binds a socket to all interfaces. |
| BindToAllInterfaces_test.py:26:1:26:20 | Attribute() | '::' binds a socket to all interfaces. |

View File

@@ -15,3 +15,12 @@ s.bind(('84.68.10.12', 8080))
# binds to all interfaces, insecure
ALL_LOCALS = "0.0.0.0"
s.bind((ALL_LOCALS, 9090))
# binds to all interfaces, insecure
tup = (ALL_LOCALS, 8080)
s.bind(tup)
# IPv6
s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
s.bind(("::", 8080)) # NOT OK