From 4709e8139d88ff445352716fdc6177e8a4995d70 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Mon, 3 May 2021 01:43:56 +0000 Subject: [PATCH 01/74] JPython code injection --- .../CWE/CWE-094/JPythonInjection.java | 48 ++++ .../CWE/CWE-094/JPythonInjection.qhelp | 34 +++ .../Security/CWE/CWE-094/JPythonInjection.ql | 68 +++++ .../CWE-094/JPythonInjection.expected | 11 + .../security/CWE-094/JPythonInjection.java | 64 +++++ .../security/CWE-094/JPythonInjection.qlref | 1 + .../query-tests/security/CWE-094/options | 2 +- .../jpython-2.7.2/org/python/core/PyCode.java | 43 +++ .../org/python/core/PyException.java | 12 + .../org/python/core/PyObject.java | 11 + .../org/python/core/PySystemState.java | 177 ++++++++++++ .../org/python/core/ThreadState.java | 28 ++ .../org/python/util/PythonInterpreter.java | 252 ++++++++++++++++++ 13 files changed, 750 insertions(+), 1 deletion(-) create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.java create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.qhelp create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.ql create mode 100644 java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.expected create mode 100644 java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.java create mode 100644 java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.qlref create mode 100644 java/ql/test/stubs/jpython-2.7.2/org/python/core/PyCode.java create mode 100644 java/ql/test/stubs/jpython-2.7.2/org/python/core/PyException.java create mode 100644 java/ql/test/stubs/jpython-2.7.2/org/python/core/PyObject.java create mode 100644 java/ql/test/stubs/jpython-2.7.2/org/python/core/PySystemState.java create mode 100644 java/ql/test/stubs/jpython-2.7.2/org/python/core/ThreadState.java create mode 100644 java/ql/test/stubs/jpython-2.7.2/org/python/util/PythonInterpreter.java diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.java b/java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.java new file mode 100644 index 00000000000..13db6830a71 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.java @@ -0,0 +1,48 @@ +public class JPythonInjection extends HttpServlet { + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/plain"); + String code = request.getParameter("code"); + PythonInterpreter interpreter = null; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + try { + interpreter = new PythonInterpreter(); + interpreter.setOut(out); + interpreter.setErr(out); + + // BAD: allow arbitrary JPython expression to execute + interpreter.exec(code); + out.flush(); + + response.getWriter().print(out.toString()); + } catch(PyException ex) { + response.getWriter().println(ex.getMessage()); + } finally { + if (interpreter != null) { + interpreter.close(); + } + out.close(); + } + } + + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/plain"); + String code = request.getParameter("code"); + PythonInterpreter interpreter = null; + + try { + interpreter = new PythonInterpreter(); + // BAD: allow arbitrary JPython expression to evaluate + PyObject py = interpreter.eval(code); + + response.getWriter().print(py.toString()); + } catch(PyException ex) { + response.getWriter().println(ex.getMessage()); + } finally { + if (interpreter != null) { + interpreter.close(); + } + } + } +} + diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.qhelp b/java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.qhelp new file mode 100644 index 00000000000..dddbb2d618a --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.qhelp @@ -0,0 +1,34 @@ + + + + +

Python has been the most widely used programming language in recent years, and JPython + is a popular Java implementation of Python. It allows embedded Python scripting inside + Java applications and provides an interactive interpreter that can be used to interact + with Java packages or with running Java applications. If an expression is built using + attacker-controlled data and then evaluated, it may allow the attacker to run arbitrary + code.

+
+ + +

In general, including user input in JPython expression should be avoided. If user input + must be included in an expression, it should be then evaluated in a safe context that + doesn't allow arbitrary code invocation.

+
+ + +

The following code could execute random code in JPython Interpreter

+ +
+ + +
  • + JPython Organization: JPython and Java Integration +
  • +
  • + PortSwigger: Python code injection +
  • +
    +
    diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.ql new file mode 100644 index 00000000000..a6621d89c26 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.ql @@ -0,0 +1,68 @@ +/** + * @name Injection in JPython + * @description Evaluation of a user-controlled malicious expression in JPython + * may lead to remote code execution. + * @kind path-problem + * @id java/jpython-injection + * @tags security + * external/cwe/cwe-094 + * external/cwe/cwe-095 + */ + +import java +import semmle.code.java.dataflow.FlowSources +import DataFlow::PathGraph + +/** The class `org.python.util.PythonInterpreter`. */ +class PythonInterpreter extends RefType { + PythonInterpreter() { this.hasQualifiedName("org.python.util", "PythonInterpreter") } +} + +/** A method that evaluates, compiles or executes a JPython expression. */ +class InterpretExprMethod extends Method { + InterpretExprMethod() { + this.getDeclaringType().getAnAncestor*() instanceof PythonInterpreter and + ( + hasName("exec") or + hasName("eval") or + hasName("compile") + ) + } +} + +/** Holds if a JPython expression if evaluated, compiled or executed. */ +predicate runCode(MethodAccess ma, Expr sink) { + exists(Method m | m = ma.getMethod() | + m instanceof InterpretExprMethod and + sink = ma.getArgument(0) + ) +} + +/** Sink of an expression interpreted by JPython interpreter. */ +class CodeInjectionSink extends DataFlow::ExprNode { + CodeInjectionSink() { runCode(_, this.getExpr()) } + + MethodAccess getMethodAccess() { runCode(result, this.getExpr()) } +} + +class CodeInjectionConfiguration extends TaintTracking::Configuration { + CodeInjectionConfiguration() { this = "CodeInjectionConfiguration" } + + override predicate isSource(DataFlow::Node source) { + source instanceof RemoteFlowSource + or + source instanceof LocalUserInput + } + + override predicate isSink(DataFlow::Node sink) { sink instanceof CodeInjectionSink } + + override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) { + // @RequestBody MyQueryObj query; interpreter.exec(query.getInterpreterCode()); + exists(MethodAccess ma | ma.getQualifier() = node1.asExpr() and ma = node2.asExpr()) + } +} + +from DataFlow::PathNode source, DataFlow::PathNode sink, CodeInjectionConfiguration conf +where conf.hasFlowPath(source, sink) +select sink.getNode().(CodeInjectionSink).getMethodAccess(), source, sink, "JPython evaluate $@.", + source.getNode(), "user input" diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.expected b/java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.expected new file mode 100644 index 00000000000..f5816001939 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.expected @@ -0,0 +1,11 @@ +edges +| JPythonInjection.java:22:23:22:50 | getParameter(...) : String | JPythonInjection.java:30:28:30:31 | code | +| JPythonInjection.java:47:21:47:48 | getParameter(...) : String | JPythonInjection.java:52:40:52:43 | code | +nodes +| JPythonInjection.java:22:23:22:50 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JPythonInjection.java:30:28:30:31 | code | semmle.label | code | +| JPythonInjection.java:47:21:47:48 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JPythonInjection.java:52:40:52:43 | code | semmle.label | code | +#select +| JPythonInjection.java:30:11:30:32 | exec(...) | JPythonInjection.java:22:23:22:50 | getParameter(...) : String | JPythonInjection.java:30:28:30:31 | code | JPython evaluate $@. | JPythonInjection.java:22:23:22:50 | getParameter(...) | user input | +| JPythonInjection.java:52:23:52:44 | eval(...) | JPythonInjection.java:47:21:47:48 | getParameter(...) : String | JPythonInjection.java:52:40:52:43 | code | JPython evaluate $@. | JPythonInjection.java:47:21:47:48 | getParameter(...) | user input | diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.java b/java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.java new file mode 100644 index 00000000000..a0515eb4212 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.java @@ -0,0 +1,64 @@ +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.python.core.PyObject; +import org.python.core.PyException; +import org.python.util.PythonInterpreter; + +public class JPythonInjection extends HttpServlet { + private static final long serialVersionUID = 1L; + + public JPythonInjection() { + super(); + } + + // BAD: allow arbitrary JPython expression to execute + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/plain"); + String code = request.getParameter("code"); + PythonInterpreter interpreter = null; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + try { + interpreter = new PythonInterpreter(); + interpreter.setOut(out); + interpreter.setErr(out); + interpreter.exec(code); + out.flush(); + + response.getWriter().print(out.toString()); + } catch(PyException ex) { + response.getWriter().println(ex.getMessage()); + } finally { + if (interpreter != null) { + interpreter.close(); + } + out.close(); + } + } + + // BAD: allow arbitrary JPython expression to evaluate + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/plain"); + String code = request.getParameter("code"); + PythonInterpreter interpreter = null; + + try { + interpreter = new PythonInterpreter(); + PyObject py = interpreter.eval(code); + + response.getWriter().print(py.toString()); + } catch(PyException ex) { + response.getWriter().println(ex.getMessage()); + } finally { + if (interpreter != null) { + interpreter.close(); + } + } + } +} + diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.qlref b/java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.qlref new file mode 100644 index 00000000000..80217a193bd --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.qlref @@ -0,0 +1 @@ +experimental/Security/CWE/CWE-094/JPythonInjection.ql \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/options b/java/ql/test/experimental/query-tests/security/CWE-094/options index 18e3518fc97..ccf3a24f215 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-094/options +++ b/java/ql/test/experimental/query-tests/security/CWE-094/options @@ -1,2 +1,2 @@ -//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/springframework-5.2.3:${testdir}/../../../../stubs/mvel2-2.4.7:${testdir}/../../../../stubs/jsr223-api:${testdir}/../../../../stubs/apache-commons-jexl-2.1.1:${testdir}/../../../../stubs/apache-commons-jexl-3.1:${testdir}/../../../../stubs/scriptengine:${testdir}/../../../../stubs/java-ee-el:${testdir}/../../../../stubs/juel-2.2:${testdir}/../../../stubs/groovy-all-3.0.7:${testdir}/../../../../stubs/servlet-api-2.4 +//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/springframework-5.2.3:${testdir}/../../../../stubs/mvel2-2.4.7:${testdir}/../../../../stubs/jsr223-api:${testdir}/../../../../stubs/apache-commons-jexl-2.1.1:${testdir}/../../../../stubs/apache-commons-jexl-3.1:${testdir}/../../../../stubs/scriptengine:${testdir}/../../../../stubs/java-ee-el:${testdir}/../../../../stubs/juel-2.2:${testdir}/../../../stubs/groovy-all-3.0.7:${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/jpython-2.7.2 diff --git a/java/ql/test/stubs/jpython-2.7.2/org/python/core/PyCode.java b/java/ql/test/stubs/jpython-2.7.2/org/python/core/PyCode.java new file mode 100644 index 00000000000..9b7c99f94fa --- /dev/null +++ b/java/ql/test/stubs/jpython-2.7.2/org/python/core/PyCode.java @@ -0,0 +1,43 @@ +// Copyright (c) Corporation for National Research Initiatives +package org.python.core; + +/** + * A super class for all python code implementations. + */ +public abstract class PyCode extends PyObject +{ + abstract public PyObject call(ThreadState state, + PyObject args[], String keywords[], + PyObject globals, PyObject[] defaults, + PyObject closure); + + abstract public PyObject call(ThreadState state, + PyObject self, PyObject args[], + String keywords[], + PyObject globals, PyObject[] defaults, + PyObject closure); + + abstract public PyObject call(ThreadState state, + PyObject globals, PyObject[] defaults, + PyObject closure); + + abstract public PyObject call(ThreadState state, + PyObject arg1, PyObject globals, + PyObject[] defaults, PyObject closure); + + abstract public PyObject call(ThreadState state, + PyObject arg1, PyObject arg2, + PyObject globals, PyObject[] defaults, + PyObject closure); + + abstract public PyObject call(ThreadState state, + PyObject arg1, PyObject arg2, PyObject arg3, + PyObject globals, PyObject[] defaults, + PyObject closure); + + abstract public PyObject call(ThreadState state, + PyObject arg1, PyObject arg2, PyObject arg3, PyObject arg4, + PyObject globals, PyObject[] defaults, + PyObject closure); + +} diff --git a/java/ql/test/stubs/jpython-2.7.2/org/python/core/PyException.java b/java/ql/test/stubs/jpython-2.7.2/org/python/core/PyException.java new file mode 100644 index 00000000000..3a0a6c52c69 --- /dev/null +++ b/java/ql/test/stubs/jpython-2.7.2/org/python/core/PyException.java @@ -0,0 +1,12 @@ +// Copyright (c) Corporation for National Research Initiatives +package org.python.core; +import java.io.*; + +/** + * A wrapper for all python exception. Note that the well-known python exceptions are not + * subclasses of PyException. Instead the python exception class is stored in the type + * field and value or class instance is stored in the value field. + */ +public class PyException extends RuntimeException +{ +} diff --git a/java/ql/test/stubs/jpython-2.7.2/org/python/core/PyObject.java b/java/ql/test/stubs/jpython-2.7.2/org/python/core/PyObject.java new file mode 100644 index 00000000000..00993123461 --- /dev/null +++ b/java/ql/test/stubs/jpython-2.7.2/org/python/core/PyObject.java @@ -0,0 +1,11 @@ +// Copyright (c) Corporation for National Research Initiatives +package org.python.core; + +import java.io.Serializable; + +/** + * All objects known to the Jython runtime system are represented by an instance of the class + * {@code PyObject} or one of its subclasses. + */ +public class PyObject implements Serializable { +} diff --git a/java/ql/test/stubs/jpython-2.7.2/org/python/core/PySystemState.java b/java/ql/test/stubs/jpython-2.7.2/org/python/core/PySystemState.java new file mode 100644 index 00000000000..8444bbba70e --- /dev/null +++ b/java/ql/test/stubs/jpython-2.7.2/org/python/core/PySystemState.java @@ -0,0 +1,177 @@ +// Copyright (c) Corporation for National Research Initiatives +package org.python.core; + +import java.io.File; +import java.io.IOException; +import java.util.Properties; + +/** + * The "sys" module. + */ +// xxx Many have lamented, this should really be a module! +// but it will require some refactoring to see this wish come true. +public class PySystemState extends PyObject { + public PySystemState() { + } + + public static void classDictInit(PyObject dict) { + } + + public ClassLoader getSyspathJavaLoader() { + return null; + } + + // xxx fix this accessors + public PyObject __findattr_ex__(String name) { + return null; + } + + public void __setattr__(String name, PyObject value) { + } + + public void __delattr__(String name) { + } + + public PyObject gettrace() { + return null; + } + + public void settrace(PyObject tracefunc) { + } + + /** + * Change the current working directory to the specified path. + * + * path is assumed to be absolute and canonical (via os.path.realpath). + * + * @param path a path String + */ + public void setCurrentWorkingDir(String path) { + } + + /** + * Return a string representing the current working directory. + * + * @return a path String + */ + public String getCurrentWorkingDir() { + return null; + } + + /** + * Resolve a path. Returns the full path taking the current working directory into account. + * + * @param path a path String + * @return a resolved path String + */ + public String getPath(String path) { + return null; + } + + /** + * Resolve a path, returning a {@link File}, taking the current working directory into account. + * + * @param path a path String + * @return a resolved File + */ + public File getFile(String path) { + return null; + } + + public ClassLoader getClassLoader() { + return null; + } + + public void setClassLoader(ClassLoader classLoader) { + } + + public static Properties getBaseProperties() { + return null; + } + + public static synchronized void initialize() { + } + + public static synchronized void initialize(Properties preProperties, + Properties postProperties) { + } + + public static synchronized void initialize(Properties preProperties, Properties postProperties, + String[] argv) { + } + + public static synchronized void initialize(Properties preProperties, Properties postProperties, + String[] argv, ClassLoader classLoader) { + } + + /** + * Add a classpath directory to the list of places that are searched for java packages. + *

    + * Note. Classes found in directory and sub-directory are not made available to jython by + * this call. It only makes the java package found in the directory available. This call is + * mostly useful if jython is embedded in an application that deals with its own class loaders. + * A servlet container is a very good example. Calling + * {@code add_classdir("/WEB-INF/classes")} makes the java packages in WEB-INF classes + * available to jython import. However the actual class loading is completely handled by the + * servlet container's context classloader. + */ + public static void add_classdir(String directoryPath) { + } + + /** + * Add a .jar and .zip directory to the list of places that are searched for java .jar and .zip + * files. The .jar and .zip files found will not be cached. + *

    + * Note. Classes in .jar and .zip files found in the directory are not made available to + * jython by this call. See the note for add_classdir(dir) for more details. + * + * @param directoryPath The name of a directory. + * + * @see #add_classdir + */ + public static void add_extdir(String directoryPath) { + } + + /** + * Add a .jar and .zip directory to the list of places that are searched for java .jar and .zip + * files. + *

    + * Note. Classes in .jar and .zip files found in the directory are not made available to + * jython by this call. See the note for add_classdir(dir) for more details. + * + * @param directoryPath The name of a directory. + * @param cache Controls if the packages in the zip and jar file should be cached. + * + * @see #add_classdir + */ + public static void add_extdir(String directoryPath, boolean cache) { + } + + // Not public by design. We can't rebind the displayhook if + // a reflected function is inserted in the class dict. + + /** + * Exit a Python program with the given status. + * + * @param status the value to exit with + * @throws PyException {@code SystemExit} always throws this exception. When caught at top level + * the program will exit. + */ + public static void exit(PyObject status) { + } + + /** + * Exit a Python program with the status 0. + */ + public static void exit() { + } + + public static void exc_clear() { + } + + public void cleanup() { + } + + public void close() { + } +} diff --git a/java/ql/test/stubs/jpython-2.7.2/org/python/core/ThreadState.java b/java/ql/test/stubs/jpython-2.7.2/org/python/core/ThreadState.java new file mode 100644 index 00000000000..920270fe053 --- /dev/null +++ b/java/ql/test/stubs/jpython-2.7.2/org/python/core/ThreadState.java @@ -0,0 +1,28 @@ +// Copyright (c) Corporation for National Research Initiatives +package org.python.core; + +// a ThreadState refers to one PySystemState; this weak ref allows for tracking all ThreadState objects +// that refer to a given PySystemState + +public class ThreadState { + + public PyException exception; + + public ThreadState(PySystemState systemState) { + setSystemState(systemState); + } + + public void setSystemState(PySystemState systemState) { + } + + public PySystemState getSystemState() { + return null; + } + + public boolean enterRepr(PyObject obj) { + return false; + } + + public void exitRepr(PyObject obj) { + } +} diff --git a/java/ql/test/stubs/jpython-2.7.2/org/python/util/PythonInterpreter.java b/java/ql/test/stubs/jpython-2.7.2/org/python/util/PythonInterpreter.java new file mode 100644 index 00000000000..92c50917b59 --- /dev/null +++ b/java/ql/test/stubs/jpython-2.7.2/org/python/util/PythonInterpreter.java @@ -0,0 +1,252 @@ +package org.python.util; + +import java.io.Closeable; +import java.io.Reader; +import java.io.StringReader; +import java.util.Properties; + +import org.python.core.PyCode; +import org.python.core.PyObject; + +/** + * The PythonInterpreter class is a standard wrapper for a Jython interpreter for embedding in a + * Java application. + */ +public class PythonInterpreter implements Closeable { + + /** + * Initializes the Jython runtime. This should only be called once, before any other Python + * objects (including PythonInterpreter) are created. + * + * @param preProperties A set of properties. Typically System.getProperties() is used. + * preProperties override properties from the registry file. + * @param postProperties Another set of properties. Values like python.home, python.path and all + * other values from the registry files can be added to this property set. + * postProperties override system properties and registry properties. + * @param argv Command line arguments, assigned to sys.argv. + */ + public static void + initialize(Properties preProperties, Properties postProperties, String[] argv) { + } + + /** + * Creates a new interpreter with an empty local namespace. + */ + public PythonInterpreter() { + } + + /** + * Creates a new interpreter with the ability to maintain a separate local namespace for each + * thread (set by invoking setLocals()). + * + * @param dict a Python mapping object (e.g., a dictionary) for use as the default namespace + */ + public static PythonInterpreter threadLocalStateInterpreter(PyObject dict) { + return null; + } + + /** + * Creates a new interpreter with a specified local namespace. + * + * @param dict a Python mapping object (e.g., a dictionary) for use as the namespace + */ + public PythonInterpreter(PyObject dict) { + } + + /** + * Sets a Python object to use for the standard output stream, sys.stdout. This + * stream is used in a byte-oriented way (mostly) that depends on the type of file-like object. + * The behaviour as implemented is: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Stream behaviour for various object types
    Python type of object o written
    str/bytesunicodeAny other type
    {@link PyFile}as bytes directlyrespect {@link PyFile#encoding}call str(o) first
    {@link PyFileWriter}each byte value as a charwrite as Java charscall o.toString() first
    Other {@link PyObject} finvoke f.write(str(o))invoke f.write(o)invoke f.write(str(o))
    + * + * @param outStream Python file-like object to use as the output stream + */ + public void setOut(PyObject outStream) { + } + + /** + * Sets a {@link java.io.Writer} to use for the standard output stream, sys.stdout. + * The behaviour as implemented is to output each object o by calling + * o.toString() and writing this as UTF-16. + * + * @param outStream to use as the output stream + */ + public void setOut(java.io.Writer outStream) { + } + + /** + * Sets a {@link java.io.OutputStream} to use for the standard output stream. + * + * @param outStream OutputStream to use as output stream + */ + public void setOut(java.io.OutputStream outStream) { + } + + /** + * Sets a Python object to use for the standard output stream, sys.stderr. This + * stream is used in a byte-oriented way (mostly) that depends on the type of file-like object, + * in the same way as {@link #setOut(PyObject)}. + * + * @param outStream Python file-like object to use as the error output stream + */ + public void setErr(PyObject outStream) { + } + + /** + * Sets a {@link java.io.Writer} to use for the standard output stream, sys.stdout. + * The behaviour as implemented is to output each object o by calling + * o.toString() and writing this as UTF-16. + * + * @param outStream to use as the error output stream + */ + public void setErr(java.io.Writer outStream) { + } + + public void setErr(java.io.OutputStream outStream) { + } + + /** + * Evaluates a string as a Python expression and returns the result. + */ + public PyObject eval(String s) { + return null; + } + + /** + * Evaluates a Python code object and returns the result. + */ + public PyObject eval(PyObject code) { + return null; + } + + /** + * Executes a string of Python source in the local namespace. + * + * In some environments, such as Windows, Unicode characters in the script will be converted + * into ascii question marks (?). This can be avoided by first compiling the fragment using + * PythonInterpreter.compile(), and using the overridden form of this method which takes a + * PyCode object. Code page declarations are not supported. + */ + public void exec(String s) { + } + + /** + * Executes a Python code object in the local namespace. + */ + public void exec(PyObject code) { + } + + /** + * Executes a file of Python source in the local namespace. + */ + public void execfile(String filename) { + } + + public void execfile(java.io.InputStream s) { + } + + public void execfile(java.io.InputStream s, String name) { + } + + /** + * Compiles a string of Python source as either an expression (if possible) or a module. + * + * Designed for use by a JSR 223 implementation: "the Scripting API does not distinguish between + * scripts which return values and those which do not, nor do they make the corresponding + * distinction between evaluating or executing objects." (SCR.4.2.1) + */ + public PyCode compile(String script) { + return null; + } + + public PyCode compile(Reader reader) { + return null; + } + + public PyCode compile(String script, String filename) { + return null; + } + + public PyCode compile(Reader reader, String filename) { + return null; + } + + /** + * Sets a variable in the local namespace. + * + * @param name the name of the variable + * @param value the object to set the variable to (as converted to an appropriate Python object) + */ + public void set(String name, Object value) { + } + + /** + * Sets a variable in the local namespace. + * + * @param name the name of the variable + * @param value the Python object to set the variable to + */ + public void set(String name, PyObject value) { + } + + /** + * Returns the value of a variable in the local namespace. + * + * @param name the name of the variable + * @return the value of the variable, or null if that name isn't assigned + */ + public PyObject get(String name) { + return null; + } + + /** + * Returns the value of a variable in the local namespace. + * + * The value will be returned as an instance of the given Java class. + * interp.get("foo", Object.class) will return the most appropriate generic Java + * object. + * + * @param name the name of the variable + * @param javaclass the class of object to return + * @return the value of the variable as the given class, or null if that name isn't assigned + */ + public T get(String name, Class javaclass) { + return null; + } + + public void cleanup() { + } + + public void close() { + } +} From 703fbf139aaf828b1bd954f5b60714036a99df64 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Tue, 4 May 2021 02:54:49 +0000 Subject: [PATCH 02/74] Add more methods and update the library name --- .../CWE/CWE-094/JPythonInjection.java | 48 ------ .../CWE/CWE-094/JPythonInjection.qhelp | 34 ----- .../Security/CWE/CWE-094/JPythonInjection.ql | 68 --------- .../Security/CWE/CWE-094/JythonInjection.java | 47 ++++++ .../CWE/CWE-094/JythonInjection.qhelp | 34 +++++ .../Security/CWE/CWE-094/JythonInjection.ql | 123 +++++++++++++++ .../CWE-094/JPythonInjection.expected | 11 -- .../security/CWE-094/JPythonInjection.java | 64 -------- .../security/CWE-094/JPythonInjection.qlref | 1 - .../security/CWE-094/JythonInjection.expected | 21 +++ .../security/CWE-094/JythonInjection.java | 144 ++++++++++++++++++ .../security/CWE-094/JythonInjection.qlref | 1 + .../query-tests/security/CWE-094/options | 2 +- .../org/python/antlr/base/mod.java | 5 + .../org/python/core/BytecodeLoader.java | 47 ++++++ .../org/python/core/CompileMode.java | 11 ++ .../org/python/core/CompilerFlags.java | 17 +++ .../jython-2.7.2/org/python/core/Py.java | 134 ++++++++++++++++ .../org/python/core/PyCode.java | 0 .../org/python/core/PyException.java | 0 .../org/python/core/PyObject.java | 0 .../org/python/core/PySystemState.java | 0 .../org/python/core/ThreadState.java | 0 .../python/util/InteractiveInterpreter.java | 114 ++++++++++++++ .../org/python/util/PythonInterpreter.java | 0 25 files changed, 699 insertions(+), 227 deletions(-) delete mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.java delete mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.qhelp delete mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.ql create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.java create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.qhelp create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.ql delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.expected delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.java delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.qlref create mode 100644 java/ql/test/experimental/query-tests/security/CWE-094/JythonInjection.expected create mode 100644 java/ql/test/experimental/query-tests/security/CWE-094/JythonInjection.java create mode 100644 java/ql/test/experimental/query-tests/security/CWE-094/JythonInjection.qlref create mode 100644 java/ql/test/stubs/jython-2.7.2/org/python/antlr/base/mod.java create mode 100644 java/ql/test/stubs/jython-2.7.2/org/python/core/BytecodeLoader.java create mode 100644 java/ql/test/stubs/jython-2.7.2/org/python/core/CompileMode.java create mode 100644 java/ql/test/stubs/jython-2.7.2/org/python/core/CompilerFlags.java create mode 100644 java/ql/test/stubs/jython-2.7.2/org/python/core/Py.java rename java/ql/test/stubs/{jpython-2.7.2 => jython-2.7.2}/org/python/core/PyCode.java (100%) rename java/ql/test/stubs/{jpython-2.7.2 => jython-2.7.2}/org/python/core/PyException.java (100%) rename java/ql/test/stubs/{jpython-2.7.2 => jython-2.7.2}/org/python/core/PyObject.java (100%) rename java/ql/test/stubs/{jpython-2.7.2 => jython-2.7.2}/org/python/core/PySystemState.java (100%) rename java/ql/test/stubs/{jpython-2.7.2 => jython-2.7.2}/org/python/core/ThreadState.java (100%) create mode 100644 java/ql/test/stubs/jython-2.7.2/org/python/util/InteractiveInterpreter.java rename java/ql/test/stubs/{jpython-2.7.2 => jython-2.7.2}/org/python/util/PythonInterpreter.java (100%) diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.java b/java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.java deleted file mode 100644 index 13db6830a71..00000000000 --- a/java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.java +++ /dev/null @@ -1,48 +0,0 @@ -public class JPythonInjection extends HttpServlet { - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - response.setContentType("text/plain"); - String code = request.getParameter("code"); - PythonInterpreter interpreter = null; - ByteArrayOutputStream out = new ByteArrayOutputStream(); - - try { - interpreter = new PythonInterpreter(); - interpreter.setOut(out); - interpreter.setErr(out); - - // BAD: allow arbitrary JPython expression to execute - interpreter.exec(code); - out.flush(); - - response.getWriter().print(out.toString()); - } catch(PyException ex) { - response.getWriter().println(ex.getMessage()); - } finally { - if (interpreter != null) { - interpreter.close(); - } - out.close(); - } - } - - protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - response.setContentType("text/plain"); - String code = request.getParameter("code"); - PythonInterpreter interpreter = null; - - try { - interpreter = new PythonInterpreter(); - // BAD: allow arbitrary JPython expression to evaluate - PyObject py = interpreter.eval(code); - - response.getWriter().print(py.toString()); - } catch(PyException ex) { - response.getWriter().println(ex.getMessage()); - } finally { - if (interpreter != null) { - interpreter.close(); - } - } - } -} - diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.qhelp b/java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.qhelp deleted file mode 100644 index dddbb2d618a..00000000000 --- a/java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.qhelp +++ /dev/null @@ -1,34 +0,0 @@ - - - - -

    Python has been the most widely used programming language in recent years, and JPython - is a popular Java implementation of Python. It allows embedded Python scripting inside - Java applications and provides an interactive interpreter that can be used to interact - with Java packages or with running Java applications. If an expression is built using - attacker-controlled data and then evaluated, it may allow the attacker to run arbitrary - code.

    - - - -

    In general, including user input in JPython expression should be avoided. If user input - must be included in an expression, it should be then evaluated in a safe context that - doesn't allow arbitrary code invocation.

    -
    - - -

    The following code could execute random code in JPython Interpreter

    - -
    - - -
  • - JPython Organization: JPython and Java Integration -
  • -
  • - PortSwigger: Python code injection -
  • -
    - diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.ql deleted file mode 100644 index a6621d89c26..00000000000 --- a/java/ql/src/experimental/Security/CWE/CWE-094/JPythonInjection.ql +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @name Injection in JPython - * @description Evaluation of a user-controlled malicious expression in JPython - * may lead to remote code execution. - * @kind path-problem - * @id java/jpython-injection - * @tags security - * external/cwe/cwe-094 - * external/cwe/cwe-095 - */ - -import java -import semmle.code.java.dataflow.FlowSources -import DataFlow::PathGraph - -/** The class `org.python.util.PythonInterpreter`. */ -class PythonInterpreter extends RefType { - PythonInterpreter() { this.hasQualifiedName("org.python.util", "PythonInterpreter") } -} - -/** A method that evaluates, compiles or executes a JPython expression. */ -class InterpretExprMethod extends Method { - InterpretExprMethod() { - this.getDeclaringType().getAnAncestor*() instanceof PythonInterpreter and - ( - hasName("exec") or - hasName("eval") or - hasName("compile") - ) - } -} - -/** Holds if a JPython expression if evaluated, compiled or executed. */ -predicate runCode(MethodAccess ma, Expr sink) { - exists(Method m | m = ma.getMethod() | - m instanceof InterpretExprMethod and - sink = ma.getArgument(0) - ) -} - -/** Sink of an expression interpreted by JPython interpreter. */ -class CodeInjectionSink extends DataFlow::ExprNode { - CodeInjectionSink() { runCode(_, this.getExpr()) } - - MethodAccess getMethodAccess() { runCode(result, this.getExpr()) } -} - -class CodeInjectionConfiguration extends TaintTracking::Configuration { - CodeInjectionConfiguration() { this = "CodeInjectionConfiguration" } - - override predicate isSource(DataFlow::Node source) { - source instanceof RemoteFlowSource - or - source instanceof LocalUserInput - } - - override predicate isSink(DataFlow::Node sink) { sink instanceof CodeInjectionSink } - - override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) { - // @RequestBody MyQueryObj query; interpreter.exec(query.getInterpreterCode()); - exists(MethodAccess ma | ma.getQualifier() = node1.asExpr() and ma = node2.asExpr()) - } -} - -from DataFlow::PathNode source, DataFlow::PathNode sink, CodeInjectionConfiguration conf -where conf.hasFlowPath(source, sink) -select sink.getNode().(CodeInjectionSink).getMethodAccess(), source, sink, "JPython evaluate $@.", - source.getNode(), "user input" diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.java b/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.java new file mode 100644 index 00000000000..fca518443d1 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.java @@ -0,0 +1,47 @@ +public class JythonInjection extends HttpServlet { + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/plain"); + String code = request.getParameter("code"); + PythonInterpreter interpreter = null; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + try { + interpreter = new PythonInterpreter(); + interpreter.setOut(out); + interpreter.setErr(out); + + // BAD: allow arbitrary Jython expression to execute + interpreter.exec(code); + out.flush(); + + response.getWriter().print(out.toString()); + } catch(PyException ex) { + response.getWriter().println(ex.getMessage()); + } finally { + if (interpreter != null) { + interpreter.close(); + } + out.close(); + } + } + + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/plain"); + String code = request.getParameter("code"); + PythonInterpreter interpreter = null; + + try { + interpreter = new PythonInterpreter(); + // BAD: allow arbitrary Jython expression to evaluate + PyObject py = interpreter.eval(code); + + response.getWriter().print(py.toString()); + } catch(PyException ex) { + response.getWriter().println(ex.getMessage()); + } finally { + if (interpreter != null) { + interpreter.close(); + } + } + } +} diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.qhelp b/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.qhelp new file mode 100644 index 00000000000..8916296f93b --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.qhelp @@ -0,0 +1,34 @@ + + + + +

    Python has been the most widely used programming language in recent years, and Jython + (formerly known as JPython) is a popular Java implementation of Python. It allows + embedded Python scripting inside Java applications and provides an interactive interpreter + that can be used to interact with Java packages or with running Java applications. If an + expression is built using attacker-controlled data and then evaluated, it may allow the + attacker to run arbitrary code.

    +
    + + +

    In general, including user input in Jython expression should be avoided. If user input + must be included in an expression, it should be then evaluated in a safe context that + doesn't allow arbitrary code invocation.

    +
    + + +

    The following code could execute arbitrary code in Jython Interpreter

    + +
    + + +
  • + Jython Organization: Jython and Java Integration +
  • +
  • + PortSwigger: Python code injection +
  • +
    +
    diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.ql new file mode 100644 index 00000000000..088c33e00fd --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.ql @@ -0,0 +1,123 @@ +/** + * @name Injection in Jython + * @description Evaluation of a user-controlled malicious expression in Java Python + * interpreter may lead to remote code execution. + * @kind path-problem + * @id java/jython-injection + * @tags security + * external/cwe/cwe-094 + * external/cwe/cwe-095 + */ + +import java +import semmle.code.java.dataflow.FlowSources +import DataFlow::PathGraph + +/** The class `org.python.util.PythonInterpreter`. */ +class PythonInterpreter extends RefType { + PythonInterpreter() { this.hasQualifiedName("org.python.util", "PythonInterpreter") } +} + +/** A method that evaluates, compiles or executes a Jython expression. */ +class InterpretExprMethod extends Method { + InterpretExprMethod() { + this.getDeclaringType().getAnAncestor*() instanceof PythonInterpreter and + ( + getName().matches("exec%") or + hasName("eval") or + hasName("compile") or + getName().matches("run%") + ) + } +} + +/** The class `org.python.core.BytecodeLoader`. */ +class BytecodeLoader extends RefType { + BytecodeLoader() { this.hasQualifiedName("org.python.core", "BytecodeLoader") } +} + +/** Holds if a Jython expression if evaluated, compiled or executed. */ +predicate runCode(MethodAccess ma, Expr sink) { + exists(Method m | m = ma.getMethod() | + m instanceof InterpretExprMethod and + sink = ma.getArgument(0) + ) +} + +/** A method that loads Java class data. */ +class LoadClassMethod extends Method { + LoadClassMethod() { + this.getDeclaringType().getAnAncestor*() instanceof BytecodeLoader and + ( + hasName("makeClass") or + hasName("makeCode") + ) + } +} + +/** Holds if a Java class file is loaded. */ +predicate loadClass(MethodAccess ma, Expr sink) { + exists(Method m, int i | m = ma.getMethod() | + m instanceof LoadClassMethod and + m.getParameter(i).getType() instanceof Array and // makeClass(java.lang.String name, byte[] data, ...) + sink = ma.getArgument(i) + ) +} + +/** The class `org.python.core.Py`. */ +class Py extends RefType { + Py() { this.hasQualifiedName("org.python.core", "Py") } +} + +/** A method that compiles code with `Py`. */ +class PyCompileMethod extends Method { + PyCompileMethod() { + this.getDeclaringType().getAnAncestor*() instanceof Py and + getName().matches("compile%") + } +} + +/** Holds if source code is compiled with `PyCompileMethod`. */ +predicate compile(MethodAccess ma, Expr sink) { + exists(Method m | m = ma.getMethod() | + m instanceof PyCompileMethod and + sink = ma.getArgument(0) + ) +} + +/** Sink of an expression loaded by Jython. */ +class CodeInjectionSink extends DataFlow::ExprNode { + CodeInjectionSink() { + runCode(_, this.getExpr()) or + loadClass(_, this.getExpr()) or + compile(_, this.getExpr()) + } + + MethodAccess getMethodAccess() { + runCode(result, this.getExpr()) or + loadClass(result, this.getExpr()) or + compile(result, this.getExpr()) + } +} + +class CodeInjectionConfiguration extends TaintTracking::Configuration { + CodeInjectionConfiguration() { this = "CodeInjectionConfiguration" } + + override predicate isSource(DataFlow::Node source) { + source instanceof RemoteFlowSource + or + source instanceof LocalUserInput + } + + override predicate isSink(DataFlow::Node sink) { sink instanceof CodeInjectionSink } + + override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) { + // @RequestBody MyQueryObj query; interpreter.exec(query.getInterpreterCode()); + exists(MethodAccess ma | ma.getQualifier() = node1.asExpr() and ma = node2.asExpr()) + } +} + +from DataFlow::PathNode source, DataFlow::PathNode sink, CodeInjectionConfiguration conf +where conf.hasFlowPath(source, sink) +select sink.getNode().(CodeInjectionSink).getMethodAccess(), source, sink, "Jython evaluate $@.", + source.getNode(), "user input" diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.expected b/java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.expected deleted file mode 100644 index f5816001939..00000000000 --- a/java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.expected +++ /dev/null @@ -1,11 +0,0 @@ -edges -| JPythonInjection.java:22:23:22:50 | getParameter(...) : String | JPythonInjection.java:30:28:30:31 | code | -| JPythonInjection.java:47:21:47:48 | getParameter(...) : String | JPythonInjection.java:52:40:52:43 | code | -nodes -| JPythonInjection.java:22:23:22:50 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JPythonInjection.java:30:28:30:31 | code | semmle.label | code | -| JPythonInjection.java:47:21:47:48 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JPythonInjection.java:52:40:52:43 | code | semmle.label | code | -#select -| JPythonInjection.java:30:11:30:32 | exec(...) | JPythonInjection.java:22:23:22:50 | getParameter(...) : String | JPythonInjection.java:30:28:30:31 | code | JPython evaluate $@. | JPythonInjection.java:22:23:22:50 | getParameter(...) | user input | -| JPythonInjection.java:52:23:52:44 | eval(...) | JPythonInjection.java:47:21:47:48 | getParameter(...) : String | JPythonInjection.java:52:40:52:43 | code | JPython evaluate $@. | JPythonInjection.java:47:21:47:48 | getParameter(...) | user input | diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.java b/java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.java deleted file mode 100644 index a0515eb4212..00000000000 --- a/java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.java +++ /dev/null @@ -1,64 +0,0 @@ -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.python.core.PyObject; -import org.python.core.PyException; -import org.python.util.PythonInterpreter; - -public class JPythonInjection extends HttpServlet { - private static final long serialVersionUID = 1L; - - public JPythonInjection() { - super(); - } - - // BAD: allow arbitrary JPython expression to execute - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - response.setContentType("text/plain"); - String code = request.getParameter("code"); - PythonInterpreter interpreter = null; - ByteArrayOutputStream out = new ByteArrayOutputStream(); - - try { - interpreter = new PythonInterpreter(); - interpreter.setOut(out); - interpreter.setErr(out); - interpreter.exec(code); - out.flush(); - - response.getWriter().print(out.toString()); - } catch(PyException ex) { - response.getWriter().println(ex.getMessage()); - } finally { - if (interpreter != null) { - interpreter.close(); - } - out.close(); - } - } - - // BAD: allow arbitrary JPython expression to evaluate - protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - response.setContentType("text/plain"); - String code = request.getParameter("code"); - PythonInterpreter interpreter = null; - - try { - interpreter = new PythonInterpreter(); - PyObject py = interpreter.eval(code); - - response.getWriter().print(py.toString()); - } catch(PyException ex) { - response.getWriter().println(ex.getMessage()); - } finally { - if (interpreter != null) { - interpreter.close(); - } - } - } -} - diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.qlref b/java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.qlref deleted file mode 100644 index 80217a193bd..00000000000 --- a/java/ql/test/experimental/query-tests/security/CWE-094/JPythonInjection.qlref +++ /dev/null @@ -1 +0,0 @@ -experimental/Security/CWE/CWE-094/JPythonInjection.ql \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/JythonInjection.expected b/java/ql/test/experimental/query-tests/security/CWE-094/JythonInjection.expected new file mode 100644 index 00000000000..4f66cc83fbd --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-094/JythonInjection.expected @@ -0,0 +1,21 @@ +edges +| JythonInjection.java:28:23:28:50 | getParameter(...) : String | JythonInjection.java:36:30:36:33 | code | +| JythonInjection.java:53:23:53:50 | getParameter(...) : String | JythonInjection.java:58:44:58:47 | code | +| JythonInjection.java:73:23:73:50 | getParameter(...) : String | JythonInjection.java:81:35:81:38 | code | +| JythonInjection.java:97:23:97:50 | getParameter(...) : String | JythonInjection.java:106:61:106:75 | getBytes(...) | +nodes +| JythonInjection.java:28:23:28:50 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JythonInjection.java:36:30:36:33 | code | semmle.label | code | +| JythonInjection.java:53:23:53:50 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JythonInjection.java:58:44:58:47 | code | semmle.label | code | +| JythonInjection.java:73:23:73:50 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JythonInjection.java:81:35:81:38 | code | semmle.label | code | +| JythonInjection.java:97:23:97:50 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JythonInjection.java:106:61:106:75 | getBytes(...) | semmle.label | getBytes(...) | +| JythonInjection.java:131:40:131:63 | getInputStream(...) | semmle.label | getInputStream(...) | +#select +| JythonInjection.java:36:13:36:34 | exec(...) | JythonInjection.java:28:23:28:50 | getParameter(...) : String | JythonInjection.java:36:30:36:33 | code | Jython evaluate $@. | JythonInjection.java:28:23:28:50 | getParameter(...) | user input | +| JythonInjection.java:58:27:58:48 | eval(...) | JythonInjection.java:53:23:53:50 | getParameter(...) : String | JythonInjection.java:58:44:58:47 | code | Jython evaluate $@. | JythonInjection.java:53:23:53:50 | getParameter(...) | user input | +| JythonInjection.java:81:13:81:39 | runsource(...) | JythonInjection.java:73:23:73:50 | getParameter(...) : String | JythonInjection.java:81:35:81:38 | code | Jython evaluate $@. | JythonInjection.java:73:23:73:50 | getParameter(...) | user input | +| JythonInjection.java:106:29:106:134 | makeCode(...) | JythonInjection.java:97:23:97:50 | getParameter(...) : String | JythonInjection.java:106:61:106:75 | getBytes(...) | Jython evaluate $@. | JythonInjection.java:97:23:97:50 | getParameter(...) | user input | +| JythonInjection.java:131:29:131:109 | compile(...) | JythonInjection.java:131:40:131:63 | getInputStream(...) | JythonInjection.java:131:40:131:63 | getInputStream(...) | Jython evaluate $@. | JythonInjection.java:131:40:131:63 | getInputStream(...) | user input | diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/JythonInjection.java b/java/ql/test/experimental/query-tests/security/CWE-094/JythonInjection.java new file mode 100644 index 00000000000..682e8af5113 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-094/JythonInjection.java @@ -0,0 +1,144 @@ +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.python.core.BytecodeLoader; +import org.python.core.Py; +import org.python.core.PyCode; +import org.python.core.PyException; +import org.python.core.PyObject; +import org.python.util.InteractiveInterpreter; +import org.python.util.PythonInterpreter; + +public class JythonInjection extends HttpServlet { + private static final long serialVersionUID = 1L; + + public JythonInjection() { + super(); + } + + // BAD: allow arbitrary Jython expression to execute + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/plain"); + String code = request.getParameter("code"); + PythonInterpreter interpreter = null; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + try { + interpreter = new PythonInterpreter(); + interpreter.setOut(out); + interpreter.setErr(out); + interpreter.exec(code); + out.flush(); + + response.getWriter().print(out.toString()); + } catch(PyException ex) { + response.getWriter().println(ex.getMessage()); + } finally { + if (interpreter != null) { + interpreter.close(); + } + out.close(); + } + } + + // BAD: allow arbitrary Jython expression to evaluate + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/plain"); + String code = request.getParameter("code"); + PythonInterpreter interpreter = null; + + try { + interpreter = new PythonInterpreter(); + PyObject py = interpreter.eval(code); + + response.getWriter().print(py.toString()); + } catch(PyException ex) { + response.getWriter().println(ex.getMessage()); + } finally { + if (interpreter != null) { + interpreter.close(); + } + } + } + + // BAD: allow arbitrary Jython expression to run + protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/plain"); + String code = request.getParameter("code"); + InteractiveInterpreter interpreter = null; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + try { + interpreter = new InteractiveInterpreter(); + interpreter.setOut(out); + interpreter.setErr(out); + interpreter.runsource(code); + out.flush(); + + response.getWriter().print(out.toString()); + } catch(PyException ex) { + response.getWriter().println(ex.getMessage()); + } finally { + if (interpreter != null) { + interpreter.close(); + } + } + } + + // BAD: load arbitrary class file to execute + protected void doTrace(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/plain"); + String code = request.getParameter("code"); + PythonInterpreter interpreter = null; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + try { + interpreter = new PythonInterpreter(); + interpreter.setOut(out); + interpreter.setErr(out); + + PyCode pyCode = BytecodeLoader.makeCode("test", code.getBytes(), getServletContext().getRealPath("/com/example/test.pyc")); + interpreter.exec(pyCode); + out.flush(); + + response.getWriter().print(out.toString()); + } catch(PyException ex) { + response.getWriter().println(ex.getMessage()); + } finally { + if (interpreter != null) { + interpreter.close(); + } + } + } + + // BAD: Compile Python code to execute + protected void doHead(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/plain"); + PythonInterpreter interpreter = null; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + try { + interpreter = new PythonInterpreter(); + interpreter.setOut(out); + interpreter.setErr(out); + + PyCode pyCode = Py.compile(request.getInputStream(), "Test.py", org.python.core.CompileMode.eval); + interpreter.exec(pyCode); + out.flush(); + + response.getWriter().print(out.toString()); + } catch(PyException ex) { + response.getWriter().println(ex.getMessage()); + } finally { + if (interpreter != null) { + interpreter.close(); + } + } + } +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/JythonInjection.qlref b/java/ql/test/experimental/query-tests/security/CWE-094/JythonInjection.qlref new file mode 100644 index 00000000000..0ba9fd60621 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-094/JythonInjection.qlref @@ -0,0 +1 @@ +experimental/Security/CWE/CWE-094/JythonInjection.ql \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/options b/java/ql/test/experimental/query-tests/security/CWE-094/options index ccf3a24f215..95bc9acaa08 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-094/options +++ b/java/ql/test/experimental/query-tests/security/CWE-094/options @@ -1,2 +1,2 @@ -//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/springframework-5.2.3:${testdir}/../../../../stubs/mvel2-2.4.7:${testdir}/../../../../stubs/jsr223-api:${testdir}/../../../../stubs/apache-commons-jexl-2.1.1:${testdir}/../../../../stubs/apache-commons-jexl-3.1:${testdir}/../../../../stubs/scriptengine:${testdir}/../../../../stubs/java-ee-el:${testdir}/../../../../stubs/juel-2.2:${testdir}/../../../stubs/groovy-all-3.0.7:${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/jpython-2.7.2 +//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/springframework-5.2.3:${testdir}/../../../../stubs/mvel2-2.4.7:${testdir}/../../../../stubs/jsr223-api:${testdir}/../../../../stubs/apache-commons-jexl-2.1.1:${testdir}/../../../../stubs/apache-commons-jexl-3.1:${testdir}/../../../../stubs/scriptengine:${testdir}/../../../../stubs/java-ee-el:${testdir}/../../../../stubs/juel-2.2:${testdir}/../../../stubs/groovy-all-3.0.7:${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/jython-2.7.2 diff --git a/java/ql/test/stubs/jython-2.7.2/org/python/antlr/base/mod.java b/java/ql/test/stubs/jython-2.7.2/org/python/antlr/base/mod.java new file mode 100644 index 00000000000..785212f62fa --- /dev/null +++ b/java/ql/test/stubs/jython-2.7.2/org/python/antlr/base/mod.java @@ -0,0 +1,5 @@ +// Autogenerated AST node +package org.python.antlr.base; + +public abstract class mod { +} diff --git a/java/ql/test/stubs/jython-2.7.2/org/python/core/BytecodeLoader.java b/java/ql/test/stubs/jython-2.7.2/org/python/core/BytecodeLoader.java new file mode 100644 index 00000000000..e414216ed03 --- /dev/null +++ b/java/ql/test/stubs/jython-2.7.2/org/python/core/BytecodeLoader.java @@ -0,0 +1,47 @@ +// Copyright (c) Corporation for National Research Initiatives +package org.python.core; + +import java.util.List; + +/** + * Utility class for loading compiled Python modules and Java classes defined in Python modules. + */ +public class BytecodeLoader { + + /** + * Turn the Java class file data into a Java class. + * + * @param name fully-qualified binary name of the class + * @param data a class file as a byte array + * @param referents super-classes and interfaces that the new class will reference. + */ + @SuppressWarnings("unchecked") + public static Class makeClass(String name, byte[] data, Class... referents) { + return null; + } + + /** + * Turn the Java class file data into a Java class. + * + * @param name the name of the class + * @param referents super-classes and interfaces that the new class will reference. + * @param data a class file as a byte array + */ + public static Class makeClass(String name, List> referents, byte[] data) { + return null; + } + + /** + * Turn the Java class file data for a compiled Python module into a {@code PyCode} object, by + * constructing an instance of the named class and calling the instance's + * {@link PyRunnable#getMain()}. + * + * @param name fully-qualified binary name of the class + * @param data a class file as a byte array + * @param filename to provide to the constructor of the named class + * @return the {@code PyCode} object produced by the named class' {@code getMain} + */ + public static PyCode makeCode(String name, byte[] data, String filename) { + return null; + } +} diff --git a/java/ql/test/stubs/jython-2.7.2/org/python/core/CompileMode.java b/java/ql/test/stubs/jython-2.7.2/org/python/core/CompileMode.java new file mode 100644 index 00000000000..cf7ad1e7201 --- /dev/null +++ b/java/ql/test/stubs/jython-2.7.2/org/python/core/CompileMode.java @@ -0,0 +1,11 @@ +package org.python.core; + +public enum CompileMode { + eval, + single, + exec; + + public static CompileMode getMode(String mode) { + return null; + } +} diff --git a/java/ql/test/stubs/jython-2.7.2/org/python/core/CompilerFlags.java b/java/ql/test/stubs/jython-2.7.2/org/python/core/CompilerFlags.java new file mode 100644 index 00000000000..916b93c84ea --- /dev/null +++ b/java/ql/test/stubs/jython-2.7.2/org/python/core/CompilerFlags.java @@ -0,0 +1,17 @@ +// At some future point this will also be extended - in conjunction with +// Py#compileFlags - to add +// support for a compiler factory that user code can choose in place of the +// normal compiler. +// (Perhaps a better name might have been "CompilerOptions".) + +package org.python.core; + +import java.io.Serializable; + +public class CompilerFlags implements Serializable { + public CompilerFlags() { + } + + public CompilerFlags(int co_flags) { + } +} diff --git a/java/ql/test/stubs/jython-2.7.2/org/python/core/Py.java b/java/ql/test/stubs/jython-2.7.2/org/python/core/Py.java new file mode 100644 index 00000000000..cc0c9f1e4bd --- /dev/null +++ b/java/ql/test/stubs/jython-2.7.2/org/python/core/Py.java @@ -0,0 +1,134 @@ +// Copyright (c) Corporation for National Research Initiatives +package org.python.core; + +import java.io.InputStream; +import java.io.Serializable; + +import org.python.antlr.base.mod; + +public final class Py { + /** + Convert a given PyObject to an instance of a Java class. + Identical to o.__tojava__(c) except that it will + raise a TypeError if the conversion fails. + @param o the PyObject to convert. + @param c the class to convert it to. + **/ + @SuppressWarnings("unchecked") + public static T tojava(PyObject o, Class c) { + return null; + } + + // ??pending: was @deprecated but is actually used by proxie code. + // Can get rid of it? + public static Object tojava(PyObject o, String s) { + return null; + } + + /** + * Uses the PyObjectAdapter passed to {@link PySystemState#initialize} to turn o into a PyObject. + * + * @see ClassicPyObjectAdapter - default PyObjectAdapter type + */ + public static PyObject java2py(Object o) { + return null; + } + + /** + * Uses the PyObjectAdapter passed to {@link PySystemState#initialize} to turn + * objects into an array of PyObjects. + * + * @see ClassicPyObjectAdapter - default PyObjectAdapter type + */ + public static PyObject[] javas2pys(Object... objects) { + return null; + } + + public static PyObject makeClass(String name, PyObject[] bases, PyCode code, + PyObject[] closure_cells) { + return null; + } + + public static PyObject makeClass(String name, PyObject base, PyObject dict) { + return null; + } + + /** + * Create a new Python class. + * + * @param name the String name of the class + * @param bases an array of PyObject base classes + * @param dict the class's namespace, containing the class body + * definition + * @return a new Python Class PyObject + */ + public static PyObject makeClass(String name, PyObject[] bases, PyObject dict) { + return null; + } + + public static CompilerFlags getCompilerFlags() { + return null; + } + + public static CompilerFlags getCompilerFlags(int flags, boolean dont_inherit) { + return null; + } + + public static CompilerFlags getCompilerFlags(CompilerFlags flags, boolean dont_inherit) { + return null; + } + + // w/o compiler-flags + public static PyCode compile(InputStream istream, String filename, CompileMode kind) { + return null; + } + + /** + * Entry point for compiling modules. + * + * @param node Module node, coming from the parsing process + * @param name Internal name for the compiled code. Typically generated by + * calling {@link #getName()}. + * @param filename Source file name + * @param linenumbers True to track source line numbers on the generated + * code + * @param printResults True to call the sys.displayhook on the result of + * the code + * @param cflags Compiler flags + * @return Code object for the compiled module + */ + public static PyCode compile_flags(mod node, String name, String filename, + boolean linenumbers, boolean printResults, + CompilerFlags cflags) { + return null; + } + + public static PyCode compile_flags(mod node, String filename, + CompileMode kind, CompilerFlags cflags) { + return null; + } + + /** + * Compiles python source code coming from a file or another external stream + */ + public static PyCode compile_flags(InputStream istream, String filename, + CompileMode kind, CompilerFlags cflags) { + return null; + } + + /** + * Compiles python source code coming from String (raw bytes) data. + * + * If the String is properly decoded (from PyUnicode) the PyCF_SOURCE_IS_UTF8 flag + * should be specified. + */ + public static PyCode compile_flags(String data, String filename, + CompileMode kind, CompilerFlags cflags) { + return null; + } + + public static PyObject compile_command_flags(String string, String filename, + CompileMode kind, CompilerFlags cflags, boolean stdprompt) { + return null; + } +} diff --git a/java/ql/test/stubs/jpython-2.7.2/org/python/core/PyCode.java b/java/ql/test/stubs/jython-2.7.2/org/python/core/PyCode.java similarity index 100% rename from java/ql/test/stubs/jpython-2.7.2/org/python/core/PyCode.java rename to java/ql/test/stubs/jython-2.7.2/org/python/core/PyCode.java diff --git a/java/ql/test/stubs/jpython-2.7.2/org/python/core/PyException.java b/java/ql/test/stubs/jython-2.7.2/org/python/core/PyException.java similarity index 100% rename from java/ql/test/stubs/jpython-2.7.2/org/python/core/PyException.java rename to java/ql/test/stubs/jython-2.7.2/org/python/core/PyException.java diff --git a/java/ql/test/stubs/jpython-2.7.2/org/python/core/PyObject.java b/java/ql/test/stubs/jython-2.7.2/org/python/core/PyObject.java similarity index 100% rename from java/ql/test/stubs/jpython-2.7.2/org/python/core/PyObject.java rename to java/ql/test/stubs/jython-2.7.2/org/python/core/PyObject.java diff --git a/java/ql/test/stubs/jpython-2.7.2/org/python/core/PySystemState.java b/java/ql/test/stubs/jython-2.7.2/org/python/core/PySystemState.java similarity index 100% rename from java/ql/test/stubs/jpython-2.7.2/org/python/core/PySystemState.java rename to java/ql/test/stubs/jython-2.7.2/org/python/core/PySystemState.java diff --git a/java/ql/test/stubs/jpython-2.7.2/org/python/core/ThreadState.java b/java/ql/test/stubs/jython-2.7.2/org/python/core/ThreadState.java similarity index 100% rename from java/ql/test/stubs/jpython-2.7.2/org/python/core/ThreadState.java rename to java/ql/test/stubs/jython-2.7.2/org/python/core/ThreadState.java diff --git a/java/ql/test/stubs/jython-2.7.2/org/python/util/InteractiveInterpreter.java b/java/ql/test/stubs/jython-2.7.2/org/python/util/InteractiveInterpreter.java new file mode 100644 index 00000000000..b12fa617227 --- /dev/null +++ b/java/ql/test/stubs/jython-2.7.2/org/python/util/InteractiveInterpreter.java @@ -0,0 +1,114 @@ +// Copyright (c) Corporation for National Research Initiatives +package org.python.util; + +import org.python.core.*; + +/** + * This class provides the interface for compiling and running code that supports an interactive + * interpreter. + */ +// Based on CPython-1.5.2's code module +public class InteractiveInterpreter extends PythonInterpreter { + + /** + * Construct an InteractiveInterpreter with all default characteristics: default state (from + * {@link Py#getSystemState()}), and a new empty dictionary of local variables. + * */ + public InteractiveInterpreter() { + } + + /** + * Construct an InteractiveInterpreter with state (from {@link Py#getSystemState()}), and the + * specified dictionary of local variables. + * + * @param locals dictionary to use, or if null, a new empty one will be created + */ + public InteractiveInterpreter(PyObject locals) { + } + + /** + * Construct an InteractiveInterpreter with, and system state the specified dictionary of local + * variables. + * + * @param locals dictionary to use, or if null, a new empty one will be created + * @param systemState interpreter state, or if null use {@link Py#getSystemState()} + */ + public InteractiveInterpreter(PyObject locals, PySystemState systemState) { + } + + /** + * Compile and run some source in the interpreter, in the mode {@link CompileMode#single} which + * is used for incremental compilation at the interactive console, known as {@code }. + * + * @param source Python code + * @return true to indicate a partial statement was entered + */ + public boolean runsource(String source) { + return false; + } + + /** + * Compile and run some source in the interpreter, in the mode {@link CompileMode#single} which + * is used for incremental compilation at the interactive console. + * + * @param source Python code + * @param filename name with which to label this console input (e.g. in error messages). + * @return true to indicate a partial statement was entered + */ + public boolean runsource(String source, String filename) { + return false; + } + + /** + * Compile and run some source in the interpreter, according to the {@link CompileMode} given. + * This method supports incremental compilation and interpretation through the return value, + * where {@code true} signifies that more input is expected in order to complete the Python + * statement. An interpreter can use this to decide whether to use {@code sys.ps1} + * ("{@code >>> }") or {@code sys.ps2} ("{@code ... }") to prompt the next line. The arguments + * are the same as the mandatory ones in the Python {@code compile()} command. + *

    + * One the following can happen: + *

      + *
    1. The input is incorrect; compilation raised an exception (SyntaxError or OverflowError). A + * syntax traceback will be printed by calling {@link #showexception(PyException)}. Return is + * {@code false}.
    2. + * + *
    3. The input is incomplete, and more input is required; compilation returned no code. + * Nothing happens. Return is {@code true}.
    4. + * + *
    5. The input is complete; compilation returned a code object. The code is executed by + * calling {@link #runcode(PyObject)} (which also handles run-time exceptions, except for + * SystemExit). Return is {@code false}.
    6. + *
    + * + * @param source Python code + * @param filename name with which to label this console input (e.g. in error messages). + * @param kind of compilation required: {@link CompileMode#eval}, {@link CompileMode#exec} or + * {@link CompileMode#single} + * @return {@code true} to indicate a partial statement was provided + */ + public boolean runsource(String source, String filename, CompileMode kind) { + return false; + } + + /** + * Execute a code object. When an exception occurs, {@link #showexception(PyException)} is + * called to display a stack trace, except in the case of SystemExit, which is re-raised. + *

    + * A note about KeyboardInterrupt: this exception may occur elsewhere in this code, and may not + * always be caught. The caller should be prepared to deal with it. + **/ + + // Make this run in another thread somehow???? + public void runcode(PyObject code) { + } + + public void showexception(PyException exc) { + } + + public void write(String data) { + } + + public void resetbuffer() { + } +} diff --git a/java/ql/test/stubs/jpython-2.7.2/org/python/util/PythonInterpreter.java b/java/ql/test/stubs/jython-2.7.2/org/python/util/PythonInterpreter.java similarity index 100% rename from java/ql/test/stubs/jpython-2.7.2/org/python/util/PythonInterpreter.java rename to java/ql/test/stubs/jython-2.7.2/org/python/util/PythonInterpreter.java From 7ed20a8b2c07be79f5ba5936d52870fd2c21940f Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Mon, 10 May 2021 10:55:21 +0200 Subject: [PATCH 03/74] Python: Add reminder to update docs for new frameworks --- python/ql/src/semmle/python/Frameworks.qll | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/ql/src/semmle/python/Frameworks.qll b/python/ql/src/semmle/python/Frameworks.qll index a00511ca545..1c8ef1f551b 100644 --- a/python/ql/src/semmle/python/Frameworks.qll +++ b/python/ql/src/semmle/python/Frameworks.qll @@ -2,6 +2,8 @@ * Helper file that imports all framework modeling. */ +// If you add modeling of a new framework/library, remember to add it it to the docs in +// `docs/codeql/support/reusables/frameworks.rst` private import semmle.python.frameworks.Cryptodome private import semmle.python.frameworks.Cryptography private import semmle.python.frameworks.Dill From 8afdf2654035bcc38eb4c343572f94d0b7d4560d Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Mon, 10 May 2021 10:57:33 +0200 Subject: [PATCH 04/74] Python: Add modeling of idna PyPI package --- docs/codeql/support/reusables/frameworks.rst | 1 + .../2021-05-10-idna-add-modeling.md | 2 + python/ql/src/semmle/python/Frameworks.qll | 1 + .../ql/src/semmle/python/frameworks/Idna.qll | 40 +++++++++++++++++++ .../frameworks/idna/ConceptsTest.expected | 0 .../frameworks/idna/ConceptsTest.ql | 2 + .../frameworks/idna/InlineTaintTest.expected | 3 ++ .../frameworks/idna/InlineTaintTest.ql | 1 + .../frameworks/idna/taint_test.py | 13 ++++++ 9 files changed, 63 insertions(+) create mode 100644 python/change-notes/2021-05-10-idna-add-modeling.md create mode 100644 python/ql/src/semmle/python/frameworks/Idna.qll create mode 100644 python/ql/test/library-tests/frameworks/idna/ConceptsTest.expected create mode 100644 python/ql/test/library-tests/frameworks/idna/ConceptsTest.ql create mode 100644 python/ql/test/library-tests/frameworks/idna/InlineTaintTest.expected create mode 100644 python/ql/test/library-tests/frameworks/idna/InlineTaintTest.ql create mode 100644 python/ql/test/library-tests/frameworks/idna/taint_test.py diff --git a/docs/codeql/support/reusables/frameworks.rst b/docs/codeql/support/reusables/frameworks.rst index afdf63bdb7b..d54c07454bc 100644 --- a/docs/codeql/support/reusables/frameworks.rst +++ b/docs/codeql/support/reusables/frameworks.rst @@ -158,6 +158,7 @@ Python built-in support dill, Serialization fabric, Utility library invoke, Utility library + idna, Utility library mysql-connector-python, Database MySQLdb, Database psycopg2, Database diff --git a/python/change-notes/2021-05-10-idna-add-modeling.md b/python/change-notes/2021-05-10-idna-add-modeling.md new file mode 100644 index 00000000000..95856ffe5a8 --- /dev/null +++ b/python/change-notes/2021-05-10-idna-add-modeling.md @@ -0,0 +1,2 @@ +lgtm,codescanning +* Added modeling of the PyPI package `idna`, for encoding/decoding Internationalised Domain Names in Applications. diff --git a/python/ql/src/semmle/python/Frameworks.qll b/python/ql/src/semmle/python/Frameworks.qll index 1c8ef1f551b..ed8f308c70a 100644 --- a/python/ql/src/semmle/python/Frameworks.qll +++ b/python/ql/src/semmle/python/Frameworks.qll @@ -10,6 +10,7 @@ private import semmle.python.frameworks.Dill private import semmle.python.frameworks.Django private import semmle.python.frameworks.Fabric private import semmle.python.frameworks.Flask +private import semmle.python.frameworks.Idna private import semmle.python.frameworks.Invoke private import semmle.python.frameworks.MysqlConnectorPython private import semmle.python.frameworks.MySQLdb diff --git a/python/ql/src/semmle/python/frameworks/Idna.qll b/python/ql/src/semmle/python/frameworks/Idna.qll new file mode 100644 index 00000000000..331cafbd084 --- /dev/null +++ b/python/ql/src/semmle/python/frameworks/Idna.qll @@ -0,0 +1,40 @@ +/** + * Provides classes modeling security-relevant aspects of the `idna` PyPI package. + * See https://pypi.org/project/idna/. + */ + +private import python +private import semmle.python.dataflow.new.DataFlow +private import semmle.python.dataflow.new.TaintTracking +private import semmle.python.Concepts +private import semmle.python.ApiGraphs + +/** + * Provides models for the `idna` PyPI package. + * See https://pypi.org/project/idna/. + */ +private module IdnaModel { + /** A call to `idna.encode`. */ + private class IdnaEncodeCall extends Encoding::Range, DataFlow::CallCfgNode { + IdnaEncodeCall() { this = API::moduleImport("idna").getMember("encode").getACall() } + + override DataFlow::Node getAnInput() { result = [this.getArg(0), this.getArgByName("s")] } + + override DataFlow::Node getOutput() { result = this } + + override string getFormat() { result = "IDNA" } + } + + /** A call to `idna.decode`. */ + private class IdnaDecodeCall extends Decoding::Range, DataFlow::CallCfgNode { + IdnaDecodeCall() { this = API::moduleImport("idna").getMember("decode").getACall() } + + override DataFlow::Node getAnInput() { result = [this.getArg(0), this.getArgByName("s")] } + + override DataFlow::Node getOutput() { result = this } + + override string getFormat() { result = "IDNA" } + + override predicate mayExecuteInput() { none() } + } +} diff --git a/python/ql/test/library-tests/frameworks/idna/ConceptsTest.expected b/python/ql/test/library-tests/frameworks/idna/ConceptsTest.expected new file mode 100644 index 00000000000..e69de29bb2d diff --git a/python/ql/test/library-tests/frameworks/idna/ConceptsTest.ql b/python/ql/test/library-tests/frameworks/idna/ConceptsTest.ql new file mode 100644 index 00000000000..b557a0bccb6 --- /dev/null +++ b/python/ql/test/library-tests/frameworks/idna/ConceptsTest.ql @@ -0,0 +1,2 @@ +import python +import experimental.meta.ConceptsTest diff --git a/python/ql/test/library-tests/frameworks/idna/InlineTaintTest.expected b/python/ql/test/library-tests/frameworks/idna/InlineTaintTest.expected new file mode 100644 index 00000000000..79d760d87f4 --- /dev/null +++ b/python/ql/test/library-tests/frameworks/idna/InlineTaintTest.expected @@ -0,0 +1,3 @@ +argumentToEnsureNotTaintedNotMarkedAsSpurious +untaintedArgumentToEnsureTaintedNotMarkedAsMissing +failures diff --git a/python/ql/test/library-tests/frameworks/idna/InlineTaintTest.ql b/python/ql/test/library-tests/frameworks/idna/InlineTaintTest.ql new file mode 100644 index 00000000000..027ad8667be --- /dev/null +++ b/python/ql/test/library-tests/frameworks/idna/InlineTaintTest.ql @@ -0,0 +1 @@ +import experimental.meta.InlineTaintTest diff --git a/python/ql/test/library-tests/frameworks/idna/taint_test.py b/python/ql/test/library-tests/frameworks/idna/taint_test.py new file mode 100644 index 00000000000..61b9dad166c --- /dev/null +++ b/python/ql/test/library-tests/frameworks/idna/taint_test.py @@ -0,0 +1,13 @@ +import idna + +def test_idna(): + ts = TAINTED_STRING + tb = TAINTED_BYTES + + ensure_tainted( + idna.encode(ts), # $ tainted encodeInput=ts encodeOutput=Attribute() encodeFormat=IDNA + idna.encode(s=ts), # $ tainted encodeInput=ts encodeOutput=Attribute() encodeFormat=IDNA + + idna.decode(tb), # $ tainted decodeInput=tb decodeOutput=Attribute() decodeFormat=IDNA + idna.decode(s=tb), # $ tainted decodeInput=tb decodeOutput=Attribute() decodeFormat=IDNA + ) From 3e5dc1efb7ea527e8b2d7b8149ce2f5dbc76a2ef Mon Sep 17 00:00:00 2001 From: Asger Feldthaus Date: Mon, 10 May 2021 13:17:25 +0200 Subject: [PATCH 05/74] JS: More robust hasUnderlyingType --- javascript/ql/src/semmle/javascript/TypeScript.qll | 2 +- javascript/ql/src/semmle/javascript/dataflow/DataFlow.qll | 1 - .../TypeScript/HasUnderlyingType/HasUnderlyingType.expected | 4 ++++ .../TypeScript/HasUnderlyingType/HasUnderlyingType.ql | 4 ++++ .../test/library-tests/TypeScript/HasUnderlyingType/foo.ts | 5 +++++ 5 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 javascript/ql/test/library-tests/TypeScript/HasUnderlyingType/foo.ts diff --git a/javascript/ql/src/semmle/javascript/TypeScript.qll b/javascript/ql/src/semmle/javascript/TypeScript.qll index 4fe263c8389..0b611140449 100644 --- a/javascript/ql/src/semmle/javascript/TypeScript.qll +++ b/javascript/ql/src/semmle/javascript/TypeScript.qll @@ -725,7 +725,7 @@ class TypeAccess extends @typeaccess, TypeExpr, TypeRef { spec.getImportedName() = exportedName and this = spec.getLocal().(TypeDecl).getLocalTypeName().getAnAccess() or - spec instanceof ImportNamespaceSpecifier and + (spec instanceof ImportNamespaceSpecifier or spec instanceof ImportDefaultSpecifier) and this = spec.getLocal().(LocalNamespaceDecl).getLocalNamespaceName().getAMemberAccess(exportedName) ) diff --git a/javascript/ql/src/semmle/javascript/dataflow/DataFlow.qll b/javascript/ql/src/semmle/javascript/dataflow/DataFlow.qll index c73c894ca4b..f02e9b0f287 100644 --- a/javascript/ql/src/semmle/javascript/dataflow/DataFlow.qll +++ b/javascript/ql/src/semmle/javascript/dataflow/DataFlow.qll @@ -239,7 +239,6 @@ module DataFlow { private TypeAnnotation getFallbackTypeAnnotation() { exists(BindingPattern pattern | this = valueNode(pattern) and - not ast_node_type(pattern, _) and result = pattern.getTypeAnnotation() ) or diff --git a/javascript/ql/test/library-tests/TypeScript/HasUnderlyingType/HasUnderlyingType.expected b/javascript/ql/test/library-tests/TypeScript/HasUnderlyingType/HasUnderlyingType.expected index 95c6d7e03d9..47e57e0a242 100644 --- a/javascript/ql/test/library-tests/TypeScript/HasUnderlyingType/HasUnderlyingType.expected +++ b/javascript/ql/test/library-tests/TypeScript/HasUnderlyingType/HasUnderlyingType.expected @@ -1,2 +1,6 @@ +underlyingTypeNode +| foo | Bar | foo.ts:3:1:5:1 | use (instance (member Bar (member exports (module foo)))) | +| foo | Bar | foo.ts:3:12:3:12 | use (instance (member Bar (member exports (module foo)))) | +#select | tst.ts:8:14:8:16 | arg | Base in global scope | | tst.ts:8:14:8:16 | arg | Sub in global scope | diff --git a/javascript/ql/test/library-tests/TypeScript/HasUnderlyingType/HasUnderlyingType.ql b/javascript/ql/test/library-tests/TypeScript/HasUnderlyingType/HasUnderlyingType.ql index 77e311b3b76..72d4e6d0f3d 100644 --- a/javascript/ql/test/library-tests/TypeScript/HasUnderlyingType/HasUnderlyingType.ql +++ b/javascript/ql/test/library-tests/TypeScript/HasUnderlyingType/HasUnderlyingType.ql @@ -3,3 +3,7 @@ import javascript from Expr e, TypeName typeName where e.getType().hasUnderlyingTypeName(typeName) select e, typeName + +query API::Node underlyingTypeNode(string mod, string name) { + result = API::Node::ofType(mod, name) +} diff --git a/javascript/ql/test/library-tests/TypeScript/HasUnderlyingType/foo.ts b/javascript/ql/test/library-tests/TypeScript/HasUnderlyingType/foo.ts new file mode 100644 index 00000000000..1b5be79068a --- /dev/null +++ b/javascript/ql/test/library-tests/TypeScript/HasUnderlyingType/foo.ts @@ -0,0 +1,5 @@ +import foo from "foo"; + +function f(x: foo.Bar) { + return x; +} From 3fe9a3d9334699a4db47ebd6ee9e5c3d2640cd45 Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Mon, 10 May 2021 12:15:40 +0200 Subject: [PATCH 06/74] Python: Add modeling of simplejson PyPI package I noticed that we don't handle PostUpdateNote very well in the concept tests, for exmaple for `json.dump(...)` there _should_ have been an `encodeOutput` as part of the inline expectations. I'll work on fixing that up in a separate PR, to keep things clean. --- docs/codeql/support/reusables/frameworks.rst | 1 + .../2021-05-10-simplejson-add-modeling.md | 2 + python/ql/src/semmle/python/Frameworks.qll | 1 + .../semmle/python/frameworks/Simplejson.qll | 84 +++++++++++++++++++ .../simplejson/ConceptsTest.expected | 0 .../frameworks/simplejson/ConceptsTest.ql | 2 + .../simplejson/InlineTaintTest.expected | 3 + .../frameworks/simplejson/InlineTaintTest.ql | 1 + .../frameworks/simplejson/taint_test.py | 46 ++++++++++ 9 files changed, 140 insertions(+) create mode 100644 python/change-notes/2021-05-10-simplejson-add-modeling.md create mode 100644 python/ql/src/semmle/python/frameworks/Simplejson.qll create mode 100644 python/ql/test/library-tests/frameworks/simplejson/ConceptsTest.expected create mode 100644 python/ql/test/library-tests/frameworks/simplejson/ConceptsTest.ql create mode 100644 python/ql/test/library-tests/frameworks/simplejson/InlineTaintTest.expected create mode 100644 python/ql/test/library-tests/frameworks/simplejson/InlineTaintTest.ql create mode 100644 python/ql/test/library-tests/frameworks/simplejson/taint_test.py diff --git a/docs/codeql/support/reusables/frameworks.rst b/docs/codeql/support/reusables/frameworks.rst index d54c07454bc..4ea461ed435 100644 --- a/docs/codeql/support/reusables/frameworks.rst +++ b/docs/codeql/support/reusables/frameworks.rst @@ -156,6 +156,7 @@ Python built-in support Tornado, Web framework PyYAML, Serialization dill, Serialization + simplejson, Serialization fabric, Utility library invoke, Utility library idna, Utility library diff --git a/python/change-notes/2021-05-10-simplejson-add-modeling.md b/python/change-notes/2021-05-10-simplejson-add-modeling.md new file mode 100644 index 00000000000..910441bdeac --- /dev/null +++ b/python/change-notes/2021-05-10-simplejson-add-modeling.md @@ -0,0 +1,2 @@ +lgtm,codescanning +* Added modeling of the PyPI package `simplejson`. diff --git a/python/ql/src/semmle/python/Frameworks.qll b/python/ql/src/semmle/python/Frameworks.qll index ed8f308c70a..bec73652394 100644 --- a/python/ql/src/semmle/python/Frameworks.qll +++ b/python/ql/src/semmle/python/Frameworks.qll @@ -16,6 +16,7 @@ private import semmle.python.frameworks.MysqlConnectorPython private import semmle.python.frameworks.MySQLdb private import semmle.python.frameworks.Psycopg2 private import semmle.python.frameworks.PyMySQL +private import semmle.python.frameworks.Simplejson private import semmle.python.frameworks.Stdlib private import semmle.python.frameworks.Tornado private import semmle.python.frameworks.Yaml diff --git a/python/ql/src/semmle/python/frameworks/Simplejson.qll b/python/ql/src/semmle/python/frameworks/Simplejson.qll new file mode 100644 index 00000000000..41676705a40 --- /dev/null +++ b/python/ql/src/semmle/python/frameworks/Simplejson.qll @@ -0,0 +1,84 @@ +/** + * Provides classes modeling security-relevant aspects of the `simplejson` PyPI package. + * See https://simplejson.readthedocs.io/en/latest/. + */ + +private import python +private import semmle.python.dataflow.new.DataFlow +private import semmle.python.dataflow.new.TaintTracking +private import semmle.python.Concepts +private import semmle.python.ApiGraphs + +/** + * Provides models for the `simplejson` PyPI package. + * See https://simplejson.readthedocs.io/en/latest/. + */ +private module SimplejsonModel { + /** + * A call to `simplejson.dumps`. + * + * See https://simplejson.readthedocs.io/en/latest/#simplejson.dumps + */ + private class SimplejsonDumpsCall extends Encoding::Range, DataFlow::CallCfgNode { + SimplejsonDumpsCall() { this = API::moduleImport("simplejson").getMember("dumps").getACall() } + + override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("obj")] } + + override DataFlow::Node getOutput() { result = this } + + override string getFormat() { result = "JSON" } + } + + /** + * A call to `simplejson.dump`. + * + * See https://simplejson.readthedocs.io/en/latest/#simplejson.dump + */ + private class SimplejsonDumpCall extends Encoding::Range, DataFlow::CallCfgNode { + SimplejsonDumpCall() { this = API::moduleImport("simplejson").getMember("dump").getACall() } + + override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("obj")] } + + override DataFlow::Node getOutput() { + result.(DataFlow::PostUpdateNode).getPreUpdateNode() in [ + this.getArg(1), this.getArgByName("fp") + ] + } + + override string getFormat() { result = "JSON" } + } + + /** + * A call to `simplejson.loads`. + * + * See https://simplejson.readthedocs.io/en/latest/#simplejson.loads + */ + private class SimplejsonLoadsCall extends Decoding::Range, DataFlow::CallCfgNode { + SimplejsonLoadsCall() { this = API::moduleImport("simplejson").getMember("loads").getACall() } + + override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("s")] } + + override DataFlow::Node getOutput() { result = this } + + override string getFormat() { result = "JSON" } + + override predicate mayExecuteInput() { none() } + } + + /** + * A call to `simplejson.load`. + * + * See https://simplejson.readthedocs.io/en/latest/#simplejson.load + */ + private class SimplejsonLoadCall extends Decoding::Range, DataFlow::CallCfgNode { + SimplejsonLoadCall() { this = API::moduleImport("simplejson").getMember("load").getACall() } + + override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("fp")] } + + override DataFlow::Node getOutput() { result = this } + + override string getFormat() { result = "JSON" } + + override predicate mayExecuteInput() { none() } + } +} diff --git a/python/ql/test/library-tests/frameworks/simplejson/ConceptsTest.expected b/python/ql/test/library-tests/frameworks/simplejson/ConceptsTest.expected new file mode 100644 index 00000000000..e69de29bb2d diff --git a/python/ql/test/library-tests/frameworks/simplejson/ConceptsTest.ql b/python/ql/test/library-tests/frameworks/simplejson/ConceptsTest.ql new file mode 100644 index 00000000000..b557a0bccb6 --- /dev/null +++ b/python/ql/test/library-tests/frameworks/simplejson/ConceptsTest.ql @@ -0,0 +1,2 @@ +import python +import experimental.meta.ConceptsTest diff --git a/python/ql/test/library-tests/frameworks/simplejson/InlineTaintTest.expected b/python/ql/test/library-tests/frameworks/simplejson/InlineTaintTest.expected new file mode 100644 index 00000000000..79d760d87f4 --- /dev/null +++ b/python/ql/test/library-tests/frameworks/simplejson/InlineTaintTest.expected @@ -0,0 +1,3 @@ +argumentToEnsureNotTaintedNotMarkedAsSpurious +untaintedArgumentToEnsureTaintedNotMarkedAsMissing +failures diff --git a/python/ql/test/library-tests/frameworks/simplejson/InlineTaintTest.ql b/python/ql/test/library-tests/frameworks/simplejson/InlineTaintTest.ql new file mode 100644 index 00000000000..027ad8667be --- /dev/null +++ b/python/ql/test/library-tests/frameworks/simplejson/InlineTaintTest.ql @@ -0,0 +1 @@ +import experimental.meta.InlineTaintTest diff --git a/python/ql/test/library-tests/frameworks/simplejson/taint_test.py b/python/ql/test/library-tests/frameworks/simplejson/taint_test.py new file mode 100644 index 00000000000..2b7de24a0c2 --- /dev/null +++ b/python/ql/test/library-tests/frameworks/simplejson/taint_test.py @@ -0,0 +1,46 @@ +import simplejson +from io import StringIO + +def test(): + ts = TAINTED_STRING + tainted_obj = {"foo": ts} + + encoded = simplejson.dumps(tainted_obj) # $ encodeOutput=Attribute() encodeFormat=JSON encodeInput=tainted_obj + + ensure_tainted( + encoded, # $ tainted + simplejson.dumps(tainted_obj), # $ tainted encodeOutput=Attribute() encodeFormat=JSON encodeInput=tainted_obj + simplejson.dumps(obj=tainted_obj), # $ tainted encodeOutput=Attribute() encodeFormat=JSON encodeInput=tainted_obj + simplejson.loads(encoded), # $ tainted decodeOutput=Attribute() decodeFormat=JSON decodeInput=encoded + simplejson.loads(s=encoded), # $ tainted decodeOutput=Attribute() decodeFormat=JSON decodeInput=encoded + ) + + # load/dump with file-like + tainted_filelike = StringIO() + simplejson.dump(tainted_obj, tainted_filelike) # $ encodeFormat=JSON encodeInput=tainted_obj + + tainted_filelike.seek(0) + ensure_tainted( + tainted_filelike, # $ tainted + simplejson.load(tainted_filelike), # $ tainted decodeOutput=Attribute() decodeFormat=JSON decodeInput=tainted_filelike + ) + + # load/dump with file-like using keyword-args + tainted_filelike = StringIO() + simplejson.dump(obj=tainted_obj, fp=tainted_filelike) # $ encodeFormat=JSON encodeInput=tainted_obj + + tainted_filelike.seek(0) + ensure_tainted( + tainted_filelike, # $ tainted + simplejson.load(fp=tainted_filelike), # $ tainted decodeOutput=Attribute() decodeFormat=JSON decodeInput=tainted_filelike + ) + +# To make things runable + +TAINTED_STRING = "TAINTED_STRING" +def ensure_tainted(*args): + print("- ensure_tainted") + for i, arg in enumerate(args): + print("arg {}: {!r}".format(i, arg)) + +test() From 784e0cdb96b1f5dbe72aecc0d595e78cdd6dd085 Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Mon, 10 May 2021 14:28:54 +0200 Subject: [PATCH 07/74] Python: Improve tests of `json` module Inspired by the work on previous commit --- .../defaultAdditionalTaintStep/test_json.py | 45 +++++++------------ 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/python/ql/test/experimental/dataflow/tainttracking/defaultAdditionalTaintStep/test_json.py b/python/ql/test/experimental/dataflow/tainttracking/defaultAdditionalTaintStep/test_json.py index a7398b4d001..46ac4afc219 100644 --- a/python/ql/test/experimental/dataflow/tainttracking/defaultAdditionalTaintStep/test_json.py +++ b/python/ql/test/experimental/dataflow/tainttracking/defaultAdditionalTaintStep/test_json.py @@ -11,54 +11,43 @@ if TYPE_CHECKING: # Actual tests from io import StringIO - -# Workaround for Python3 not having unicode -import sys -if sys.version_info[0] == 3: - unicode = str +import json def test(): print("\n# test") ts = TAINTED_STRING - import json + + encoded = json.dumps(ts) ensure_tainted( + encoded, # $ tainted json.dumps(ts), # $ tainted - json.loads(json.dumps(ts)), # $ tainted + json.dumps(obj=ts), # $ MISSING: tainted + json.loads(encoded), # $ tainted + json.loads(s=encoded), # $ MISSING: tainted ) - # For Python2, need to convert to unicode for StringIO to work - tainted_filelike = StringIO(unicode(json.dumps(ts))) + # load/dump with file-like + tainted_filelike = StringIO() + json.dump(ts, tainted_filelike) + tainted_filelike.seek(0) ensure_tainted( tainted_filelike, # $ MISSING: tainted json.load(tainted_filelike), # $ MISSING: tainted ) -def non_syntacical(): - print("\n# non_syntacical") - ts = TAINTED_STRING - - # a less syntactical approach - from json import load, loads, dumps - - dumps_alias = dumps - - ensure_tainted( - dumps(ts), # $ tainted - dumps_alias(ts), # $ tainted - loads(dumps(ts)), # $ tainted - ) - - # For Python2, need to convert to unicode for StringIO to work - tainted_filelike = StringIO(unicode(dumps(ts))) + # load/dump with file-like using keyword-args + tainted_filelike = StringIO() + json.dump(obj=ts, fp=tainted_filelike) + tainted_filelike.seek(0) ensure_tainted( tainted_filelike, # $ MISSING: tainted - load(tainted_filelike), # $ MISSING: tainted + json.load(fp=tainted_filelike), # $ MISSING: tainted ) + # Make tests runable test() -non_syntacical() From 63f28d7d9b1946b97c1b3150957268ac5e316c9e Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Mon, 10 May 2021 14:33:27 +0200 Subject: [PATCH 08/74] Python: Model keyword args to json loads/dumps --- python/ql/src/semmle/python/frameworks/Stdlib.qll | 4 ++-- .../tainttracking/defaultAdditionalTaintStep/test_json.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/python/ql/src/semmle/python/frameworks/Stdlib.qll b/python/ql/src/semmle/python/frameworks/Stdlib.qll index 07753e9fde5..afc1fb45f25 100644 --- a/python/ql/src/semmle/python/frameworks/Stdlib.qll +++ b/python/ql/src/semmle/python/frameworks/Stdlib.qll @@ -511,7 +511,7 @@ private module Stdlib { override predicate mayExecuteInput() { none() } - override DataFlow::Node getAnInput() { result.asCfgNode() = node.getArg(0) } + override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("s")] } override DataFlow::Node getOutput() { result = this } @@ -525,7 +525,7 @@ private module Stdlib { private class JsonDumpsCall extends Encoding::Range, DataFlow::CallCfgNode { JsonDumpsCall() { this = json().getMember("dumps").getACall() } - override DataFlow::Node getAnInput() { result.asCfgNode() = node.getArg(0) } + override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("obj")] } override DataFlow::Node getOutput() { result = this } diff --git a/python/ql/test/experimental/dataflow/tainttracking/defaultAdditionalTaintStep/test_json.py b/python/ql/test/experimental/dataflow/tainttracking/defaultAdditionalTaintStep/test_json.py index 46ac4afc219..f91bcacd3bc 100644 --- a/python/ql/test/experimental/dataflow/tainttracking/defaultAdditionalTaintStep/test_json.py +++ b/python/ql/test/experimental/dataflow/tainttracking/defaultAdditionalTaintStep/test_json.py @@ -22,9 +22,9 @@ def test(): ensure_tainted( encoded, # $ tainted json.dumps(ts), # $ tainted - json.dumps(obj=ts), # $ MISSING: tainted + json.dumps(obj=ts), # $ tainted json.loads(encoded), # $ tainted - json.loads(s=encoded), # $ MISSING: tainted + json.loads(s=encoded), # $ tainted ) # load/dump with file-like From 72d08f4d6eeea58d96bd8a252203caf687ee93ed Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Mon, 10 May 2021 14:35:33 +0200 Subject: [PATCH 09/74] Python: Model json load/dump --- .../src/semmle/python/frameworks/Stdlib.qll | 34 +++++++++++++++++++ .../defaultAdditionalTaintStep/test_json.py | 8 ++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/python/ql/src/semmle/python/frameworks/Stdlib.qll b/python/ql/src/semmle/python/frameworks/Stdlib.qll index afc1fb45f25..69023b8940a 100644 --- a/python/ql/src/semmle/python/frameworks/Stdlib.qll +++ b/python/ql/src/semmle/python/frameworks/Stdlib.qll @@ -518,6 +518,22 @@ private module Stdlib { override string getFormat() { result = "JSON" } } + /** + * A call to `json.load` + * See https://docs.python.org/3/library/json.html#json.load + */ + private class JsonLoadCall extends Decoding::Range, DataFlow::CallCfgNode { + JsonLoadCall() { this = json().getMember("load").getACall() } + + override predicate mayExecuteInput() { none() } + + override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("fp")] } + + override DataFlow::Node getOutput() { result = this } + + override string getFormat() { result = "JSON" } + } + /** * A call to `json.dumps` * See https://docs.python.org/3/library/json.html#json.dumps @@ -532,6 +548,24 @@ private module Stdlib { override string getFormat() { result = "JSON" } } + /** + * A call to `json.dump` + * See https://docs.python.org/3/library/json.html#json.dump + */ + private class JsonDumpCall extends Encoding::Range, DataFlow::CallCfgNode { + JsonDumpCall() { this = json().getMember("dump").getACall() } + + override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("obj")] } + + override DataFlow::Node getOutput() { + result.(DataFlow::PostUpdateNode).getPreUpdateNode() in [ + this.getArg(1), this.getArgByName("fp") + ] + } + + override string getFormat() { result = "JSON" } + } + // --------------------------------------------------------------------------- // cgi // --------------------------------------------------------------------------- diff --git a/python/ql/test/experimental/dataflow/tainttracking/defaultAdditionalTaintStep/test_json.py b/python/ql/test/experimental/dataflow/tainttracking/defaultAdditionalTaintStep/test_json.py index f91bcacd3bc..b1b0536b03b 100644 --- a/python/ql/test/experimental/dataflow/tainttracking/defaultAdditionalTaintStep/test_json.py +++ b/python/ql/test/experimental/dataflow/tainttracking/defaultAdditionalTaintStep/test_json.py @@ -33,8 +33,8 @@ def test(): tainted_filelike.seek(0) ensure_tainted( - tainted_filelike, # $ MISSING: tainted - json.load(tainted_filelike), # $ MISSING: tainted + tainted_filelike, # $ tainted + json.load(tainted_filelike), # $ tainted ) # load/dump with file-like using keyword-args @@ -43,8 +43,8 @@ def test(): tainted_filelike.seek(0) ensure_tainted( - tainted_filelike, # $ MISSING: tainted - json.load(fp=tainted_filelike), # $ MISSING: tainted + tainted_filelike, # $ tainted + json.load(fp=tainted_filelike), # $ tainted ) From c2a6b811fcb2236c2400a18a54041543aa729dbc Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Mon, 10 May 2021 15:07:55 +0200 Subject: [PATCH 10/74] Python: Add modeling of ujson PyPI package The problem with `tainted_filelike` not having taint, is that in the call `ujson.dump(tainted_obj, tainted_filelike)` there is no PostUpdateNote for `tainted_filelike` :( The reason is that points-to is not able to resolve the call, so none of the clauses in `argumentPreUpdateNode` matches See https://github.com/github/codeql/blob/08731fc6cf4ba6951cd4e8f239eac1f3388d3957/python/ql/src/semmle/python/dataflow/new/internal/DataFlowPrivate.qll#L101-L111 Let's deal with that issue in an other PR though --- docs/codeql/support/reusables/frameworks.rst | 1 + .../2021-05-10-ujson-add-modeling.md | 2 + python/ql/src/semmle/python/Frameworks.qll | 1 + .../ql/src/semmle/python/frameworks/Ujson.qll | 76 +++++++++++++++++++ .../frameworks/ujson/ConceptsTest.expected | 0 .../frameworks/ujson/ConceptsTest.ql | 2 + .../frameworks/ujson/InlineTaintTest.expected | 3 + .../frameworks/ujson/InlineTaintTest.ql | 1 + .../frameworks/ujson/taint_test.py | 44 +++++++++++ 9 files changed, 130 insertions(+) create mode 100644 python/change-notes/2021-05-10-ujson-add-modeling.md create mode 100644 python/ql/src/semmle/python/frameworks/Ujson.qll create mode 100644 python/ql/test/library-tests/frameworks/ujson/ConceptsTest.expected create mode 100644 python/ql/test/library-tests/frameworks/ujson/ConceptsTest.ql create mode 100644 python/ql/test/library-tests/frameworks/ujson/InlineTaintTest.expected create mode 100644 python/ql/test/library-tests/frameworks/ujson/InlineTaintTest.ql create mode 100644 python/ql/test/library-tests/frameworks/ujson/taint_test.py diff --git a/docs/codeql/support/reusables/frameworks.rst b/docs/codeql/support/reusables/frameworks.rst index 4ea461ed435..5bc7d572e7f 100644 --- a/docs/codeql/support/reusables/frameworks.rst +++ b/docs/codeql/support/reusables/frameworks.rst @@ -157,6 +157,7 @@ Python built-in support PyYAML, Serialization dill, Serialization simplejson, Serialization + ujson, Serialization fabric, Utility library invoke, Utility library idna, Utility library diff --git a/python/change-notes/2021-05-10-ujson-add-modeling.md b/python/change-notes/2021-05-10-ujson-add-modeling.md new file mode 100644 index 00000000000..2d7cdc4b68a --- /dev/null +++ b/python/change-notes/2021-05-10-ujson-add-modeling.md @@ -0,0 +1,2 @@ +lgtm,codescanning +* Added modeling of the PyPI package `ujson`. diff --git a/python/ql/src/semmle/python/Frameworks.qll b/python/ql/src/semmle/python/Frameworks.qll index bec73652394..3115c3ffac6 100644 --- a/python/ql/src/semmle/python/Frameworks.qll +++ b/python/ql/src/semmle/python/Frameworks.qll @@ -19,4 +19,5 @@ private import semmle.python.frameworks.PyMySQL private import semmle.python.frameworks.Simplejson private import semmle.python.frameworks.Stdlib private import semmle.python.frameworks.Tornado +private import semmle.python.frameworks.Ujson private import semmle.python.frameworks.Yaml diff --git a/python/ql/src/semmle/python/frameworks/Ujson.qll b/python/ql/src/semmle/python/frameworks/Ujson.qll new file mode 100644 index 00000000000..145f7f883a9 --- /dev/null +++ b/python/ql/src/semmle/python/frameworks/Ujson.qll @@ -0,0 +1,76 @@ +/** + * Provides classes modeling security-relevant aspects of the `ujson` PyPI package. + * See https://pypi.org/project/ujson/. + */ + +private import python +private import semmle.python.dataflow.new.DataFlow +private import semmle.python.dataflow.new.TaintTracking +private import semmle.python.Concepts +private import semmle.python.ApiGraphs + +/** + * Provides models for the `ujson` PyPI package. + * See https://pypi.org/project/ujson/. + */ +private module UjsonModel { + /** + * A call to `usjon.dumps` or `ujson.encode`. + */ + private class UjsonDumpsCall extends Encoding::Range, DataFlow::CallCfgNode { + UjsonDumpsCall() { this = API::moduleImport("ujson").getMember(["dumps", "encode"]).getACall() } + + override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("obj")] } + + override DataFlow::Node getOutput() { result = this } + + override string getFormat() { result = "JSON" } + } + + /** + * A call to `ujson.dump`. + */ + private class UjsonDumpCall extends Encoding::Range, DataFlow::CallCfgNode { + UjsonDumpCall() { this = API::moduleImport("ujson").getMember("dump").getACall() } + + override DataFlow::Node getAnInput() { result = this.getArg(0) } + + override DataFlow::Node getOutput() { + result.(DataFlow::PostUpdateNode).getPreUpdateNode() = this.getArg(1) + } + + override string getFormat() { result = "JSON" } + } + + /** + * A call to `ujson.loads` or `ujson.decode`. + */ + private class UjsonLoadsCall extends Decoding::Range, DataFlow::CallCfgNode { + UjsonLoadsCall() { this = API::moduleImport("ujson").getMember(["loads", "decode"]).getACall() } + + // Note: Most other JSON libraries allow the keyword argument `s`, but as of version + // 4.0.2 `ujson` uses `obj` instead. + override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("obj")] } + + override DataFlow::Node getOutput() { result = this } + + override string getFormat() { result = "JSON" } + + override predicate mayExecuteInput() { none() } + } + + /** + * A call to `ujson.load`. + */ + private class UjsonLoadCall extends Decoding::Range, DataFlow::CallCfgNode { + UjsonLoadCall() { this = API::moduleImport("ujson").getMember("load").getACall() } + + override DataFlow::Node getAnInput() { result = this.getArg(0) } + + override DataFlow::Node getOutput() { result = this } + + override string getFormat() { result = "JSON" } + + override predicate mayExecuteInput() { none() } + } +} diff --git a/python/ql/test/library-tests/frameworks/ujson/ConceptsTest.expected b/python/ql/test/library-tests/frameworks/ujson/ConceptsTest.expected new file mode 100644 index 00000000000..e69de29bb2d diff --git a/python/ql/test/library-tests/frameworks/ujson/ConceptsTest.ql b/python/ql/test/library-tests/frameworks/ujson/ConceptsTest.ql new file mode 100644 index 00000000000..b557a0bccb6 --- /dev/null +++ b/python/ql/test/library-tests/frameworks/ujson/ConceptsTest.ql @@ -0,0 +1,2 @@ +import python +import experimental.meta.ConceptsTest diff --git a/python/ql/test/library-tests/frameworks/ujson/InlineTaintTest.expected b/python/ql/test/library-tests/frameworks/ujson/InlineTaintTest.expected new file mode 100644 index 00000000000..79d760d87f4 --- /dev/null +++ b/python/ql/test/library-tests/frameworks/ujson/InlineTaintTest.expected @@ -0,0 +1,3 @@ +argumentToEnsureNotTaintedNotMarkedAsSpurious +untaintedArgumentToEnsureTaintedNotMarkedAsMissing +failures diff --git a/python/ql/test/library-tests/frameworks/ujson/InlineTaintTest.ql b/python/ql/test/library-tests/frameworks/ujson/InlineTaintTest.ql new file mode 100644 index 00000000000..027ad8667be --- /dev/null +++ b/python/ql/test/library-tests/frameworks/ujson/InlineTaintTest.ql @@ -0,0 +1 @@ +import experimental.meta.InlineTaintTest diff --git a/python/ql/test/library-tests/frameworks/ujson/taint_test.py b/python/ql/test/library-tests/frameworks/ujson/taint_test.py new file mode 100644 index 00000000000..2c965518393 --- /dev/null +++ b/python/ql/test/library-tests/frameworks/ujson/taint_test.py @@ -0,0 +1,44 @@ +import ujson +from io import StringIO + +def test(): + ts = TAINTED_STRING + tainted_obj = {"foo": ts} + + encoded = ujson.dumps(tainted_obj) # $ encodeOutput=Attribute() encodeFormat=JSON encodeInput=tainted_obj + + ensure_tainted( + encoded, # $ tainted + ujson.dumps(tainted_obj), # $ tainted encodeOutput=Attribute() encodeFormat=JSON encodeInput=tainted_obj + ujson.dumps(obj=tainted_obj), # $ tainted encodeOutput=Attribute() encodeFormat=JSON encodeInput=tainted_obj + ujson.loads(encoded), # $ tainted decodeOutput=Attribute() decodeFormat=JSON decodeInput=encoded + ujson.loads(obj=encoded), # $ tainted decodeOutput=Attribute() decodeFormat=JSON decodeInput=encoded + + ujson.encode(tainted_obj), # $ tainted encodeOutput=Attribute() encodeFormat=JSON encodeInput=tainted_obj + ujson.encode(obj=tainted_obj), # $ tainted encodeOutput=Attribute() encodeFormat=JSON encodeInput=tainted_obj + ujson.decode(encoded), # $ tainted decodeOutput=Attribute() decodeFormat=JSON decodeInput=encoded + ujson.decode(obj=encoded), # $ tainted decodeOutput=Attribute() decodeFormat=JSON decodeInput=encoded + ) + + # load/dump with file-like + tainted_filelike = StringIO() + ujson.dump(tainted_obj, tainted_filelike) # $ encodeFormat=JSON encodeInput=tainted_obj + + tainted_filelike.seek(0) + ensure_tainted( + tainted_filelike, # $ MISSING: tainted + ujson.load(tainted_filelike), # $ decodeOutput=Attribute() decodeFormat=JSON decodeInput=tainted_filelike MISSING: tainted + ) + + # load/dump with file-like using keyword-args does not work in `ujson` + + +# To make things runable + +TAINTED_STRING = "TAINTED_STRING" +def ensure_tainted(*args): + print("- ensure_tainted") + for i, arg in enumerate(args): + print("arg {}: {!r}".format(i, arg)) + +test() From 1b0d5053e7a7a1fccaa463761fc8d7a5e142f7a5 Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Mon, 10 May 2021 16:21:29 +0200 Subject: [PATCH 11/74] Python: simplejson load/dump only works with lib installed Which I had done locally. Problem is the same about not having PostUpdateNode when points-to is not able to resolve the call, so I'm happy to just make CI happy right now, and hopefully we'll get a fix to the underlying problem soon :blush: --- .../library-tests/frameworks/simplejson/taint_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/ql/test/library-tests/frameworks/simplejson/taint_test.py b/python/ql/test/library-tests/frameworks/simplejson/taint_test.py index 2b7de24a0c2..6f5d45b0b33 100644 --- a/python/ql/test/library-tests/frameworks/simplejson/taint_test.py +++ b/python/ql/test/library-tests/frameworks/simplejson/taint_test.py @@ -21,8 +21,8 @@ def test(): tainted_filelike.seek(0) ensure_tainted( - tainted_filelike, # $ tainted - simplejson.load(tainted_filelike), # $ tainted decodeOutput=Attribute() decodeFormat=JSON decodeInput=tainted_filelike + tainted_filelike, # $ MISSING: tainted + simplejson.load(tainted_filelike), # $ decodeOutput=Attribute() decodeFormat=JSON decodeInput=tainted_filelike MISSING: tainted ) # load/dump with file-like using keyword-args @@ -31,8 +31,8 @@ def test(): tainted_filelike.seek(0) ensure_tainted( - tainted_filelike, # $ tainted - simplejson.load(fp=tainted_filelike), # $ tainted decodeOutput=Attribute() decodeFormat=JSON decodeInput=tainted_filelike + tainted_filelike, # $ MISSING: tainted + simplejson.load(fp=tainted_filelike), # $ decodeOutput=Attribute() decodeFormat=JSON decodeInput=tainted_filelike MISSING: tainted ) # To make things runable From 54f191cfe37e136bcc7189b2fb01f44dbb01758b Mon Sep 17 00:00:00 2001 From: Erik Krogh Kristensen Date: Tue, 11 May 2021 11:23:03 +0200 Subject: [PATCH 12/74] add support for rejected promise values in API graphs --- .../ql/src/semmle/javascript/ApiGraphs.qll | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/javascript/ql/src/semmle/javascript/ApiGraphs.qll b/javascript/ql/src/semmle/javascript/ApiGraphs.qll index 385dc1e76d3..590142590a4 100644 --- a/javascript/ql/src/semmle/javascript/ApiGraphs.qll +++ b/javascript/ql/src/semmle/javascript/ApiGraphs.qll @@ -183,6 +183,11 @@ module API { */ Node getPromised() { result = getASuccessor(Label::promised()) } + /** + * Gets a node representing the error wrapped in the `Promise` object represented by this node. + */ + Node getPromisedError() { result = getASuccessor(Label::promisedError()) } + /** * Gets a string representation of the lexicographically least among all shortest access paths * from the root to this node. @@ -468,6 +473,9 @@ module API { or lbl = Label::promised() and PromiseFlow::storeStep(rhs, pred, Promises::valueProp()) + or + lbl = Label::promisedError() and + PromiseFlow::storeStep(rhs, pred, Promises::errorProp()) ) or exists(DataFlow::ClassNode cls, string name | @@ -482,6 +490,12 @@ module API { rhs = f.getAReturn() ) or + exists(DataFlow::FunctionNode f | + base = MkAsyncFuncResult(f) and + lbl = Label::promisedError() and + rhs = f.getExceptionalReturn() + ) + or exists(int i | lbl = Label::parameter(i) and argumentPassing(base, i, rhs) @@ -559,6 +573,9 @@ module API { or lbl = Label::promised() and PromiseFlow::loadStep(pred, ref, Promises::valueProp()) + or + lbl = Label::promisedError() and + PromiseFlow::loadStep(pred, ref, Promises::errorProp()) ) or exists(DataFlow::Node def, DataFlow::FunctionNode fn | @@ -962,6 +979,9 @@ private module Label { /** Gets the `promised` edge label connecting a promise to its contained value. */ string promised() { result = "promised" } + + /** Gets the `promisedError` edge label connecting a promise to its rejected value. */ + string promisedError() { result = "promisedError" } } private class NodeModuleSourcesNodes extends DataFlow::SourceNode::Range { From 52991dc4a1ec07410fa2818776f756c12418f707 Mon Sep 17 00:00:00 2001 From: Erik Krogh Kristensen Date: Tue, 11 May 2021 11:23:51 +0200 Subject: [PATCH 13/74] rewrite the axios model to use API graphs --- .../javascript/frameworks/ClientRequests.qll | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/javascript/ql/src/semmle/javascript/frameworks/ClientRequests.qll b/javascript/ql/src/semmle/javascript/frameworks/ClientRequests.qll index 451ad3f24d2..78f7ec92a16 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/ClientRequests.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/ClientRequests.qll @@ -206,19 +206,14 @@ module ClientRequest { /** * A model of a URL request made using the `axios` library. */ - class AxiosUrlRequest extends ClientRequest::Range { + class AxiosUrlRequest extends ClientRequest::Range, API::CallNode { string method; AxiosUrlRequest() { - exists(string moduleName, DataFlow::SourceNode callee | this = callee.getACall() | - moduleName = "axios" and - ( - callee = DataFlow::moduleImport(moduleName) and method = "request" - or - callee = DataFlow::moduleMember(moduleName, method) and - (method = httpMethodName() or method = "request") - ) - ) + this = API::moduleImport("axios").getACall() and method = "request" + or + this = API::moduleImport("axios").getMember(method).getACall() and + method = [httpMethodName(), "request"] } private int getOptionsArgIndex() { @@ -247,12 +242,10 @@ module ClientRequest { method = "request" and result = getOptionArgument(0, "data") or - (method = "post" or method = "put" or method = "put") and - (result = getArgument(1) or result = getOptionArgument(2, "data")) + method = ["post", "put", "put"] and + result = [getArgument(1), getOptionArgument(2, "data")] or - exists(string name | name = "headers" or name = "params" | - result = getOptionArgument([0 .. 2], name) - ) + result = getOptionArgument([0 .. 2], ["headers", "params"]) } /** Gets the response type from the options passed in. */ From 99e98419dc3e38c06c2cf6ec6136d88efeae7af6 Mon Sep 17 00:00:00 2001 From: Erik Krogh Kristensen Date: Tue, 11 May 2021 11:24:21 +0200 Subject: [PATCH 14/74] add support for error values in an axios client request --- .../semmle/javascript/frameworks/ClientRequests.qll | 5 +++++ .../ClientRequests/ClientRequests.expected | 5 +++++ .../library-tests/frameworks/ClientRequests/tst.js | 13 +++++++++++++ 3 files changed, 23 insertions(+) diff --git a/javascript/ql/src/semmle/javascript/frameworks/ClientRequests.qll b/javascript/ql/src/semmle/javascript/frameworks/ClientRequests.qll index 78f7ec92a16..477f9354d03 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/ClientRequests.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/ClientRequests.qll @@ -268,6 +268,11 @@ module ClientRequest { responseType = getResponseType() and promise = true and result = this + or + responseType = getResponseType() and + promise = false and + result = + getReturn().getPromisedError().getMember("response").getMember("data").getAnImmediateUse() } } diff --git a/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected b/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected index e448b888e12..c6542eb009d 100644 --- a/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected +++ b/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected @@ -87,6 +87,7 @@ test_ClientRequest | tst.js:271:3:271:61 | proxy.w ... 080' }) | | tst.js:274:1:283:2 | httpPro ... true\\n}) | | tst.js:286:20:286:55 | new Web ... :8080') | +| tst.js:296:5:299:6 | axios({ ... \\n }) | test_getADataNode | tst.js:53:5:53:23 | axios({data: data}) | tst.js:53:18:53:21 | data | | tst.js:57:5:57:39 | axios.p ... data2}) | tst.js:57:19:57:23 | data1 | @@ -227,6 +228,8 @@ test_getUrl | tst.js:271:3:271:61 | proxy.w ... 080' }) | tst.js:271:33:271:58 | 'http:/ ... m:8080' | | tst.js:274:1:283:2 | httpPro ... true\\n}) | tst.js:275:13:281:5 | {\\n ... ,\\n } | | tst.js:286:20:286:55 | new Web ... :8080') | tst.js:286:34:286:54 | 'ws://l ... t:8080' | +| tst.js:296:5:299:6 | axios({ ... \\n }) | tst.js:296:11:299:5 | {\\n ... ,\\n } | +| tst.js:296:5:299:6 | axios({ ... \\n }) | tst.js:298:14:298:44 | "http:/ ... -axios" | test_getAResponseDataNode | tst.js:19:5:19:23 | requestPromise(url) | tst.js:19:5:19:23 | requestPromise(url) | text | true | | tst.js:21:5:21:23 | superagent.get(url) | tst.js:21:5:21:23 | superagent.get(url) | stream | true | @@ -294,3 +297,5 @@ test_getAResponseDataNode | tst.js:235:5:237:6 | needle. ... \\n }) | tst.js:235:67:235:70 | resp | fetch.response | false | | tst.js:235:5:237:6 | needle. ... \\n }) | tst.js:235:73:235:76 | body | json | false | | tst.js:286:20:286:55 | new Web ... :8080') | tst.js:291:44:291:53 | event.data | json | false | +| tst.js:296:5:299:6 | axios({ ... \\n }) | tst.js:296:5:299:6 | axios({ ... \\n }) | json | true | +| tst.js:296:5:299:6 | axios({ ... \\n }) | tst.js:303:26:303:42 | err.response.data | json | false | diff --git a/javascript/ql/test/library-tests/frameworks/ClientRequests/tst.js b/javascript/ql/test/library-tests/frameworks/ClientRequests/tst.js index bc15565c072..f284ffaa407 100644 --- a/javascript/ql/test/library-tests/frameworks/ClientRequests/tst.js +++ b/javascript/ql/test/library-tests/frameworks/ClientRequests/tst.js @@ -290,4 +290,17 @@ function webSocket() { socket.addEventListener('message', function (event) { console.log("Data from server: " + event.data); }); +} + +function moreAxios() { + axios({ + method: 'GET', + url: "http://example.org/more-axios", + }).then( + x => res.send(x.data), + (err) => { + const status = err.response.status; + const data = err.response.data; + } + ); } \ No newline at end of file From 56b1f15ddaaeb5b53267293933b3850058678f6c Mon Sep 17 00:00:00 2001 From: Jonathan Leitschuh Date: Fri, 30 Apr 2021 19:18:35 -0400 Subject: [PATCH 15/74] [Java] Add taint tracking through Jackson deserialization --- .../jackson/JacksonSerializability.qll | 19 ++++++++ .../dataflow/taint-jackson/Test.java | 38 +++++++++++++-- .../dataflow/taint-jackson/dataFlow.expected | 48 ------------------- .../dataflow/taint-jackson/dataFlow.ql | 29 ++++++++--- .../jackson/databind/ObjectMapper.java | 4 ++ .../jackson/databind/ObjectReader.java | 45 +++++++++++++++++ 6 files changed, 125 insertions(+), 58 deletions(-) create mode 100644 java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectReader.java diff --git a/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll b/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll index 3356e31d965..c028af71d8d 100644 --- a/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll +++ b/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll @@ -50,6 +50,15 @@ library class JacksonWriteValueMethod extends Method, TaintPreservingCallable { } } +library class JacksonReadValueMethod extends Method, TaintPreservingCallable { + JacksonReadValueMethod() { + getDeclaringType().hasQualifiedName("com.fasterxml.jackson.databind", "ObjectReader") and + hasName("readValue") + } + + override predicate returnsTaintFrom(int arg) { arg = 0 } +} + /** A type whose values are explicitly serialized in a call to a Jackson method. */ library class ExplicitlyWrittenJacksonSerializableType extends JacksonSerializableType { ExplicitlyWrittenJacksonSerializableType() { @@ -135,6 +144,16 @@ class JacksonDeserializableField extends DeserializableField { } } +class JacksonDeserializableFieldAccess extends FieldAccess { + JacksonDeserializableFieldAccess() { getField() instanceof JacksonDeserializableField } +} + +class JacksonDeseializedTaintStep extends AdditionalTaintStep { + override predicate step(DataFlow::Node node1, DataFlow::Node node2) { + node2.asExpr().(JacksonDeserializableFieldAccess).getQualifier() = node1.asExpr() + } +} + /** * A call to the `addMixInAnnotations` or `addMixIn` Jackson method. * diff --git a/java/ql/test/library-tests/dataflow/taint-jackson/Test.java b/java/ql/test/library-tests/dataflow/taint-jackson/Test.java index 2caf9e4ee80..16b223ed2e8 100644 --- a/java/ql/test/library-tests/dataflow/taint-jackson/Test.java +++ b/java/ql/test/library-tests/dataflow/taint-jackson/Test.java @@ -8,28 +8,44 @@ import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.ObjectReader; class Test { + public static class Potato { + private String name; + + private String getName() { + return name; + } + } + public static String taint() { return "tainted"; } + public static void sink(Object any) {} + public static void jacksonObjectMapper() throws java.io.FileNotFoundException, java.io.UnsupportedEncodingException { String s = taint(); ObjectMapper om = new ObjectMapper(); File file = new File("testFile"); om.writeValue(file, s); + sink(file); //$hasTaintFlow OutputStream out = new FileOutputStream(file); om.writeValue(out, s); + sink(file); //$hasTaintFlow Writer writer = new StringWriter(); om.writeValue(writer, s); + sink(writer); //$hasTaintFlow JsonGenerator generator = new JsonFactory().createGenerator(new StringWriter()); om.writeValue(generator, s); + sink(generator); //$hasTaintFlow String t = om.writeValueAsString(s); - System.out.println(t); + sink(t); //$hasTaintFlow byte[] bs = om.writeValueAsBytes(s); String reconstructed = new String(bs, "utf-8"); - System.out.println(reconstructed); + sink(bs); //$hasTaintFlow + sink(reconstructed); //$hasTaintFlow } public static void jacksonObjectWriter() throws java.io.FileNotFoundException, java.io.UnsupportedEncodingException { @@ -37,16 +53,30 @@ class Test { ObjectWriter ow = new ObjectWriter(); File file = new File("testFile"); ow.writeValue(file, s); + sink(file); //$hasTaintFlow OutputStream out = new FileOutputStream(file); ow.writeValue(out, s); + sink(out); //$hasTaintFlow Writer writer = new StringWriter(); ow.writeValue(writer, s); + sink(writer); //$hasTaintFlow JsonGenerator generator = new JsonFactory().createGenerator(new StringWriter()); ow.writeValue(generator, s); + sink(generator); //$hasTaintFlow String t = ow.writeValueAsString(s); - System.out.println(t); + sink(t); //$hasTaintFlow byte[] bs = ow.writeValueAsBytes(s); String reconstructed = new String(bs, "utf-8"); - System.out.println(reconstructed); + sink(bs); //$hasTaintFlow + sink(reconstructed); //$hasTaintFlow + } + + public static void jacksonObjectReader() throws java.io.IOException { + String s = taint(); + ObjectMapper om = new ObjectMapper(); + ObjectReader reader = om.readerFor(Potato.class); + sink(reader.readValue(s)); //$hasTaintFlow + sink(reader.readValue(s, Potato.class).name); //$hasTaintFlow + sink(reader.readValue(s, Potato.class).getName()); //$hasTaintFlow } } diff --git a/java/ql/test/library-tests/dataflow/taint-jackson/dataFlow.expected b/java/ql/test/library-tests/dataflow/taint-jackson/dataFlow.expected index 122b21d50fe..e69de29bb2d 100644 --- a/java/ql/test/library-tests/dataflow/taint-jackson/dataFlow.expected +++ b/java/ql/test/library-tests/dataflow/taint-jackson/dataFlow.expected @@ -1,48 +0,0 @@ -| ../../../stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java:10:43:10:54 | value | -| ../../../stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java:13:73:13:84 | value | -| ../../../stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java:16:44:16:55 | value | -| ../../../stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java:19:36:19:47 | value | -| ../../../stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java:22:35:22:46 | value | -| ../../../stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java:26:36:26:47 | value | -| ../../../stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectWriter.java:10:43:10:54 | value | -| ../../../stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectWriter.java:13:73:13:84 | value | -| ../../../stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectWriter.java:16:44:16:55 | value | -| ../../../stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectWriter.java:19:36:19:47 | value | -| ../../../stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectWriter.java:22:35:22:46 | value | -| ../../../stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectWriter.java:26:36:26:47 | value | -| Test.java:18:14:18:20 | taint(...) | -| Test.java:21:17:21:20 | file [post update] | -| Test.java:21:23:21:23 | s | -| Test.java:22:43:22:46 | file | -| Test.java:23:17:23:19 | out [post update] | -| Test.java:23:22:23:22 | s | -| Test.java:25:17:25:22 | writer [post update] | -| Test.java:25:25:25:25 | s | -| Test.java:27:17:27:25 | generator [post update] | -| Test.java:27:28:27:28 | s | -| Test.java:28:14:28:37 | writeValueAsString(...) | -| Test.java:28:36:28:36 | s | -| Test.java:29:22:29:22 | t | -| Test.java:30:15:30:37 | writeValueAsBytes(...) | -| Test.java:30:36:30:36 | s | -| Test.java:31:26:31:48 | new String(...) | -| Test.java:31:37:31:38 | bs | -| Test.java:32:22:32:34 | reconstructed | -| Test.java:36:14:36:20 | taint(...) | -| Test.java:39:17:39:20 | file [post update] | -| Test.java:39:23:39:23 | s | -| Test.java:40:43:40:46 | file | -| Test.java:41:17:41:19 | out [post update] | -| Test.java:41:22:41:22 | s | -| Test.java:43:17:43:22 | writer [post update] | -| Test.java:43:25:43:25 | s | -| Test.java:45:17:45:25 | generator [post update] | -| Test.java:45:28:45:28 | s | -| Test.java:46:14:46:37 | writeValueAsString(...) | -| Test.java:46:36:46:36 | s | -| Test.java:47:22:47:22 | t | -| Test.java:48:15:48:37 | writeValueAsBytes(...) | -| Test.java:48:36:48:36 | s | -| Test.java:49:26:49:48 | new String(...) | -| Test.java:49:37:49:38 | bs | -| Test.java:50:22:50:34 | reconstructed | diff --git a/java/ql/test/library-tests/dataflow/taint-jackson/dataFlow.ql b/java/ql/test/library-tests/dataflow/taint-jackson/dataFlow.ql index 333cf485f07..3ee51339b7b 100644 --- a/java/ql/test/library-tests/dataflow/taint-jackson/dataFlow.ql +++ b/java/ql/test/library-tests/dataflow/taint-jackson/dataFlow.ql @@ -1,17 +1,34 @@ +import java import semmle.code.java.dataflow.DataFlow import semmle.code.java.dataflow.TaintTracking import semmle.code.java.dataflow.FlowSources +import TestUtilities.InlineExpectationsTest class Conf extends TaintTracking::Configuration { Conf() { this = "qltest:dataflow:jackson" } - override predicate isSource(DataFlow::Node source) { - source.asExpr().(MethodAccess).getMethod().hasName("taint") + override predicate isSource(DataFlow::Node n) { + n.asExpr().(MethodAccess).getMethod().hasName("taint") + or + n instanceof RemoteFlowSource } - override predicate isSink(DataFlow::Node sink) { any() } + override predicate isSink(DataFlow::Node n) { + exists(MethodAccess ma | ma.getMethod().hasName("sink") | n.asExpr() = ma.getAnArgument()) + } } -from DataFlow::Node source, DataFlow::Node sink, Conf config -where config.hasFlow(source, sink) -select sink +class HasFlowTest extends InlineExpectationsTest { + HasFlowTest() { this = "HasFlowTest" } + + override string getARelevantTag() { result = "hasTaintFlow" } + + 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) | + sink.getLocation() = location and + element = sink.toString() and + value = "" + ) + } +} \ No newline at end of file diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java index 455e0c0d309..af6218e4146 100644 --- a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java +++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java @@ -26,4 +26,8 @@ public class ObjectMapper { public String writeValueAsString(Object value) { return null; } + + public ObjectReader readerFor(Class type) { + return null; + } } diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectReader.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectReader.java new file mode 100644 index 00000000000..1916730da36 --- /dev/null +++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectReader.java @@ -0,0 +1,45 @@ +package com.fasterxml.jackson.databind; + +import java.io.*; + +public class ObjectReader { + public ObjectReader forType(Class valueType) { + return null; + } + + public T readValue(String src) { + return null; + } + + public T readValue(String src, Class valueType) throws IOException { + return null; + } + + public T readValue(byte[] content) throws IOException { + return null; + } + + public T readValue(byte[] content, Class valueType) throws IOException { + return null; + } + + public T readValue(File src) throws IOException { + return null; + } + + public T readValue(InputStream src) throws IOException { + return null; + } + + public T readValue(InputStream src, Class valueType) throws IOException { + return null; + } + + public T readValue(Reader src) throws IOException { + return null; + } + + public T readValue(Reader src, Class valueType) throws IOException { + return null; + } +} \ No newline at end of file From d0638db6e72836399061b506d029ad8a7d0e2f9d Mon Sep 17 00:00:00 2001 From: Jonathan Leitschuh Date: Mon, 3 May 2021 12:02:40 -0400 Subject: [PATCH 16/74] [Java] Add data flow through Iterator deserializers for Jackson --- .../jackson/JacksonSerializability.qll | 31 ++++++++++++---- .../dataflow/taint-jackson/Test.java | 15 ++++++++ .../jackson/databind/MappingIterator.java | 28 ++++++++++++++ .../jackson/databind/ObjectReader.java | 37 +++++++++++++++++++ 4 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/MappingIterator.java diff --git a/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll b/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll index c028af71d8d..1cfe34d7167 100644 --- a/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll +++ b/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll @@ -28,7 +28,7 @@ abstract class JacksonSerializableType extends Type { } * A method used for serializing objects using Jackson. The final parameter is the object to be * serialized. */ -library class JacksonWriteValueMethod extends Method, TaintPreservingCallable { +private class JacksonWriteValueMethod extends Method, TaintPreservingCallable { JacksonWriteValueMethod() { ( getDeclaringType().hasQualifiedName("com.fasterxml.jackson.databind", "ObjectWriter") or @@ -50,17 +50,17 @@ library class JacksonWriteValueMethod extends Method, TaintPreservingCallable { } } -library class JacksonReadValueMethod extends Method, TaintPreservingCallable { +private class JacksonReadValueMethod extends Method, TaintPreservingCallable { JacksonReadValueMethod() { getDeclaringType().hasQualifiedName("com.fasterxml.jackson.databind", "ObjectReader") and - hasName("readValue") + hasName(["readValue", "readValues"]) } override predicate returnsTaintFrom(int arg) { arg = 0 } } /** A type whose values are explicitly serialized in a call to a Jackson method. */ -library class ExplicitlyWrittenJacksonSerializableType extends JacksonSerializableType { +private class ExplicitlyWrittenJacksonSerializableType extends JacksonSerializableType { ExplicitlyWrittenJacksonSerializableType() { exists(MethodAccess ma | // A call to a Jackson write method... @@ -71,8 +71,20 @@ library class ExplicitlyWrittenJacksonSerializableType extends JacksonSerializab } } +/** A type whose values are explicitly deserialized in a call to a Jackson method. */ +private class ExplicitlyReadJacksonSerializableType extends JacksonDeserializableType { + ExplicitlyReadJacksonSerializableType() { + exists(MethodAccess ma | + // A call to a Jackson write method... + ma.getMethod() instanceof JacksonReadValueMethod and + // ...where `this` is used in the final argument, indicating that this type will be deserialized. + usesType(ma.getArgument(ma.getNumArgument() - 1).getType(), this) + ) + } +} + /** A type used in a `JacksonSerializableField` declaration. */ -library class FieldReferencedJacksonSerializableType extends JacksonSerializableType { +private class FieldReferencedJacksonSerializableType extends JacksonSerializableType { FieldReferencedJacksonSerializableType() { exists(JacksonSerializableField f | usesType(f.getType(), this)) } @@ -105,7 +117,7 @@ private class TypeLiteralToJacksonDatabindFlowConfiguration extends DataFlow5::C } /** A type whose values are explicitly deserialized in a call to a Jackson method. */ -library class ExplicitlyReadJacksonDeserializableType extends JacksonDeserializableType { +private class ExplicitlyReadJacksonDeserializableType extends JacksonDeserializableType { ExplicitlyReadJacksonDeserializableType() { exists(TypeLiteralToJacksonDatabindFlowConfiguration conf | usesType(conf.getSourceWithFlowToJacksonDatabind().getTypeName().getType(), this) @@ -114,7 +126,7 @@ library class ExplicitlyReadJacksonDeserializableType extends JacksonDeserializa } /** A type used in a `JacksonDeserializableField` declaration. */ -library class FieldReferencedJacksonDeSerializableType extends JacksonDeserializableType { +private class FieldReferencedJacksonDeSerializableType extends JacksonDeserializableType { FieldReferencedJacksonDeSerializableType() { exists(JacksonDeserializableField f | usesType(f.getType(), this)) } @@ -144,10 +156,15 @@ class JacksonDeserializableField extends DeserializableField { } } +/** A call to a field that may be deserialized using the Jackson JSON framework. */ class JacksonDeserializableFieldAccess extends FieldAccess { JacksonDeserializableFieldAccess() { getField() instanceof JacksonDeserializableField } } +/** + * When an object is deserialized by the Jackson JSON framework using a tainted input source, + * the fields that the framework deserialized are themselves tainted input data. + */ class JacksonDeseializedTaintStep extends AdditionalTaintStep { override predicate step(DataFlow::Node node1, DataFlow::Node node2) { node2.asExpr().(JacksonDeserializableFieldAccess).getQualifier() = node1.asExpr() diff --git a/java/ql/test/library-tests/dataflow/taint-jackson/Test.java b/java/ql/test/library-tests/dataflow/taint-jackson/Test.java index 16b223ed2e8..26f9182339a 100644 --- a/java/ql/test/library-tests/dataflow/taint-jackson/Test.java +++ b/java/ql/test/library-tests/dataflow/taint-jackson/Test.java @@ -3,6 +3,7 @@ import java.io.FileOutputStream; import java.io.OutputStream; import java.io.StringWriter; import java.io.Writer; +import java.util.Iterator; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; @@ -79,4 +80,18 @@ class Test { sink(reader.readValue(s, Potato.class).name); //$hasTaintFlow sink(reader.readValue(s, Potato.class).getName()); //$hasTaintFlow } + + public static void jacksonObjectReaderIterable() throws java.io.IOException { + String s = taint(); + ObjectMapper om = new ObjectMapper(); + ObjectReader reader = om.readerFor(Potato.class); + sink(reader.readValues(s)); //$hasTaintFlow + Iterator pIterator = reader.readValues(s, Potato.class); + while(pIterator.hasNext()) { + Potato p = pIterator.next(); + sink(p); //$hasTaintFlow + sink(p.name); //$hasTaintFlow + sink(p.getName()); //$hasTaintFlow + } + } } diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/MappingIterator.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/MappingIterator.java new file mode 100644 index 00000000000..72e08df2719 --- /dev/null +++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/MappingIterator.java @@ -0,0 +1,28 @@ +package com.fasterxml.jackson.databind; + +import java.io.Closeable; +import java.io.IOException; +import java.util.*; + +public class MappingIterator implements Iterator, Closeable { + + @Override + public boolean hasNext() { + return false; + } + + @Override + public T next() { + return null; + } + + @Override + public void remove() { + + } + + @Override + public void close() throws IOException { + + } +} \ No newline at end of file diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectReader.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectReader.java index 1916730da36..875c07dace3 100644 --- a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectReader.java +++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectReader.java @@ -42,4 +42,41 @@ public class ObjectReader { public T readValue(Reader src, Class valueType) throws IOException { return null; } + + public MappingIterator readValues(String src) { + return null; + } + + public MappingIterator readValues(String src, Class valueType) throws IOException { + return null; + } + + public MappingIterator readValues(byte[] content) throws IOException { + return null; + } + + public MappingIterator readValues(byte[] content, Class valueType) throws IOException { + return null; + } + + public MappingIterator readValues(File src) throws IOException { + return null; + } + + public MappingIterator readValues(InputStream src) throws IOException { + return null; + } + + public MappingIterator readValues(InputStream src, Class valueType) throws IOException { + return null; + } + + public MappingIterator readValues(Reader src) throws IOException { + return null; + } + + public MappingIterator readValues(Reader src, Class valueType) throws IOException { + return null; + } + } \ No newline at end of file From d0b0b767a28d98ce40d3c2fddee477608757cdcd Mon Sep 17 00:00:00 2001 From: Jonathan Leitschuh Date: Mon, 3 May 2021 12:23:33 -0400 Subject: [PATCH 17/74] Apply suggestions from code review Co-authored-by: Marcono1234 --- .../code/java/frameworks/jackson/JacksonSerializability.qll | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll b/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll index 1cfe34d7167..2ddb12e828d 100644 --- a/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll +++ b/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll @@ -75,7 +75,7 @@ private class ExplicitlyWrittenJacksonSerializableType extends JacksonSerializab private class ExplicitlyReadJacksonSerializableType extends JacksonDeserializableType { ExplicitlyReadJacksonSerializableType() { exists(MethodAccess ma | - // A call to a Jackson write method... + // A call to a Jackson read method... ma.getMethod() instanceof JacksonReadValueMethod and // ...where `this` is used in the final argument, indicating that this type will be deserialized. usesType(ma.getArgument(ma.getNumArgument() - 1).getType(), this) @@ -126,8 +126,8 @@ private class ExplicitlyReadJacksonDeserializableType extends JacksonDeserializa } /** A type used in a `JacksonDeserializableField` declaration. */ -private class FieldReferencedJacksonDeSerializableType extends JacksonDeserializableType { - FieldReferencedJacksonDeSerializableType() { +private class FieldReferencedJacksonDeserializableType extends JacksonDeserializableType { + FieldReferencedJacksonDeserializableType() { exists(JacksonDeserializableField f | usesType(f.getType(), this)) } } From b871f48c509b960c75847903300f7cbf7664fb6a Mon Sep 17 00:00:00 2001 From: Jonathan Leitschuh Date: Mon, 3 May 2021 12:40:48 -0400 Subject: [PATCH 18/74] [Java] Add release note to Jackson change --- .../change-notes/2021-05-03-jackson-dataflow-deserialization.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 java/change-notes/2021-05-03-jackson-dataflow-deserialization.md diff --git a/java/change-notes/2021-05-03-jackson-dataflow-deserialization.md b/java/change-notes/2021-05-03-jackson-dataflow-deserialization.md new file mode 100644 index 00000000000..475f8dbee4f --- /dev/null +++ b/java/change-notes/2021-05-03-jackson-dataflow-deserialization.md @@ -0,0 +1,2 @@ +lgtm,codescanning +* Increase coverage of dataflow through Jackson JSON deserialized objects. From 83d527ed190331f4ecdb165539423922ee1f704a Mon Sep 17 00:00:00 2001 From: Jonathan Leitschuh Date: Mon, 3 May 2021 13:39:54 -0400 Subject: [PATCH 19/74] Apply suggestions from code review Co-authored-by: Marcono1234 --- .../code/java/frameworks/jackson/JacksonSerializability.qll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll b/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll index 2ddb12e828d..0b75aff2b24 100644 --- a/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll +++ b/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll @@ -165,7 +165,7 @@ class JacksonDeserializableFieldAccess extends FieldAccess { * When an object is deserialized by the Jackson JSON framework using a tainted input source, * the fields that the framework deserialized are themselves tainted input data. */ -class JacksonDeseializedTaintStep extends AdditionalTaintStep { +class JacksonDeserializedTaintStep extends AdditionalTaintStep { override predicate step(DataFlow::Node node1, DataFlow::Node node2) { node2.asExpr().(JacksonDeserializableFieldAccess).getQualifier() = node1.asExpr() } From e97bad3b3395d3a6143b515bba1b12c344a881d2 Mon Sep 17 00:00:00 2001 From: Jonathan Leitschuh Date: Tue, 4 May 2021 10:58:20 -0400 Subject: [PATCH 20/74] Support field access data flow for JacksonDeserializedTaintStep --- .../code/java/frameworks/jackson/JacksonSerializability.qll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll b/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll index 0b75aff2b24..8cc70c5f36a 100644 --- a/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll +++ b/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll @@ -167,7 +167,7 @@ class JacksonDeserializableFieldAccess extends FieldAccess { */ class JacksonDeserializedTaintStep extends AdditionalTaintStep { override predicate step(DataFlow::Node node1, DataFlow::Node node2) { - node2.asExpr().(JacksonDeserializableFieldAccess).getQualifier() = node1.asExpr() + DataFlow::getFieldQualifier(node2.asExpr().(JacksonDeserializableFieldAccess)) = node1 } } From bacc3ef5b3699f03bff628e8ca1196c0f7095342 Mon Sep 17 00:00:00 2001 From: Jonathan Leitschuh Date: Mon, 10 May 2021 17:20:14 -0400 Subject: [PATCH 21/74] [Java] Jackson add support for 2 step deserialization taint flow --- .../semmle/code/java/dataflow/ExternalFlow.qll | 1 + .../frameworks/jackson/JacksonSerializability.qll | 11 +++++++++++ .../dataflow/taint-jackson/Test.java | 15 +++++++++++++++ .../com/fasterxml/jackson/databind/JsonNode.java | 4 +++- .../fasterxml/jackson/databind/ObjectMapper.java | 8 ++++++++ 5 files changed, 38 insertions(+), 1 deletion(-) diff --git a/java/ql/src/semmle/code/java/dataflow/ExternalFlow.qll b/java/ql/src/semmle/code/java/dataflow/ExternalFlow.qll index df529b49efb..1ec81d1e3ac 100644 --- a/java/ql/src/semmle/code/java/dataflow/ExternalFlow.qll +++ b/java/ql/src/semmle/code/java/dataflow/ExternalFlow.qll @@ -77,6 +77,7 @@ private module Frameworks { private import semmle.code.java.frameworks.ApacheHttp private import semmle.code.java.frameworks.apache.Lang private import semmle.code.java.frameworks.guava.Guava + private import semmle.code.java.frameworks.jackson.JacksonSerializability private import semmle.code.java.security.ResponseSplitting private import semmle.code.java.security.XSS private import semmle.code.java.security.LdapInjection diff --git a/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll b/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll index 8cc70c5f36a..ad35a802d43 100644 --- a/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll +++ b/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll @@ -9,6 +9,7 @@ import semmle.code.java.Reflection import semmle.code.java.dataflow.DataFlow import semmle.code.java.dataflow.DataFlow5 import semmle.code.java.dataflow.FlowSteps +private import semmle.code.java.dataflow.ExternalFlow /** * A `@com.fasterxml.jackson.annotation.JsonIgnore` annoation. @@ -275,3 +276,13 @@ class JacksonMixedInCallable extends Callable { ) } } + +private class JacksonModel extends SummaryModelCsv { + override predicate row(string row) { + row = + [ + "com.fasterxml.jackson.databind;ObjectMapper;true;valueToTree;;;Argument[0];ReturnValue;taint", + "com.fasterxml.jackson.databind;ObjectMapper;true;convertValue;;;Argument[0];ReturnValue;taint" + ] + } +} diff --git a/java/ql/test/library-tests/dataflow/taint-jackson/Test.java b/java/ql/test/library-tests/dataflow/taint-jackson/Test.java index 26f9182339a..3be85336e26 100644 --- a/java/ql/test/library-tests/dataflow/taint-jackson/Test.java +++ b/java/ql/test/library-tests/dataflow/taint-jackson/Test.java @@ -4,9 +4,12 @@ import java.io.OutputStream; import java.io.StringWriter; import java.io.Writer; import java.util.Iterator; +import java.util.HashMap; +import java.util.Map; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.ObjectReader; @@ -94,4 +97,16 @@ class Test { sink(p.getName()); //$hasTaintFlow } } + + public static void jacksonTwoStepDeserialization() throws java.io.IOException { + String s = taint(); + Map taintedParams = new HashMap<>(); + taintedParams.put("name", s); + ObjectMapper om = new ObjectMapper(); + JsonNode jn = om.valueToTree(taintedParams); + sink(jn); //$hasTaintFlow + Potato p = om.convertValue(jn, Potato.class); + sink(p); //$hasTaintFlow + sink(p.getName()); //$hasTaintFlow + } } diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JsonNode.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JsonNode.java index a26eb2592c6..ad3da15a26c 100644 --- a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JsonNode.java +++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JsonNode.java @@ -1,6 +1,8 @@ package com.fasterxml.jackson.databind; -public class JsonNode { +import java.util.*; + +public abstract class JsonNode implements Iterable { public JsonNode() { } } \ No newline at end of file diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java index af6218e4146..71dc99a351d 100644 --- a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java +++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectMapper.java @@ -30,4 +30,12 @@ public class ObjectMapper { public ObjectReader readerFor(Class type) { return null; } + + public T valueToTree(Object fromValue) throws IllegalArgumentException { + return null; + } + + public T convertValue(Object fromValue, Class toValueType) throws IllegalArgumentException { + return null; + } } From 5a68ac88efdf272c2f3e6dd9a9c5393b582bbcec Mon Sep 17 00:00:00 2001 From: Jonathan Leitschuh Date: Tue, 11 May 2021 10:48:22 -0400 Subject: [PATCH 22/74] Cleanup Jackson logic after code review --- .../jackson/JacksonSerializability.qll | 19 +++++++------------ .../dataflow/taint-jackson/dataFlow.ql | 2 +- .../fasterxml/jackson/databind/JsonNode.java | 2 +- .../jackson/databind/MappingIterator.java | 2 +- .../jackson/databind/ObjectReader.java | 2 +- 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll b/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll index ad35a802d43..09fd419642e 100644 --- a/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll +++ b/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll @@ -72,18 +72,6 @@ private class ExplicitlyWrittenJacksonSerializableType extends JacksonSerializab } } -/** A type whose values are explicitly deserialized in a call to a Jackson method. */ -private class ExplicitlyReadJacksonSerializableType extends JacksonDeserializableType { - ExplicitlyReadJacksonSerializableType() { - exists(MethodAccess ma | - // A call to a Jackson read method... - ma.getMethod() instanceof JacksonReadValueMethod and - // ...where `this` is used in the final argument, indicating that this type will be deserialized. - usesType(ma.getArgument(ma.getNumArgument() - 1).getType(), this) - ) - } -} - /** A type used in a `JacksonSerializableField` declaration. */ private class FieldReferencedJacksonSerializableType extends JacksonSerializableType { FieldReferencedJacksonSerializableType() { @@ -123,6 +111,13 @@ private class ExplicitlyReadJacksonDeserializableType extends JacksonDeserializa exists(TypeLiteralToJacksonDatabindFlowConfiguration conf | usesType(conf.getSourceWithFlowToJacksonDatabind().getTypeName().getType(), this) ) + or + exists(MethodAccess ma | + // A call to a Jackson read method... + ma.getMethod() instanceof JacksonReadValueMethod and + // ...where `this` is used in the final argument, indicating that this type will be deserialized. + usesType(ma.getArgument(ma.getNumArgument() - 1).getType(), this) + ) } } diff --git a/java/ql/test/library-tests/dataflow/taint-jackson/dataFlow.ql b/java/ql/test/library-tests/dataflow/taint-jackson/dataFlow.ql index 3ee51339b7b..0836906530b 100644 --- a/java/ql/test/library-tests/dataflow/taint-jackson/dataFlow.ql +++ b/java/ql/test/library-tests/dataflow/taint-jackson/dataFlow.ql @@ -31,4 +31,4 @@ class HasFlowTest extends InlineExpectationsTest { value = "" ) } -} \ No newline at end of file +} diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JsonNode.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JsonNode.java index ad3da15a26c..b04572cd4da 100644 --- a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JsonNode.java +++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/JsonNode.java @@ -5,4 +5,4 @@ import java.util.*; public abstract class JsonNode implements Iterable { public JsonNode() { } -} \ No newline at end of file +} diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/MappingIterator.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/MappingIterator.java index 72e08df2719..ac427ef01c9 100644 --- a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/MappingIterator.java +++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/MappingIterator.java @@ -25,4 +25,4 @@ public class MappingIterator implements Iterator, Closeable { public void close() throws IOException { } -} \ No newline at end of file +} diff --git a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectReader.java b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectReader.java index 875c07dace3..f067a3e95a4 100644 --- a/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectReader.java +++ b/java/ql/test/stubs/jackson-databind-2.10/com/fasterxml/jackson/databind/ObjectReader.java @@ -79,4 +79,4 @@ public class ObjectReader { return null; } -} \ No newline at end of file +} From e7cd6c997208b742129bd72c0715a27790130211 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Tue, 11 May 2021 16:56:12 +0000 Subject: [PATCH 23/74] Optimize the query --- .../Security/CWE/CWE-094/JythonInjection.java | 6 ++-- .../Security/CWE/CWE-094/JythonInjection.ql | 35 +++++++++---------- .../security/CWE-094/JythonInjection.java | 4 +-- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.java b/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.java index fca518443d1..5c1796e1f60 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.java +++ b/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.java @@ -1,3 +1,5 @@ +import org.python.util.PythonInterpreter; + public class JythonInjection extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/plain"); @@ -10,7 +12,7 @@ public class JythonInjection extends HttpServlet { interpreter.setOut(out); interpreter.setErr(out); - // BAD: allow arbitrary Jython expression to execute + // BAD: allow execution of arbitrary Python code interpreter.exec(code); out.flush(); @@ -32,7 +34,7 @@ public class JythonInjection extends HttpServlet { try { interpreter = new PythonInterpreter(); - // BAD: allow arbitrary Jython expression to evaluate + // BAD: allow execution of arbitrary Python code PyObject py = interpreter.eval(code); response.getWriter().print(py.toString()); diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.ql index 088c33e00fd..9991c0901dc 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.ql @@ -11,6 +11,7 @@ import java import semmle.code.java.dataflow.FlowSources +import semmle.code.java.frameworks.spring.SpringController import DataFlow::PathGraph /** The class `org.python.util.PythonInterpreter`. */ @@ -22,12 +23,7 @@ class PythonInterpreter extends RefType { class InterpretExprMethod extends Method { InterpretExprMethod() { this.getDeclaringType().getAnAncestor*() instanceof PythonInterpreter and - ( - getName().matches("exec%") or - hasName("eval") or - hasName("compile") or - getName().matches("run%") - ) + getName().matches(["exec%", "run%", "eval", "compile"]) } } @@ -48,14 +44,14 @@ predicate runCode(MethodAccess ma, Expr sink) { class LoadClassMethod extends Method { LoadClassMethod() { this.getDeclaringType().getAnAncestor*() instanceof BytecodeLoader and - ( - hasName("makeClass") or - hasName("makeCode") - ) + hasName(["makeClass", "makeCode"]) } } -/** Holds if a Java class file is loaded. */ +/** + * Holds if `ma` is a call to a class-loading method, and `sink` is the byte array + * representing the class to be loaded. + */ predicate loadClass(MethodAccess ma, Expr sink) { exists(Method m, int i | m = ma.getMethod() | m instanceof LoadClassMethod and @@ -69,7 +65,7 @@ class Py extends RefType { Py() { this.hasQualifiedName("org.python.core", "Py") } } -/** A method that compiles code with `Py`. */ +/** A method declared on class `Py` or one of its descendants that compiles Python code. */ class PyCompileMethod extends Method { PyCompileMethod() { this.getDeclaringType().getAnAncestor*() instanceof Py and @@ -85,7 +81,7 @@ predicate compile(MethodAccess ma, Expr sink) { ) } -/** Sink of an expression loaded by Jython. */ +/** An expression loaded by Jython. */ class CodeInjectionSink extends DataFlow::ExprNode { CodeInjectionSink() { runCode(_, this.getExpr()) or @@ -103,17 +99,18 @@ class CodeInjectionSink extends DataFlow::ExprNode { class CodeInjectionConfiguration extends TaintTracking::Configuration { CodeInjectionConfiguration() { this = "CodeInjectionConfiguration" } - override predicate isSource(DataFlow::Node source) { - source instanceof RemoteFlowSource - or - source instanceof LocalUserInput - } + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } override predicate isSink(DataFlow::Node sink) { sink instanceof CodeInjectionSink } override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) { // @RequestBody MyQueryObj query; interpreter.exec(query.getInterpreterCode()); - exists(MethodAccess ma | ma.getQualifier() = node1.asExpr() and ma = node2.asExpr()) + exists(MethodAccess ma | + ma.getMethod().getDeclaringType().getASubtype*() instanceof SpringUntrustedDataType and + not ma.getMethod().getDeclaringType() instanceof TypeObject and + ma.getQualifier() = node1.asExpr() and + ma = node2.asExpr() + ) } } diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/JythonInjection.java b/java/ql/test/experimental/query-tests/security/CWE-094/JythonInjection.java index 682e8af5113..f9b29fec6cc 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-094/JythonInjection.java +++ b/java/ql/test/experimental/query-tests/security/CWE-094/JythonInjection.java @@ -22,7 +22,7 @@ public class JythonInjection extends HttpServlet { super(); } - // BAD: allow arbitrary Jython expression to execute + // BAD: allow execution of arbitrary Python code protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/plain"); String code = request.getParameter("code"); @@ -47,7 +47,7 @@ public class JythonInjection extends HttpServlet { } } - // BAD: allow arbitrary Jython expression to evaluate + // BAD: allow execution of arbitrary Python code protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/plain"); String code = request.getParameter("code"); From 961467e06e4142ca71d144ee5c1bb7ca993b706b Mon Sep 17 00:00:00 2001 From: Tom Hvitved Date: Wed, 12 May 2021 10:15:04 +0200 Subject: [PATCH 24/74] C#: Always pass `/p:UseSharedCompilation=false` to `dotnet build` in auto builder --- .../BuildScripts.cs | 44 ++++----------- .../Semmle.Autobuild.CSharp/DotNetRule.cs | 56 ++++--------------- 2 files changed, 21 insertions(+), 79 deletions(-) diff --git a/csharp/autobuilder/Semmle.Autobuild.CSharp.Tests/BuildScripts.cs b/csharp/autobuilder/Semmle.Autobuild.CSharp.Tests/BuildScripts.cs index 99ad4c8f963..197edc2c162 100644 --- a/csharp/autobuilder/Semmle.Autobuild.CSharp.Tests/BuildScripts.cs +++ b/csharp/autobuilder/Semmle.Autobuild.CSharp.Tests/BuildScripts.cs @@ -415,7 +415,7 @@ namespace Semmle.Autobuild.CSharp.Tests actions.RunProcess["cmd.exe /C dotnet --info"] = 0; actions.RunProcess[@"cmd.exe /C dotnet clean C:\Project\test.csproj"] = 0; actions.RunProcess[@"cmd.exe /C dotnet restore C:\Project\test.csproj"] = 0; - actions.RunProcess[@"cmd.exe /C C:\odasa\tools\odasa index --auto dotnet build --no-incremental C:\Project\test.csproj"] = 0; + actions.RunProcess[@"cmd.exe /C C:\odasa\tools\odasa index --auto dotnet build --no-incremental /p:UseSharedCompilation=false C:\Project\test.csproj"] = 0; actions.FileExists["csharp.log"] = true; actions.FileExists[@"C:\Project\test.csproj"] = true; actions.GetEnvironmentVariable["CODEQL_EXTRACTOR_CSHARP_TRAP_DIR"] = ""; @@ -439,9 +439,6 @@ namespace Semmle.Autobuild.CSharp.Tests [Fact] public void TestLinuxCSharpAutoBuilder() { - actions.RunProcess["dotnet --list-runtimes"] = 0; - actions.RunProcessOut["dotnet --list-runtimes"] = @"Microsoft.AspNetCore.App 2.2.5 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] -Microsoft.NETCore.App 2.2.5 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]"; actions.RunProcess["dotnet --info"] = 0; actions.RunProcess[@"dotnet clean C:\Project/test.csproj"] = 0; actions.RunProcess[@"dotnet restore C:\Project/test.csproj"] = 0; @@ -463,7 +460,7 @@ Microsoft.NETCore.App 2.2.5 [/usr/local/share/dotnet/shared/Microsoft.NETCore.Ap actions.LoadXml[@"C:\Project/test.csproj"] = xml; var autobuilder = CreateAutoBuilder(false); - TestAutobuilderScript(autobuilder, 0, 5); + TestAutobuilderScript(autobuilder, 0, 4); } [Fact] @@ -603,8 +600,6 @@ Microsoft.NETCore.App 2.2.5 [/usr/local/share/dotnet/shared/Microsoft.NETCore.Ap [Fact] public void TestLinuxBuildCommand() { - actions.RunProcess["dotnet --list-runtimes"] = 1; - actions.RunProcessOut["dotnet --list-runtimes"] = ""; actions.RunProcess[@"C:\odasa/tools/odasa index --auto ""./build.sh --skip-tests"""] = 0; actions.FileExists["csharp.log"] = true; actions.GetEnvironmentVariable["CODEQL_EXTRACTOR_CSHARP_TRAP_DIR"] = ""; @@ -615,7 +610,7 @@ Microsoft.NETCore.App 2.2.5 [/usr/local/share/dotnet/shared/Microsoft.NETCore.Ap SkipVsWhere(); var autobuilder = CreateAutoBuilder(false, buildCommand: "./build.sh --skip-tests"); - TestAutobuilderScript(autobuilder, 0, 2); + TestAutobuilderScript(autobuilder, 0, 1); } [Fact] @@ -626,14 +621,12 @@ Microsoft.NETCore.App 2.2.5 [/usr/local/share/dotnet/shared/Microsoft.NETCore.Ap actions.GetEnvironmentVariable["CODEQL_EXTRACTOR_CSHARP_TRAP_DIR"] = ""; actions.GetEnvironmentVariable["CODEQL_EXTRACTOR_CSHARP_SOURCE_ARCHIVE_DIR"] = ""; actions.RunProcess[@"/bin/chmod u+x C:\Project/build/build.sh"] = 0; - actions.RunProcess["dotnet --list-runtimes"] = 1; - actions.RunProcessOut["dotnet --list-runtimes"] = ""; actions.RunProcess[@"C:\odasa/tools/odasa index --auto C:\Project/build/build.sh"] = 0; actions.RunProcessWorkingDirectory[@"C:\odasa/tools/odasa index --auto C:\Project/build/build.sh"] = @"C:\Project/build"; actions.FileExists["csharp.log"] = true; var autobuilder = CreateAutoBuilder(false); - TestAutobuilderScript(autobuilder, 0, 3); + TestAutobuilderScript(autobuilder, 0, 2); } [Fact] @@ -645,14 +638,12 @@ Microsoft.NETCore.App 2.2.5 [/usr/local/share/dotnet/shared/Microsoft.NETCore.Ap actions.GetEnvironmentVariable["CODEQL_EXTRACTOR_CSHARP_SOURCE_ARCHIVE_DIR"] = ""; actions.RunProcess[@"/bin/chmod u+x C:\Project/build.sh"] = 0; - actions.RunProcess["dotnet --list-runtimes"] = 1; - actions.RunProcessOut["dotnet --list-runtimes"] = ""; actions.RunProcess[@"C:\odasa/tools/odasa index --auto C:\Project/build.sh"] = 0; actions.RunProcessWorkingDirectory[@"C:\odasa/tools/odasa index --auto C:\Project/build.sh"] = @"C:\Project"; actions.FileExists["csharp.log"] = false; var autobuilder = CreateAutoBuilder(false); - TestAutobuilderScript(autobuilder, 1, 3); + TestAutobuilderScript(autobuilder, 1, 2); } [Fact] @@ -664,14 +655,12 @@ Microsoft.NETCore.App 2.2.5 [/usr/local/share/dotnet/shared/Microsoft.NETCore.Ap actions.GetEnvironmentVariable["CODEQL_EXTRACTOR_CSHARP_SOURCE_ARCHIVE_DIR"] = ""; actions.RunProcess[@"/bin/chmod u+x C:\Project/build.sh"] = 0; - actions.RunProcess["dotnet --list-runtimes"] = 1; - actions.RunProcessOut["dotnet --list-runtimes"] = ""; actions.RunProcess[@"C:\odasa/tools/odasa index --auto C:\Project/build.sh"] = 5; actions.RunProcessWorkingDirectory[@"C:\odasa/tools/odasa index --auto C:\Project/build.sh"] = @"C:\Project"; actions.FileExists["csharp.log"] = true; var autobuilder = CreateAutoBuilder(false); - TestAutobuilderScript(autobuilder, 1, 3); + TestAutobuilderScript(autobuilder, 1, 2); } [Fact] @@ -872,9 +861,6 @@ Microsoft.NETCore.App 2.2.5 [/usr/local/share/dotnet/shared/Microsoft.NETCore.Ap [Fact] public void TestSkipNugetDotnet() { - actions.RunProcess["dotnet --list-runtimes"] = 0; - actions.RunProcessOut["dotnet --list-runtimes"] = @"Microsoft.AspNetCore.App 2.1.3 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] -Microsoft.NETCore.App 2.1.3 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]"; actions.RunProcess["dotnet --info"] = 0; actions.RunProcess[@"dotnet clean C:\Project/test.csproj"] = 0; actions.RunProcess[@"dotnet restore C:\Project/test.csproj"] = 0; @@ -896,7 +882,7 @@ Microsoft.NETCore.App 2.1.3 [/usr/local/share/dotnet/shared/Microsoft.NETCore.Ap actions.LoadXml[@"C:\Project/test.csproj"] = xml; var autobuilder = CreateAutoBuilder(false, dotnetArguments: "--no-restore"); // nugetRestore=false does not work for now. - TestAutobuilderScript(autobuilder, 0, 5); + TestAutobuilderScript(autobuilder, 0, 4); } [Fact] @@ -907,13 +893,10 @@ Microsoft.NETCore.App 2.1.3 [/usr/local/share/dotnet/shared/Microsoft.NETCore.Ap actions.RunProcess[@"chmod u+x dotnet-install.sh"] = 0; actions.RunProcess[@"./dotnet-install.sh --channel release --version 2.1.3 --install-dir C:\Project/.dotnet"] = 0; actions.RunProcess[@"rm dotnet-install.sh"] = 0; - actions.RunProcess[@"C:\Project/.dotnet/dotnet --list-runtimes"] = 0; - actions.RunProcessOut[@"C:\Project/.dotnet/dotnet --list-runtimes"] = @"Microsoft.AspNetCore.App 3.0.0 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] -Microsoft.NETCore.App 3.0.0 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]"; actions.RunProcess[@"C:\Project/.dotnet/dotnet --info"] = 0; actions.RunProcess[@"C:\Project/.dotnet/dotnet clean C:\Project/test.csproj"] = 0; actions.RunProcess[@"C:\Project/.dotnet/dotnet restore C:\Project/test.csproj"] = 0; - actions.RunProcess[@"C:\odasa/tools/odasa index --auto C:\Project/.dotnet/dotnet build --no-incremental C:\Project/test.csproj"] = 0; + actions.RunProcess[@"C:\odasa/tools/odasa index --auto C:\Project/.dotnet/dotnet build --no-incremental /p:UseSharedCompilation=false C:\Project/test.csproj"] = 0; actions.FileExists["csharp.log"] = true; actions.FileExists["test.csproj"] = true; actions.GetEnvironmentVariable["CODEQL_EXTRACTOR_CSHARP_TRAP_DIR"] = ""; @@ -933,7 +916,7 @@ Microsoft.NETCore.App 3.0.0 [/usr/local/share/dotnet/shared/Microsoft.NETCore.Ap actions.DownloadFiles.Add(("https://dot.net/v1/dotnet-install.sh", "dotnet-install.sh")); var autobuilder = CreateAutoBuilder(false, dotnetVersion: "2.1.3"); - TestAutobuilderScript(autobuilder, 0, 9); + TestAutobuilderScript(autobuilder, 0, 8); } [Fact] @@ -945,11 +928,6 @@ Microsoft.NETCore.App 3.0.0 [/usr/local/share/dotnet/shared/Microsoft.NETCore.Ap actions.RunProcess[@"chmod u+x dotnet-install.sh"] = 0; actions.RunProcess[@"./dotnet-install.sh --channel release --version 2.1.3 --install-dir C:\Project/.dotnet"] = 0; actions.RunProcess[@"rm dotnet-install.sh"] = 0; - actions.RunProcess[@"C:\Project/.dotnet/dotnet --list-runtimes"] = 0; - actions.RunProcessOut[@"C:\Project/.dotnet/dotnet --list-runtimes"] = @"Microsoft.AspNetCore.App 2.1.3 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] -Microsoft.AspNetCore.App 2.1.4 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] -Microsoft.NETCore.App 2.1.3 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App] -Microsoft.NETCore.App 2.1.4 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]"; actions.RunProcess[@"C:\Project/.dotnet/dotnet --info"] = 0; actions.RunProcess[@"C:\Project/.dotnet/dotnet clean C:\Project/test.csproj"] = 0; actions.RunProcess[@"C:\Project/.dotnet/dotnet restore C:\Project/test.csproj"] = 0; @@ -973,7 +951,7 @@ Microsoft.NETCore.App 2.1.4 [/usr/local/share/dotnet/shared/Microsoft.NETCore.Ap actions.DownloadFiles.Add(("https://dot.net/v1/dotnet-install.sh", "dotnet-install.sh")); var autobuilder = CreateAutoBuilder(false, dotnetVersion: "2.1.3"); - TestAutobuilderScript(autobuilder, 0, 9); + TestAutobuilderScript(autobuilder, 0, 8); } private void TestDotnetVersionWindows(Action action, int commandsRun) @@ -984,7 +962,7 @@ Microsoft.NETCore.App 2.1.4 [/usr/local/share/dotnet/shared/Microsoft.NETCore.Ap actions.RunProcess[@"cmd.exe /C C:\Project\.dotnet\dotnet --info"] = 0; actions.RunProcess[@"cmd.exe /C C:\Project\.dotnet\dotnet clean C:\Project\test.csproj"] = 0; actions.RunProcess[@"cmd.exe /C C:\Project\.dotnet\dotnet restore C:\Project\test.csproj"] = 0; - actions.RunProcess[@"cmd.exe /C C:\odasa\tools\odasa index --auto C:\Project\.dotnet\dotnet build --no-incremental C:\Project\test.csproj"] = 0; + actions.RunProcess[@"cmd.exe /C C:\odasa\tools\odasa index --auto C:\Project\.dotnet\dotnet build --no-incremental /p:UseSharedCompilation=false C:\Project\test.csproj"] = 0; actions.FileExists["csharp.log"] = true; actions.FileExists[@"C:\Project\test.csproj"] = true; actions.GetEnvironmentVariable["CODEQL_EXTRACTOR_CSHARP_TRAP_DIR"] = ""; diff --git a/csharp/autobuilder/Semmle.Autobuild.CSharp/DotNetRule.cs b/csharp/autobuilder/Semmle.Autobuild.CSharp/DotNetRule.cs index a456c9407db..955775d0f66 100644 --- a/csharp/autobuilder/Semmle.Autobuild.CSharp/DotNetRule.cs +++ b/csharp/autobuilder/Semmle.Autobuild.CSharp/DotNetRule.cs @@ -36,7 +36,7 @@ namespace Semmle.Autobuild.CSharp builder.Log(Severity.Info, "Attempting to build using .NET Core"); } - return WithDotNet(builder, (dotNetPath, environment, compatibleClr) => + return WithDotNet(builder, (dotNetPath, environment) => { var ret = GetInfoCommand(builder.Actions, dotNetPath, environment); foreach (var projectOrSolution in builder.ProjectsOrSolutionsToBuild) @@ -49,7 +49,7 @@ namespace Semmle.Autobuild.CSharp restoreCommand.QuoteArgument(projectOrSolution.FullPath); var restore = restoreCommand.Script; - var build = GetBuildScript(builder, dotNetPath, environment, compatibleClr, projectOrSolution.FullPath); + var build = GetBuildScript(builder, dotNetPath, environment, projectOrSolution.FullPath); ret &= BuildScript.Try(clean) & BuildScript.Try(restore) & build; } @@ -57,7 +57,7 @@ namespace Semmle.Autobuild.CSharp }); } - private static BuildScript WithDotNet(Autobuilder builder, Func?, bool, BuildScript> f) + private static BuildScript WithDotNet(Autobuilder builder, Func?, BuildScript> f) { var installDir = builder.Actions.PathCombine(builder.Options.RootDirectory, ".dotnet"); var installScript = DownloadDotNet(builder, installDir); @@ -81,35 +81,10 @@ namespace Semmle.Autobuild.CSharp env = null; } - // The CLR tracer is always compatible on Windows - if (builder.Actions.IsWindows()) - return f(installDir, env, true); - - // The CLR tracer is only compatible on .NET Core >= 3 on Linux and macOS (see - // https://github.com/dotnet/coreclr/issues/19622) - return BuildScript.Bind(GetInstalledRuntimesScript(builder.Actions, installDir, env), (runtimes, runtimesRet) => - { - var compatibleClr = false; - if (runtimesRet == 0) - { - var minimumVersion = new Version(3, 0); - var regex = new Regex(@"Microsoft\.NETCore\.App (\d\.\d\.\d)"); - compatibleClr = runtimes - .Select(runtime => regex.Match(runtime)) - .Where(m => m.Success) - .Select(m => m.Groups[1].Value) - .Any(m => Version.TryParse(m, out var v) && v >= minimumVersion); - } - - if (!compatibleClr) - { - if (env is null) - env = new Dictionary(); - env.Add("UseSharedCompilation", "false"); - } - - return f(installDir, env, compatibleClr); - }); + if (env is null) + env = new Dictionary(); + env.Add("UseSharedCompilation", "false"); + return f(installDir, env); }); } @@ -122,7 +97,7 @@ namespace Semmle.Autobuild.CSharp /// are needed). /// public static BuildScript WithDotNet(Autobuilder builder, Func?, BuildScript> f) - => WithDotNet(builder, (_1, env, _2) => f(env)); + => WithDotNet(builder, (_1, env) => f(env)); ///

    /// Returns a script for downloading relevant versions of the @@ -259,14 +234,6 @@ namespace Semmle.Autobuild.CSharp return restore; } - private static BuildScript GetInstalledRuntimesScript(IBuildActions actions, string? dotNetPath, IDictionary? environment) - { - var listSdks = new CommandBuilder(actions, environment: environment, silent: true). - RunCommand(DotNetCommand(actions, dotNetPath)). - Argument("--list-runtimes"); - return listSdks.Script; - } - /// /// Gets the `dotnet build` script. /// @@ -276,17 +243,14 @@ namespace Semmle.Autobuild.CSharp /// hence the need for CLR tracing), by adding a /// `/p:UseSharedCompilation=false` argument. /// - private static BuildScript GetBuildScript(Autobuilder builder, string? dotNetPath, IDictionary? environment, bool compatibleClr, string projOrSln) + private static BuildScript GetBuildScript(Autobuilder builder, string? dotNetPath, IDictionary? environment, string projOrSln) { var build = new CommandBuilder(builder.Actions, null, environment); var script = builder.MaybeIndex(build, DotNetCommand(builder.Actions, dotNetPath)). Argument("build"). Argument("--no-incremental"); - return compatibleClr ? - script.Argument(builder.Options.DotNetArguments). - QuoteArgument(projOrSln). - Script : + return script.Argument("/p:UseSharedCompilation=false"). Argument(builder.Options.DotNetArguments). QuoteArgument(projOrSln). From 48b50f93c27c60ad41ebd112f6b699ac987503fa Mon Sep 17 00:00:00 2001 From: Jonathan Leitschuh Date: Wed, 12 May 2021 08:58:01 -0400 Subject: [PATCH 25/74] Update java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll Co-authored-by: Tony Torralba --- .../code/java/frameworks/jackson/JacksonSerializability.qll | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll b/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll index 09fd419642e..50266c377f8 100644 --- a/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll +++ b/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll @@ -53,7 +53,10 @@ private class JacksonWriteValueMethod extends Method, TaintPreservingCallable { private class JacksonReadValueMethod extends Method, TaintPreservingCallable { JacksonReadValueMethod() { - getDeclaringType().hasQualifiedName("com.fasterxml.jackson.databind", "ObjectReader") and + ( + getDeclaringType().hasQualifiedName("com.fasterxml.jackson.databind", "ObjectReader") or + getDeclaringType().hasQualifiedName("com.fasterxml.jackson.databind", "ObjectMapper") + ) and hasName(["readValue", "readValues"]) } From e94dab70b50ce64f692fb6e8db93149ce063f9d5 Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Wed, 12 May 2021 15:44:09 +0200 Subject: [PATCH 26/74] C++: Add sanitizers to cpp/uncontrolled-arithmetic. --- .../CWE/CWE-190/ArithmeticUncontrolled.ql | 111 +++++++++++++----- .../ArithmeticUncontrolled.expected | 46 -------- .../CWE/CWE-190/semmle/uncontrolled/test.c | 6 +- 3 files changed, 84 insertions(+), 79 deletions(-) diff --git a/cpp/ql/src/Security/CWE/CWE-190/ArithmeticUncontrolled.ql b/cpp/ql/src/Security/CWE/CWE-190/ArithmeticUncontrolled.ql index a4b0f131d14..6aad6cca7ce 100644 --- a/cpp/ql/src/Security/CWE/CWE-190/ArithmeticUncontrolled.ql +++ b/cpp/ql/src/Security/CWE/CWE-190/ArithmeticUncontrolled.ql @@ -15,34 +15,99 @@ import cpp import semmle.code.cpp.security.Overflow import semmle.code.cpp.security.Security import semmle.code.cpp.security.TaintTracking +import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis import TaintedWithPath -predicate isRandCall(FunctionCall fc) { fc.getTarget().getName() = "rand" } - -predicate isRandCallOrParent(Expr e) { - isRandCall(e) or - isRandCallOrParent(e.getAChild()) +predicate isUnboundedRandCall(FunctionCall fc) { + fc.getTarget().getName() = "rand" and not bounded(fc) } -predicate isRandValue(Expr e) { - isRandCall(e) +/** + * An operand `e` of a division expression (i.e., `e` is an operand of either a `DivExpr` or + * a `AssignDivExpr`) is bounded when `e` is the left-hand side of the division. + */ +pragma[inline] +predicate boundedDiv(Expr e, Expr left) { e = left } + +/** + * An operand `e` of a remainder expression `rem` (i.e., `rem` is either a `RemExpr` or + * an `AssignRemExpr`) with left-hand side `left` and right-ahnd side `right` is bounded + * when `e` is `left` and `right` is upper bounded by some number that is less than the maximum integer + * allowed by the result type of `rem`. + */ +pragma[inline] +predicate boundedRem(Expr e, Expr rem, Expr left, Expr right) { + e = left and + upperBound(right.getFullyConverted()) < exprMaxVal(rem.getFullyConverted()) +} + +/** + * An operand `e` of a bitwise and expression `andExpr` (i.e., `andExpr` is either an `BitwiseAndExpr` + * or an `AssignAndExpr`) with operands `operand1` and `operand2` is the operand that is not `e` is upper + * bounded by some number that is less than the maximum integer allowed by the result type of `andExpr`. + */ +pragma[inline] +predicate boundedBitwiseAnd(Expr e, Expr andExpr, Expr operand1, Expr operand2) { + operand1 != operand2 and + e = operand1 and + upperBound(operand2.getFullyConverted()) < exprMaxVal(andExpr.getFullyConverted()) +} + +/** + * Holds if `fc` is a part of the left operand of a binary operation that greatly reduces the range + * of possible values. + */ +predicate bounded(Expr e) { + // For `%` and `&` we require that `e` is bounded by a value that is strictly smaller than the + // maximum possible value of the result type of the operation. + // For example, the function call `rand()` is considered bounded in the following program: + // ``` + // int i = rand() % (UINT8_MAX + 1); + // ``` + // but not in: + // ``` + // unsigned char uc = rand() % (UINT8_MAX + 1); + // ``` + exists(RemExpr rem | boundedRem(e, rem, rem.getLeftOperand(), rem.getRightOperand())) + or + exists(AssignRemExpr rem | boundedRem(e, rem, rem.getLValue(), rem.getRValue())) + or + exists(BitwiseAndExpr andExpr | + boundedBitwiseAnd(e, andExpr, andExpr.getAnOperand(), andExpr.getAnOperand()) + ) + or + exists(AssignAndExpr andExpr | + boundedBitwiseAnd(e, andExpr, andExpr.getAnOperand(), andExpr.getAnOperand()) + ) + or + // Optimitically assume that a division always yields a much smaller value. + boundedDiv(e, any(DivExpr div).getLeftOperand()) + or + boundedDiv(e, any(AssignDivExpr div).getLValue()) +} + +predicate isUnboundedRandCallOrParent(Expr e) { + isUnboundedRandCall(e) + or + isUnboundedRandCallOrParent(e.getAChild()) +} + +predicate isUnboundedRandValue(Expr e) { + isUnboundedRandCall(e) or exists(MacroInvocation mi | e = mi.getExpr() and - isRandCallOrParent(e) + isUnboundedRandCallOrParent(e) ) } class SecurityOptionsArith extends SecurityOptions { override predicate isUserInput(Expr expr, string cause) { - isRandValue(expr) and - cause = "rand" and - not expr.getParent*() instanceof DivExpr + isUnboundedRandValue(expr) and + cause = "rand" } } -predicate isDiv(VariableAccess va) { exists(AssignDivExpr div | div.getLValue() = va) } - predicate missingGuard(VariableAccess va, string effect) { exists(Operation op | op.getAnOperand() = va | missingGuardAgainstUnderflow(op, va) and effect = "underflow" @@ -52,29 +117,15 @@ predicate missingGuard(VariableAccess va, string effect) { } class Configuration extends TaintTrackingConfiguration { - override predicate isSink(Element e) { - isDiv(e) - or - missingGuard(e, _) - } -} + override predicate isSink(Element e) { missingGuard(e, _) } -/** - * A value that undergoes division is likely to be bounded within a safe - * range. - */ -predicate guardedByAssignDiv(Expr origin) { - exists(VariableAccess va | - taintedWithPath(origin, va, _, _) and - isDiv(va) - ) + override predicate isBarrier(Expr e) { super.isBarrier(e) or bounded(e) } } from Expr origin, VariableAccess va, string effect, PathNode sourceNode, PathNode sinkNode where taintedWithPath(origin, va, sourceNode, sinkNode) and - missingGuard(va, effect) and - not guardedByAssignDiv(origin) + missingGuard(va, effect) select va, sourceNode, sinkNode, "$@ flows to here and is used in arithmetic, potentially causing an " + effect + ".", origin, "Uncontrolled value" diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/uncontrolled/ArithmeticUncontrolled.expected b/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/uncontrolled/ArithmeticUncontrolled.expected index ca8dd38fc3b..097efb73b9f 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/uncontrolled/ArithmeticUncontrolled.expected +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/uncontrolled/ArithmeticUncontrolled.expected @@ -7,30 +7,10 @@ edges | test.c:34:13:34:18 | call to rand | test.c:35:5:35:5 | r | | test.c:34:13:34:18 | call to rand | test.c:35:5:35:5 | r | | test.c:34:13:34:18 | call to rand | test.c:35:5:35:5 | r | -| test.c:39:13:39:21 | ... % ... | test.c:40:5:40:5 | r | -| test.c:39:13:39:21 | ... % ... | test.c:40:5:40:5 | r | -| test.c:39:13:39:21 | ... % ... | test.c:40:5:40:5 | r | -| test.c:39:13:39:21 | ... % ... | test.c:40:5:40:5 | r | | test.c:44:13:44:16 | call to rand | test.c:45:5:45:5 | r | | test.c:44:13:44:16 | call to rand | test.c:45:5:45:5 | r | | test.c:44:13:44:16 | call to rand | test.c:45:5:45:5 | r | | test.c:44:13:44:16 | call to rand | test.c:45:5:45:5 | r | -| test.c:54:13:54:16 | call to rand | test.c:56:5:56:5 | r | -| test.c:54:13:54:16 | call to rand | test.c:56:5:56:5 | r | -| test.c:54:13:54:16 | call to rand | test.c:56:5:56:5 | r | -| test.c:54:13:54:16 | call to rand | test.c:56:5:56:5 | r | -| test.c:60:13:60:16 | call to rand | test.c:61:5:61:5 | r | -| test.c:60:13:60:16 | call to rand | test.c:61:5:61:5 | r | -| test.c:60:13:60:16 | call to rand | test.c:61:5:61:5 | r | -| test.c:60:13:60:16 | call to rand | test.c:61:5:61:5 | r | -| test.c:60:13:60:16 | call to rand | test.c:62:5:62:5 | r | -| test.c:60:13:60:16 | call to rand | test.c:62:5:62:5 | r | -| test.c:60:13:60:16 | call to rand | test.c:62:5:62:5 | r | -| test.c:60:13:60:16 | call to rand | test.c:62:5:62:5 | r | -| test.c:66:13:66:16 | call to rand | test.c:67:5:67:5 | r | -| test.c:66:13:66:16 | call to rand | test.c:67:5:67:5 | r | -| test.c:66:13:66:16 | call to rand | test.c:67:5:67:5 | r | -| test.c:66:13:66:16 | call to rand | test.c:67:5:67:5 | r | | test.c:75:13:75:19 | ... ^ ... | test.c:77:9:77:9 | r | | test.c:75:13:75:19 | ... ^ ... | test.c:77:9:77:9 | r | | test.c:75:13:75:19 | ... ^ ... | test.c:77:9:77:9 | r | @@ -67,34 +47,11 @@ nodes | test.c:35:5:35:5 | r | semmle.label | r | | test.c:35:5:35:5 | r | semmle.label | r | | test.c:35:5:35:5 | r | semmle.label | r | -| test.c:39:13:39:21 | ... % ... | semmle.label | ... % ... | -| test.c:39:13:39:21 | ... % ... | semmle.label | ... % ... | -| test.c:40:5:40:5 | r | semmle.label | r | -| test.c:40:5:40:5 | r | semmle.label | r | -| test.c:40:5:40:5 | r | semmle.label | r | | test.c:44:13:44:16 | call to rand | semmle.label | call to rand | | test.c:44:13:44:16 | call to rand | semmle.label | call to rand | | test.c:45:5:45:5 | r | semmle.label | r | | test.c:45:5:45:5 | r | semmle.label | r | | test.c:45:5:45:5 | r | semmle.label | r | -| test.c:54:13:54:16 | call to rand | semmle.label | call to rand | -| test.c:54:13:54:16 | call to rand | semmle.label | call to rand | -| test.c:56:5:56:5 | r | semmle.label | r | -| test.c:56:5:56:5 | r | semmle.label | r | -| test.c:56:5:56:5 | r | semmle.label | r | -| test.c:60:13:60:16 | call to rand | semmle.label | call to rand | -| test.c:60:13:60:16 | call to rand | semmle.label | call to rand | -| test.c:61:5:61:5 | r | semmle.label | r | -| test.c:61:5:61:5 | r | semmle.label | r | -| test.c:61:5:61:5 | r | semmle.label | r | -| test.c:62:5:62:5 | r | semmle.label | r | -| test.c:62:5:62:5 | r | semmle.label | r | -| test.c:62:5:62:5 | r | semmle.label | r | -| test.c:66:13:66:16 | call to rand | semmle.label | call to rand | -| test.c:66:13:66:16 | call to rand | semmle.label | call to rand | -| test.c:67:5:67:5 | r | semmle.label | r | -| test.c:67:5:67:5 | r | semmle.label | r | -| test.c:67:5:67:5 | r | semmle.label | r | | test.c:75:13:75:19 | ... ^ ... | semmle.label | ... ^ ... | | test.c:75:13:75:19 | ... ^ ... | semmle.label | ... ^ ... | | test.c:77:9:77:9 | r | semmle.label | r | @@ -133,10 +90,7 @@ nodes #select | test.c:21:17:21:17 | r | test.c:18:13:18:16 | call to rand | test.c:21:17:21:17 | r | $@ flows to here and is used in arithmetic, potentially causing an overflow. | test.c:18:13:18:16 | call to rand | Uncontrolled value | | test.c:35:5:35:5 | r | test.c:34:13:34:18 | call to rand | test.c:35:5:35:5 | r | $@ flows to here and is used in arithmetic, potentially causing an overflow. | test.c:34:13:34:18 | call to rand | Uncontrolled value | -| test.c:40:5:40:5 | r | test.c:39:13:39:21 | ... % ... | test.c:40:5:40:5 | r | $@ flows to here and is used in arithmetic, potentially causing an overflow. | test.c:39:13:39:21 | ... % ... | Uncontrolled value | | test.c:45:5:45:5 | r | test.c:44:13:44:16 | call to rand | test.c:45:5:45:5 | r | $@ flows to here and is used in arithmetic, potentially causing an overflow. | test.c:44:13:44:16 | call to rand | Uncontrolled value | -| test.c:56:5:56:5 | r | test.c:54:13:54:16 | call to rand | test.c:56:5:56:5 | r | $@ flows to here and is used in arithmetic, potentially causing an overflow. | test.c:54:13:54:16 | call to rand | Uncontrolled value | -| test.c:67:5:67:5 | r | test.c:66:13:66:16 | call to rand | test.c:67:5:67:5 | r | $@ flows to here and is used in arithmetic, potentially causing an overflow. | test.c:66:13:66:16 | call to rand | Uncontrolled value | | test.c:77:9:77:9 | r | test.c:75:13:75:19 | ... ^ ... | test.c:77:9:77:9 | r | $@ flows to here and is used in arithmetic, potentially causing an underflow. | test.c:75:13:75:19 | ... ^ ... | Uncontrolled value | | test.c:100:5:100:5 | r | test.c:99:14:99:19 | call to rand | test.c:100:5:100:5 | r | $@ flows to here and is used in arithmetic, potentially causing an underflow. | test.c:99:14:99:19 | call to rand | Uncontrolled value | | test.cpp:25:7:25:7 | r | test.cpp:8:9:8:12 | call to rand | test.cpp:25:7:25:7 | r | $@ flows to here and is used in arithmetic, potentially causing an overflow. | test.cpp:8:9:8:12 | call to rand | Uncontrolled value | diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/uncontrolled/test.c b/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/uncontrolled/test.c index 2b67b499a3c..61f39a8e851 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/uncontrolled/test.c +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/uncontrolled/test.c @@ -37,7 +37,7 @@ void randomTester() { { int r = RANDN(100); - r += 100; // GOOD: The return from RANDN is bounded [FALSE POSITIVE] + r += 100; // GOOD: The return from RANDN is bounded } { @@ -53,7 +53,7 @@ void randomTester() { { int r = rand(); r = r / 10; - r += 100; // GOOD [FALSE POSITIVE] + r += 100; // GOOD } { @@ -64,7 +64,7 @@ void randomTester() { { int r = rand() & 0xFF; - r += 100; // GOOD [FALSE POSITIVE] + r += 100; // GOOD } { From e0f78dde56f7f31719c91cd0367922046edaa2ab Mon Sep 17 00:00:00 2001 From: Erik Krogh Kristensen Date: Wed, 12 May 2021 16:23:37 +0200 Subject: [PATCH 27/74] make the axios error catch match the non-error case --- .../ql/src/semmle/javascript/frameworks/ClientRequests.qll | 3 +-- .../frameworks/ClientRequests/ClientRequests.expected | 4 +++- .../ql/test/library-tests/frameworks/ClientRequests/tst.js | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/javascript/ql/src/semmle/javascript/frameworks/ClientRequests.qll b/javascript/ql/src/semmle/javascript/frameworks/ClientRequests.qll index 477f9354d03..bce5d6a36fc 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/ClientRequests.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/ClientRequests.qll @@ -271,8 +271,7 @@ module ClientRequest { or responseType = getResponseType() and promise = false and - result = - getReturn().getPromisedError().getMember("response").getMember("data").getAnImmediateUse() + result = getReturn().getPromisedError().getMember("response").getAnImmediateUse() } } diff --git a/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected b/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected index c6542eb009d..27a9fa10f72 100644 --- a/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected +++ b/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected @@ -298,4 +298,6 @@ test_getAResponseDataNode | tst.js:235:5:237:6 | needle. ... \\n }) | tst.js:235:73:235:76 | body | json | false | | tst.js:286:20:286:55 | new Web ... :8080') | tst.js:291:44:291:53 | event.data | json | false | | tst.js:296:5:299:6 | axios({ ... \\n }) | tst.js:296:5:299:6 | axios({ ... \\n }) | json | true | -| tst.js:296:5:299:6 | axios({ ... \\n }) | tst.js:303:26:303:42 | err.response.data | json | false | +| tst.js:296:5:299:6 | axios({ ... \\n }) | tst.js:302:28:302:39 | err.response | json | false | +| tst.js:296:5:299:6 | axios({ ... \\n }) | tst.js:303:26:303:37 | err.response | json | false | +| tst.js:296:5:299:6 | axios({ ... \\n }) | tst.js:304:27:304:38 | err.response | json | false | diff --git a/javascript/ql/test/library-tests/frameworks/ClientRequests/tst.js b/javascript/ql/test/library-tests/frameworks/ClientRequests/tst.js index f284ffaa407..40dcfc481f4 100644 --- a/javascript/ql/test/library-tests/frameworks/ClientRequests/tst.js +++ b/javascript/ql/test/library-tests/frameworks/ClientRequests/tst.js @@ -301,6 +301,7 @@ function moreAxios() { (err) => { const status = err.response.status; const data = err.response.data; + const agent = err.response.headers.useragent; } ); } \ No newline at end of file From 7d26aca793353c8564d5cac8715dbcecd87aa917 Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Wed, 12 May 2021 16:34:23 +0200 Subject: [PATCH 28/74] C++: Add change-note. --- cpp/change-notes/2021-12-05-uncontrolled-arithmetic.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 cpp/change-notes/2021-12-05-uncontrolled-arithmetic.md diff --git a/cpp/change-notes/2021-12-05-uncontrolled-arithmetic.md b/cpp/change-notes/2021-12-05-uncontrolled-arithmetic.md new file mode 100644 index 00000000000..56fbc9a44ce --- /dev/null +++ b/cpp/change-notes/2021-12-05-uncontrolled-arithmetic.md @@ -0,0 +1,2 @@ +lgtm,codescanning +* The query "Uncontrolled arithmetic" (`cpp/uncontrolled-arithmetic`) has been improved to produce fewer false positives. From 34fbafafde8be89e1383b378c052cbcdcdcc7000 Mon Sep 17 00:00:00 2001 From: Erik Krogh Kristensen Date: Wed, 12 May 2021 22:34:44 +0200 Subject: [PATCH 29/74] remove redundant "put" case --- .../ql/src/semmle/javascript/frameworks/ClientRequests.qll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javascript/ql/src/semmle/javascript/frameworks/ClientRequests.qll b/javascript/ql/src/semmle/javascript/frameworks/ClientRequests.qll index bce5d6a36fc..18204c5b59b 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/ClientRequests.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/ClientRequests.qll @@ -242,7 +242,7 @@ module ClientRequest { method = "request" and result = getOptionArgument(0, "data") or - method = ["post", "put", "put"] and + method = ["post", "put"] and result = [getArgument(1), getOptionArgument(2, "data")] or result = getOptionArgument([0 .. 2], ["headers", "params"]) From effa2b162a77031186bf26114e2e6e0e6e847154 Mon Sep 17 00:00:00 2001 From: haby0 Date: Thu, 6 May 2021 12:05:26 +0800 Subject: [PATCH 30/74] Add spring url redirection detect --- .../CWE/CWE-601/SpringUrlRedirect.java | 46 +++++++ .../CWE/CWE-601/SpringUrlRedirect.qhelp | 37 +++++ .../Security/CWE/CWE-601/SpringUrlRedirect.ql | 42 ++++++ .../CWE/CWE-601/SpringUrlRedirect.qll | 91 ++++++++++++ .../CWE-601/SpringUrlRedirect.expected | 19 +++ .../security/CWE-601/SpringUrlRedirect.java | 52 +++++++ .../security/CWE-601/SpringUrlRedirect.qlref | 1 + .../query-tests/security/CWE-601/options | 1 + .../web/servlet/ModelAndView.java | 107 +++++++++++++++ .../org/springframework/web/servlet/View.java | 20 +++ .../servlet/view/AbstractUrlBasedView.java | 39 ++++++ .../web/servlet/view/RedirectView.java | 129 ++++++++++++++++++ 12 files changed, 584 insertions(+) create mode 100644 java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.java create mode 100644 java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qhelp create mode 100644 java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql create mode 100644 java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll create mode 100644 java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.expected create mode 100644 java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.java create mode 100644 java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.qlref create mode 100644 java/ql/test/experimental/query-tests/security/CWE-601/options create mode 100644 java/ql/test/stubs/springframework-5.2.3/org/springframework/web/servlet/ModelAndView.java create mode 100644 java/ql/test/stubs/springframework-5.2.3/org/springframework/web/servlet/View.java create mode 100644 java/ql/test/stubs/springframework-5.2.3/org/springframework/web/servlet/view/AbstractUrlBasedView.java create mode 100644 java/ql/test/stubs/springframework-5.2.3/org/springframework/web/servlet/view/RedirectView.java diff --git a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.java b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.java new file mode 100644 index 00000000000..eba64aab6a8 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.java @@ -0,0 +1,46 @@ +import javax.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.view.RedirectView; + +@Controller +public class SpringUrlRedirect { + + private final static String VALID_REDIRECT = "http://127.0.0.1"; + + @GetMapping("url1") + public RedirectView bad1(String redirectUrl, HttpServletResponse response) throws Exception { + RedirectView rv = new RedirectView(); + rv.setUrl(redirectUrl); + return rv; + } + + @GetMapping("url2") + public String bad2(String redirectUrl) { + String url = "redirect:" + redirectUrl; + return url; + } + + @GetMapping("url3") + public RedirectView bad3(String redirectUrl) { + RedirectView rv = new RedirectView(redirectUrl); + return rv; + } + + @GetMapping("url4") + public ModelAndView bad4(String redirectUrl) { + return new ModelAndView("redirect:" + redirectUrl); + } + + @GetMapping("url5") + public RedirectView good1(String redirectUrl) { + RedirectView rv = new RedirectView(); + if (redirectUrl.startsWith(VALID_REDIRECT)){ + rv.setUrl(redirectUrl); + }else { + rv.setUrl(VALID_REDIRECT); + } + return rv; + } +} diff --git a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qhelp b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qhelp new file mode 100644 index 00000000000..6fe70dfb113 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qhelp @@ -0,0 +1,37 @@ + + + + + +

    Directly incorporating user input into a URL redirect request without validating the input +can facilitate phishing attacks. In these attacks, unsuspecting users can be redirected to a +malicious site that looks very similar to the real site they intend to visit, but which is +controlled by the attacker.

    + +
    + + +

    To guard against untrusted URL redirection, it is advisable to avoid putting user input +directly into a redirect URL. Instead, maintain a list of authorized +redirects on the server; then choose from that list based on the user input provided.

    + +
    + + +

    The following examples show the bad case and the good case respectively. +In bad1 method and bad2 method and bad3 method and +bad4 method, shows an HTTP request parameter being used directly in a URL redirect +without validating the input, which facilitates phishing attacks. In good1 method, +shows how to solve this problem by verifying whether the user input is a known fixed string beginning. +

    + + + +
    + +
  • A Guide To Spring Redirects: Spring Redirects.
  • +
  • Url redirection - attack and defense: Url Redirection.
  • +
    +
    diff --git a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql new file mode 100644 index 00000000000..138bce57ac9 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql @@ -0,0 +1,42 @@ +/** + * @name Spring url redirection from remote source + * @description Spring url redirection based on unvalidated user-input + * may cause redirection to malicious web sites. + * @kind path-problem + * @problem.severity error + * @precision high + * @id java/spring-unvalidated-url-redirection + * @tags security + * external/cwe-601 + */ + +import java +import SpringUrlRedirect +import semmle.code.java.dataflow.FlowSources +import DataFlow::PathGraph + +class SpringUrlRedirectFlowConfig extends TaintTracking::Configuration { + SpringUrlRedirectFlowConfig() { this = "SpringUrlRedirectFlowConfig" } + + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { sink instanceof SpringUrlRedirectSink } + + override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { + guard instanceof StartsWithSanitizer + } + + override predicate isSanitizer(DataFlow::Node node) { + // Exclude the case where the left side of the concatenated string is not `redirect:`. + // E.g: `String url = "/path?token=" + request.getParameter("token");` + exists(AddExpr ae | + ae.getRightOperand() = node.asExpr() and + not ae instanceof RedirectBuilderExpr + ) + } +} + +from DataFlow::PathNode source, DataFlow::PathNode sink, SpringUrlRedirectFlowConfig conf +where conf.hasFlowPath(source, sink) +select sink.getNode(), source, sink, "Potentially untrusted URL redirection due to $@.", + source.getNode(), "user-provided value" diff --git a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll new file mode 100644 index 00000000000..0ea88e84673 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll @@ -0,0 +1,91 @@ +import java +import DataFlow +import semmle.code.java.dataflow.FlowSources +import semmle.code.java.dataflow.DataFlow2 +import semmle.code.java.dataflow.TaintTracking +import semmle.code.java.frameworks.spring.SpringController + +class StartsWithSanitizer extends DataFlow::BarrierGuard { + StartsWithSanitizer() { + this.(MethodAccess).getMethod().hasName("startsWith") and + this.(MethodAccess).getMethod().getDeclaringType() instanceof TypeString and + this.(MethodAccess).getMethod().getNumberOfParameters() = 1 + } + + override predicate checks(Expr e, boolean branch) { + e = this.(MethodAccess).getQualifier() and branch = true + } +} + +/** + * A concatenate expression using the string `redirect:` on the left. + * + * E.g: `"redirect:" + redirectUrl` + */ +class RedirectBuilderExpr extends AddExpr { + RedirectBuilderExpr() { + this.getLeftOperand().(CompileTimeConstantExpr).getStringValue() = "redirect:" + } +} + +/** A URL redirection sink from spring controller method. */ +class SpringUrlRedirectSink extends DataFlow::Node { + SpringUrlRedirectSink() { + exists(RedirectBuilderExpr rbe | rbe.getRightOperand() = this.asExpr()) + or + exists(MethodAccess ma | + ma.getMethod().hasName("setUrl") and + ma.getMethod() + .getDeclaringType() + .hasQualifiedName("org.springframework.web.servlet.view", "AbstractUrlBasedView") and + ma.getArgument(0) = this.asExpr() and + exists(RedirectViewFlowConfig rvfc | rvfc.hasFlowToExpr(ma.getQualifier())) + ) + or + exists(ClassInstanceExpr cie | + cie.getConstructedType() + .hasQualifiedName("org.springframework.web.servlet.view", "RedirectView") and + cie.getArgument(0) = this.asExpr() + ) + or + exists(ClassInstanceExpr cie | + cie.getConstructedType().hasQualifiedName("org.springframework.web.servlet", "ModelAndView") and + cie.getArgument(0) = this.asExpr() and + exists(RedirectBuilderFlowConfig rstrbfc | rstrbfc.hasFlowToExpr(cie.getArgument(0))) + ) + } +} + +/** A data flow configuration tracing flow from remote sources to redirect builder expression. */ +private class RedirectBuilderFlowConfig extends DataFlow2::Configuration { + RedirectBuilderFlowConfig() { this = "RedirectBuilderFlowConfig" } + + override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { + exists(RedirectBuilderExpr rbe | rbe.getRightOperand() = sink.asExpr()) + } +} + +/** A data flow configuration tracing flow from RedirectView object to calling setUrl method. */ +private class RedirectViewFlowConfig extends DataFlow2::Configuration { + RedirectViewFlowConfig() { this = "RedirectViewFlowConfig" } + + override predicate isSource(DataFlow::Node src) { + exists(ClassInstanceExpr cie | + cie.getConstructedType() + .hasQualifiedName("org.springframework.web.servlet.view", "RedirectView") and + cie = src.asExpr() + ) + } + + override predicate isSink(DataFlow::Node sink) { + exists(MethodAccess ma | + ma.getMethod().hasName("setUrl") and + ma.getMethod() + .getDeclaringType() + .hasQualifiedName("org.springframework.web.servlet.view", "AbstractUrlBasedView") and + ma.getQualifier() = sink.asExpr() + ) + } +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.expected b/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.expected new file mode 100644 index 00000000000..fee0598bbee --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.expected @@ -0,0 +1,19 @@ +edges +| SpringUrlRedirect.java:13:30:13:47 | redirectUrl : String | SpringUrlRedirect.java:15:19:15:29 | redirectUrl | +| SpringUrlRedirect.java:20:24:20:41 | redirectUrl : String | SpringUrlRedirect.java:21:36:21:46 | redirectUrl | +| SpringUrlRedirect.java:26:30:26:47 | redirectUrl : String | SpringUrlRedirect.java:27:44:27:54 | redirectUrl | +| SpringUrlRedirect.java:32:30:32:47 | redirectUrl : String | SpringUrlRedirect.java:33:47:33:57 | redirectUrl | +nodes +| SpringUrlRedirect.java:13:30:13:47 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:15:19:15:29 | redirectUrl | semmle.label | redirectUrl | +| SpringUrlRedirect.java:20:24:20:41 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:21:36:21:46 | redirectUrl | semmle.label | redirectUrl | +| SpringUrlRedirect.java:26:30:26:47 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:27:44:27:54 | redirectUrl | semmle.label | redirectUrl | +| SpringUrlRedirect.java:32:30:32:47 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:33:47:33:57 | redirectUrl | semmle.label | redirectUrl | +#select +| SpringUrlRedirect.java:15:19:15:29 | redirectUrl | SpringUrlRedirect.java:13:30:13:47 | redirectUrl : String | SpringUrlRedirect.java:15:19:15:29 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:13:30:13:47 | redirectUrl | user-provided value | +| SpringUrlRedirect.java:21:36:21:46 | redirectUrl | SpringUrlRedirect.java:20:24:20:41 | redirectUrl : String | SpringUrlRedirect.java:21:36:21:46 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:20:24:20:41 | redirectUrl | user-provided value | +| SpringUrlRedirect.java:27:44:27:54 | redirectUrl | SpringUrlRedirect.java:26:30:26:47 | redirectUrl : String | SpringUrlRedirect.java:27:44:27:54 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:26:30:26:47 | redirectUrl | user-provided value | +| SpringUrlRedirect.java:33:47:33:57 | redirectUrl | SpringUrlRedirect.java:32:30:32:47 | redirectUrl : String | SpringUrlRedirect.java:33:47:33:57 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:32:30:32:47 | redirectUrl | user-provided value | diff --git a/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.java b/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.java new file mode 100644 index 00000000000..1438b0a63a1 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.java @@ -0,0 +1,52 @@ +import javax.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.view.RedirectView; + +@Controller +public class SpringUrlRedirect { + + private final static String VALID_REDIRECT = "http://127.0.0.1"; + + @GetMapping("url1") + public RedirectView bad1(String redirectUrl, HttpServletResponse response) throws Exception { + RedirectView rv = new RedirectView(); + rv.setUrl(redirectUrl); + return rv; + } + + @GetMapping("url2") + public String bad2(String redirectUrl) { + String url = "redirect:" + redirectUrl; + return url; + } + + @GetMapping("url3") + public RedirectView bad3(String redirectUrl) { + RedirectView rv = new RedirectView(redirectUrl); + return rv; + } + + @GetMapping("url4") + public ModelAndView bad4(String redirectUrl) { + return new ModelAndView("redirect:" + redirectUrl); + } + + @GetMapping("url5") + public RedirectView good1(String redirectUrl) { + RedirectView rv = new RedirectView(); + if (redirectUrl.startsWith(VALID_REDIRECT)){ + rv.setUrl(redirectUrl); + }else { + rv.setUrl(VALID_REDIRECT); + } + return rv; + } + + @GetMapping("url6") + public ModelAndView good2(String token) { + String url = "/edit?token=" + token; + return new ModelAndView("redirect:" + url); + } +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.qlref b/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.qlref new file mode 100644 index 00000000000..418be1d307b --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.qlref @@ -0,0 +1 @@ +experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql diff --git a/java/ql/test/experimental/query-tests/security/CWE-601/options b/java/ql/test/experimental/query-tests/security/CWE-601/options new file mode 100644 index 00000000000..a9289108747 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-601/options @@ -0,0 +1 @@ +//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/springframework-5.2.3/ \ No newline at end of file diff --git a/java/ql/test/stubs/springframework-5.2.3/org/springframework/web/servlet/ModelAndView.java b/java/ql/test/stubs/springframework-5.2.3/org/springframework/web/servlet/ModelAndView.java new file mode 100644 index 00000000000..53e337d5053 --- /dev/null +++ b/java/ql/test/stubs/springframework-5.2.3/org/springframework/web/servlet/ModelAndView.java @@ -0,0 +1,107 @@ +package org.springframework.web.servlet; + +import java.util.Map; +import org.springframework.http.HttpStatus; +import org.springframework.lang.Nullable; + +public class ModelAndView { + @Nullable + private Object view; + @Nullable + private HttpStatus status; + private boolean cleared = false; + + public ModelAndView() { + } + + public ModelAndView(String viewName) { + this.view = viewName; + } + + public ModelAndView(View view) { + this.view = view; + } + + public ModelAndView(String viewName, @Nullable Map model) { } + + public ModelAndView(View view, @Nullable Map model) { } + + public ModelAndView(String viewName, HttpStatus status) { } + + public ModelAndView(@Nullable String viewName, @Nullable Map model, @Nullable HttpStatus status) { } + + public ModelAndView(String viewName, String modelName, Object modelObject) { } + + public ModelAndView(View view, String modelName, Object modelObject) { } + + public void setViewName(@Nullable String viewName) { + this.view = viewName; + } + + @Nullable + public String getViewName() { + return ""; + } + + public void setView(@Nullable View view) { } + + @Nullable + public View getView() { + return null; + } + + public boolean hasView() { + return true; + } + + public boolean isReference() { + return true; + } + + @Nullable + protected Map getModelInternal() { + return null; + } + + public Map getModel() { + return null; + } + + public void setStatus(@Nullable HttpStatus status) { } + + @Nullable + public HttpStatus getStatus() { + return this.status; + } + + public ModelAndView addObject(String attributeName, @Nullable Object attributeValue) { + return null; + } + + public ModelAndView addObject(Object attributeValue) { + return null; + } + + public ModelAndView addAllObjects(@Nullable Map modelMap) { + return null; + } + + public void clear() { } + + public boolean isEmpty() { + return true; + } + + public boolean wasCleared() { + return true; + } + + public String toString() { + return ""; + } + + private String formatView() { + return ""; + } +} + diff --git a/java/ql/test/stubs/springframework-5.2.3/org/springframework/web/servlet/View.java b/java/ql/test/stubs/springframework-5.2.3/org/springframework/web/servlet/View.java new file mode 100644 index 00000000000..b2281b8c250 --- /dev/null +++ b/java/ql/test/stubs/springframework-5.2.3/org/springframework/web/servlet/View.java @@ -0,0 +1,20 @@ +package org.springframework.web.servlet; + +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.lang.Nullable; + +public interface View { + String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus"; + String PATH_VARIABLES = View.class.getName() + ".pathVariables"; + String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType"; + + @Nullable + default String getContentType() { + return null; + } + + void render(@Nullable Map var1, HttpServletRequest var2, HttpServletResponse var3) throws Exception; +} + diff --git a/java/ql/test/stubs/springframework-5.2.3/org/springframework/web/servlet/view/AbstractUrlBasedView.java b/java/ql/test/stubs/springframework-5.2.3/org/springframework/web/servlet/view/AbstractUrlBasedView.java new file mode 100644 index 00000000000..9efd87af12f --- /dev/null +++ b/java/ql/test/stubs/springframework-5.2.3/org/springframework/web/servlet/view/AbstractUrlBasedView.java @@ -0,0 +1,39 @@ +package org.springframework.web.servlet.view; + +import java.util.Locale; +import org.springframework.lang.Nullable; + +public abstract class AbstractUrlBasedView { + @Nullable + private String url; + + protected AbstractUrlBasedView() { } + + protected AbstractUrlBasedView(String url) { + this.url = url; + } + + public void setUrl(@Nullable String url) { + this.url = url; + } + + @Nullable + public String getUrl() { + return ""; + } + + public void afterPropertiesSet() throws Exception { } + + protected boolean isUrlRequired() { + return true; + } + + public boolean checkResource(Locale locale) throws Exception { + return true; + } + + public String toString() { + return ""; + } +} + diff --git a/java/ql/test/stubs/springframework-5.2.3/org/springframework/web/servlet/view/RedirectView.java b/java/ql/test/stubs/springframework-5.2.3/org/springframework/web/servlet/view/RedirectView.java new file mode 100644 index 00000000000..ee18868231a --- /dev/null +++ b/java/ql/test/stubs/springframework-5.2.3/org/springframework/web/servlet/view/RedirectView.java @@ -0,0 +1,129 @@ +package org.springframework.web.servlet.view; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Array; +import java.net.URLEncoder; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; +import org.springframework.lang.Nullable; + +public class RedirectView extends AbstractUrlBasedView { + private static final Pattern URI_TEMPLATE_VARIABLE_PATTERN = Pattern.compile("\\{([^/]+?)\\}"); + private boolean contextRelative = false; + private boolean http10Compatible = true; + private boolean exposeModelAttributes = true; + @Nullable + private String encodingScheme; + @Nullable + private HttpStatus statusCode; + private boolean expandUriTemplateVariables = true; + private boolean propagateQueryParams = false; + @Nullable + private String[] hosts; + + public RedirectView() { } + + public RedirectView(String url) { } + + public RedirectView(String url, boolean contextRelative) { } + + public RedirectView(String url, boolean contextRelative, boolean http10Compatible) { } + + public RedirectView(String url, boolean contextRelative, boolean http10Compatible, boolean exposeModelAttributes) { } + + public void setContextRelative(boolean contextRelative) { } + + public void setHttp10Compatible(boolean http10Compatible) { } + + public void setExposeModelAttributes(boolean exposeModelAttributes) { } + + public void setEncodingScheme(String encodingScheme) { } + + public void setStatusCode(HttpStatus statusCode) { } + + public void setExpandUriTemplateVariables(boolean expandUriTemplateVariables) { } + + public void setPropagateQueryParams(boolean propagateQueryParams) { } + + public boolean isPropagateQueryProperties() { + return true; + } + + public void setHosts(@Nullable String... hosts) { } + + @Nullable + public String[] getHosts() { + return this.hosts; + } + + public boolean isRedirectView() { + return true; + } + + protected boolean isContextRequired() { + return false; + } + + protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) throws IOException { } + + protected final String createTargetUrl(Map model, HttpServletRequest request) throws UnsupportedEncodingException { + return ""; + } + + private String getContextPath(HttpServletRequest request) { + return ""; + } + + protected StringBuilder replaceUriTemplateVariables(String targetUrl, Map model, Map currentUriVariables, String encodingScheme) throws UnsupportedEncodingException { + return null; + } + + private Map getCurrentRequestUriVariables(HttpServletRequest request) { + return null; + } + + protected void appendCurrentQueryParams(StringBuilder targetUrl, HttpServletRequest request) { } + + protected void appendQueryProperties(StringBuilder targetUrl, Map model, String encodingScheme) throws UnsupportedEncodingException { } + + protected Map queryProperties(Map model) { + return null; + } + + protected boolean isEligibleProperty(String key, @Nullable Object value) { + return true; + } + + protected boolean isEligibleValue(@Nullable Object value) { + return true; + } + + protected String urlEncode(String input, String encodingScheme) throws UnsupportedEncodingException { + return ""; + } + + protected String updateTargetUrl(String targetUrl, Map model, HttpServletRequest request, HttpServletResponse response) { + return ""; + } + + protected void sendRedirect(HttpServletRequest request, HttpServletResponse response, String targetUrl, boolean http10Compatible) throws IOException { } + + protected boolean isRemoteHost(String targetUrl) { + return true; + } + + protected HttpStatus getHttp11StatusCode(HttpServletRequest request, HttpServletResponse response, String targetUrl) { + return this.statusCode; + } +} + From 02e415045f04957a33049907bd3256ce84ad7378 Mon Sep 17 00:00:00 2001 From: haby0 Date: Thu, 13 May 2021 15:48:15 +0800 Subject: [PATCH 31/74] Delete RedirectBuilderFlowConfig --- .../Security/CWE/CWE-601/SpringUrlRedirect.qll | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll index 0ea88e84673..1ab5f3cd0b1 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll +++ b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll @@ -51,22 +51,11 @@ class SpringUrlRedirectSink extends DataFlow::Node { exists(ClassInstanceExpr cie | cie.getConstructedType().hasQualifiedName("org.springframework.web.servlet", "ModelAndView") and cie.getArgument(0) = this.asExpr() and - exists(RedirectBuilderFlowConfig rstrbfc | rstrbfc.hasFlowToExpr(cie.getArgument(0))) + exists(RedirectBuilderExpr rbe | rbe.getRightOperand() = this.asExpr()) ) } } -/** A data flow configuration tracing flow from remote sources to redirect builder expression. */ -private class RedirectBuilderFlowConfig extends DataFlow2::Configuration { - RedirectBuilderFlowConfig() { this = "RedirectBuilderFlowConfig" } - - override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } - - override predicate isSink(DataFlow::Node sink) { - exists(RedirectBuilderExpr rbe | rbe.getRightOperand() = sink.asExpr()) - } -} - /** A data flow configuration tracing flow from RedirectView object to calling setUrl method. */ private class RedirectViewFlowConfig extends DataFlow2::Configuration { RedirectViewFlowConfig() { this = "RedirectViewFlowConfig" } From 51067af784f47fccb040a6d99ac16491b6a58cf6 Mon Sep 17 00:00:00 2001 From: Erik Krogh Kristensen Date: Thu, 13 May 2021 22:34:10 +0200 Subject: [PATCH 32/74] add "uid" (and friends) as maybe being sensitive account info --- .../internal/SensitiveDataHeuristics.qll | 3 +- .../CWE-338/InsecureRandomness.expected | 40 +++++++++++++++++++ .../test/query-tests/Security/CWE-338/tst.js | 9 ++++- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/javascript/ql/src/semmle/javascript/security/internal/SensitiveDataHeuristics.qll b/javascript/ql/src/semmle/javascript/security/internal/SensitiveDataHeuristics.qll index ddf95b1b534..9a3a306c159 100644 --- a/javascript/ql/src/semmle/javascript/security/internal/SensitiveDataHeuristics.qll +++ b/javascript/ql/src/semmle/javascript/security/internal/SensitiveDataHeuristics.qll @@ -58,7 +58,8 @@ module HeuristicNames { */ string maybeAccountInfo() { result = "(?is).*acc(ou)?nt.*" or - result = "(?is).*(puid|username|userid).*" + result = "(?is).*(puid|username|userid).*" or + result = "(?is).*(u|^|_|[a-z(?=U)])(uid).*" } /** diff --git a/javascript/ql/test/query-tests/Security/CWE-338/InsecureRandomness.expected b/javascript/ql/test/query-tests/Security/CWE-338/InsecureRandomness.expected index e4ab385cc07..4db1bb3b088 100644 --- a/javascript/ql/test/query-tests/Security/CWE-338/InsecureRandomness.expected +++ b/javascript/ql/test/query-tests/Security/CWE-338/InsecureRandomness.expected @@ -66,6 +66,26 @@ nodes | tst.js:95:33:95:45 | Math.random() | | tst.js:95:33:95:45 | Math.random() | | tst.js:95:33:95:45 | Math.random() | +| tst.js:115:16:115:56 | Math.fl ... 00_000) | +| tst.js:115:16:115:56 | Math.fl ... 00_000) | +| tst.js:115:27:115:39 | Math.random() | +| tst.js:115:27:115:39 | Math.random() | +| tst.js:115:27:115:55 | Math.ra ... 000_000 | +| tst.js:116:22:116:62 | Math.fl ... 00_000) | +| tst.js:116:22:116:62 | Math.fl ... 00_000) | +| tst.js:116:33:116:45 | Math.random() | +| tst.js:116:33:116:45 | Math.random() | +| tst.js:116:33:116:61 | Math.ra ... 000_000 | +| tst.js:117:15:117:55 | Math.fl ... 00_000) | +| tst.js:117:15:117:55 | Math.fl ... 00_000) | +| tst.js:117:26:117:38 | Math.random() | +| tst.js:117:26:117:38 | Math.random() | +| tst.js:117:26:117:54 | Math.ra ... 000_000 | +| tst.js:118:23:118:63 | Math.fl ... 00_000) | +| tst.js:118:23:118:63 | Math.fl ... 00_000) | +| tst.js:118:34:118:46 | Math.random() | +| tst.js:118:34:118:46 | Math.random() | +| tst.js:118:34:118:62 | Math.ra ... 000_000 | edges | tst.js:2:20:2:32 | Math.random() | tst.js:2:20:2:32 | Math.random() | | tst.js:6:31:6:43 | Math.random() | tst.js:6:20:6:43 | "prefix ... andom() | @@ -114,6 +134,22 @@ edges | tst.js:84:19:84:31 | Math.random() | tst.js:84:19:84:31 | Math.random() | | tst.js:90:32:90:44 | Math.random() | tst.js:90:32:90:44 | Math.random() | | tst.js:95:33:95:45 | Math.random() | tst.js:95:33:95:45 | Math.random() | +| tst.js:115:27:115:39 | Math.random() | tst.js:115:27:115:55 | Math.ra ... 000_000 | +| tst.js:115:27:115:39 | Math.random() | tst.js:115:27:115:55 | Math.ra ... 000_000 | +| tst.js:115:27:115:55 | Math.ra ... 000_000 | tst.js:115:16:115:56 | Math.fl ... 00_000) | +| tst.js:115:27:115:55 | Math.ra ... 000_000 | tst.js:115:16:115:56 | Math.fl ... 00_000) | +| tst.js:116:33:116:45 | Math.random() | tst.js:116:33:116:61 | Math.ra ... 000_000 | +| tst.js:116:33:116:45 | Math.random() | tst.js:116:33:116:61 | Math.ra ... 000_000 | +| tst.js:116:33:116:61 | Math.ra ... 000_000 | tst.js:116:22:116:62 | Math.fl ... 00_000) | +| tst.js:116:33:116:61 | Math.ra ... 000_000 | tst.js:116:22:116:62 | Math.fl ... 00_000) | +| tst.js:117:26:117:38 | Math.random() | tst.js:117:26:117:54 | Math.ra ... 000_000 | +| tst.js:117:26:117:38 | Math.random() | tst.js:117:26:117:54 | Math.ra ... 000_000 | +| tst.js:117:26:117:54 | Math.ra ... 000_000 | tst.js:117:15:117:55 | Math.fl ... 00_000) | +| tst.js:117:26:117:54 | Math.ra ... 000_000 | tst.js:117:15:117:55 | Math.fl ... 00_000) | +| tst.js:118:34:118:46 | Math.random() | tst.js:118:34:118:62 | Math.ra ... 000_000 | +| tst.js:118:34:118:46 | Math.random() | tst.js:118:34:118:62 | Math.ra ... 000_000 | +| tst.js:118:34:118:62 | Math.ra ... 000_000 | tst.js:118:23:118:63 | Math.fl ... 00_000) | +| tst.js:118:34:118:62 | Math.ra ... 000_000 | tst.js:118:23:118:63 | Math.fl ... 00_000) | #select | tst.js:2:20:2:32 | Math.random() | tst.js:2:20:2:32 | Math.random() | tst.js:2:20:2:32 | Math.random() | Cryptographically insecure $@ in a security context. | tst.js:2:20:2:32 | Math.random() | random value | | tst.js:6:20:6:43 | "prefix ... andom() | tst.js:6:31:6:43 | Math.random() | tst.js:6:20:6:43 | "prefix ... andom() | Cryptographically insecure $@ in a security context. | tst.js:6:31:6:43 | Math.random() | random value | @@ -131,3 +167,7 @@ edges | tst.js:84:19:84:31 | Math.random() | tst.js:84:19:84:31 | Math.random() | tst.js:84:19:84:31 | Math.random() | Cryptographically insecure $@ in a security context. | tst.js:84:19:84:31 | Math.random() | random value | | tst.js:90:32:90:44 | Math.random() | tst.js:90:32:90:44 | Math.random() | tst.js:90:32:90:44 | Math.random() | Cryptographically insecure $@ in a security context. | tst.js:90:32:90:44 | Math.random() | random value | | tst.js:95:33:95:45 | Math.random() | tst.js:95:33:95:45 | Math.random() | tst.js:95:33:95:45 | Math.random() | Cryptographically insecure $@ in a security context. | tst.js:95:33:95:45 | Math.random() | random value | +| tst.js:115:16:115:56 | Math.fl ... 00_000) | tst.js:115:27:115:39 | Math.random() | tst.js:115:16:115:56 | Math.fl ... 00_000) | Cryptographically insecure $@ in a security context. | tst.js:115:27:115:39 | Math.random() | random value | +| tst.js:116:22:116:62 | Math.fl ... 00_000) | tst.js:116:33:116:45 | Math.random() | tst.js:116:22:116:62 | Math.fl ... 00_000) | Cryptographically insecure $@ in a security context. | tst.js:116:33:116:45 | Math.random() | random value | +| tst.js:117:15:117:55 | Math.fl ... 00_000) | tst.js:117:26:117:38 | Math.random() | tst.js:117:15:117:55 | Math.fl ... 00_000) | Cryptographically insecure $@ in a security context. | tst.js:117:26:117:38 | Math.random() | random value | +| tst.js:118:23:118:63 | Math.fl ... 00_000) | tst.js:118:34:118:46 | Math.random() | tst.js:118:23:118:63 | Math.fl ... 00_000) | Cryptographically insecure $@ in a security context. | tst.js:118:34:118:46 | Math.random() | random value | diff --git a/javascript/ql/test/query-tests/Security/CWE-338/tst.js b/javascript/ql/test/query-tests/Security/CWE-338/tst.js index 123799426b5..6a1abf1403c 100644 --- a/javascript/ql/test/query-tests/Security/CWE-338/tst.js +++ b/javascript/ql/test/query-tests/Security/CWE-338/tst.js @@ -109,4 +109,11 @@ function f18() { } }; var secret = genRandom(); // OK - Math.random() is only a fallback. -})(); \ No newline at end of file +})(); + +function uid() { + var uuid = Math.floor(Math.random() * 4_000_000_000); // NOT OK + var sessionUid = Math.floor(Math.random() * 4_000_000_000); // NOT OK + var uid = Math.floor(Math.random() * 4_000_000_000); // NOT OK + var my_nice_uid = Math.floor(Math.random() * 4_000_000_000); // NOT OK +} \ No newline at end of file From 662e335424a7205100388feaa263db724a850191 Mon Sep 17 00:00:00 2001 From: Erik Krogh Kristensen Date: Thu, 13 May 2021 22:54:39 +0200 Subject: [PATCH 33/74] keep python in sync --- .../python/security/internal/SensitiveDataHeuristics.qll | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/ql/src/semmle/python/security/internal/SensitiveDataHeuristics.qll b/python/ql/src/semmle/python/security/internal/SensitiveDataHeuristics.qll index ddf95b1b534..9a3a306c159 100644 --- a/python/ql/src/semmle/python/security/internal/SensitiveDataHeuristics.qll +++ b/python/ql/src/semmle/python/security/internal/SensitiveDataHeuristics.qll @@ -58,7 +58,8 @@ module HeuristicNames { */ string maybeAccountInfo() { result = "(?is).*acc(ou)?nt.*" or - result = "(?is).*(puid|username|userid).*" + result = "(?is).*(puid|username|userid).*" or + result = "(?is).*(u|^|_|[a-z(?=U)])(uid).*" } /** From 9d60ec035f991655e7ea4ead67552ec567df64c2 Mon Sep 17 00:00:00 2001 From: Erik Krogh Kristensen Date: Thu, 13 May 2021 23:04:30 +0200 Subject: [PATCH 34/74] fix casing on the uid regexp --- .../security/internal/SensitiveDataHeuristics.qll | 2 +- .../Security/CWE-338/InsecureRandomness.expected | 10 ++++++++++ javascript/ql/test/query-tests/Security/CWE-338/tst.js | 3 +++ .../security/internal/SensitiveDataHeuristics.qll | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/javascript/ql/src/semmle/javascript/security/internal/SensitiveDataHeuristics.qll b/javascript/ql/src/semmle/javascript/security/internal/SensitiveDataHeuristics.qll index 9a3a306c159..589c37120b9 100644 --- a/javascript/ql/src/semmle/javascript/security/internal/SensitiveDataHeuristics.qll +++ b/javascript/ql/src/semmle/javascript/security/internal/SensitiveDataHeuristics.qll @@ -59,7 +59,7 @@ module HeuristicNames { string maybeAccountInfo() { result = "(?is).*acc(ou)?nt.*" or result = "(?is).*(puid|username|userid).*" or - result = "(?is).*(u|^|_|[a-z(?=U)])(uid).*" + result = "(?s).*([uU]|^|_|[a-z](?=U))([uU][iI][dD]).*" } /** diff --git a/javascript/ql/test/query-tests/Security/CWE-338/InsecureRandomness.expected b/javascript/ql/test/query-tests/Security/CWE-338/InsecureRandomness.expected index 4db1bb3b088..42da210c266 100644 --- a/javascript/ql/test/query-tests/Security/CWE-338/InsecureRandomness.expected +++ b/javascript/ql/test/query-tests/Security/CWE-338/InsecureRandomness.expected @@ -86,6 +86,12 @@ nodes | tst.js:118:34:118:46 | Math.random() | | tst.js:118:34:118:46 | Math.random() | | tst.js:118:34:118:62 | Math.ra ... 000_000 | +| tst.js:120:16:120:28 | Math.random() | +| tst.js:120:16:120:28 | Math.random() | +| tst.js:120:16:120:28 | Math.random() | +| tst.js:121:18:121:30 | Math.random() | +| tst.js:121:18:121:30 | Math.random() | +| tst.js:121:18:121:30 | Math.random() | edges | tst.js:2:20:2:32 | Math.random() | tst.js:2:20:2:32 | Math.random() | | tst.js:6:31:6:43 | Math.random() | tst.js:6:20:6:43 | "prefix ... andom() | @@ -150,6 +156,8 @@ edges | tst.js:118:34:118:46 | Math.random() | tst.js:118:34:118:62 | Math.ra ... 000_000 | | tst.js:118:34:118:62 | Math.ra ... 000_000 | tst.js:118:23:118:63 | Math.fl ... 00_000) | | tst.js:118:34:118:62 | Math.ra ... 000_000 | tst.js:118:23:118:63 | Math.fl ... 00_000) | +| tst.js:120:16:120:28 | Math.random() | tst.js:120:16:120:28 | Math.random() | +| tst.js:121:18:121:30 | Math.random() | tst.js:121:18:121:30 | Math.random() | #select | tst.js:2:20:2:32 | Math.random() | tst.js:2:20:2:32 | Math.random() | tst.js:2:20:2:32 | Math.random() | Cryptographically insecure $@ in a security context. | tst.js:2:20:2:32 | Math.random() | random value | | tst.js:6:20:6:43 | "prefix ... andom() | tst.js:6:31:6:43 | Math.random() | tst.js:6:20:6:43 | "prefix ... andom() | Cryptographically insecure $@ in a security context. | tst.js:6:31:6:43 | Math.random() | random value | @@ -171,3 +179,5 @@ edges | tst.js:116:22:116:62 | Math.fl ... 00_000) | tst.js:116:33:116:45 | Math.random() | tst.js:116:22:116:62 | Math.fl ... 00_000) | Cryptographically insecure $@ in a security context. | tst.js:116:33:116:45 | Math.random() | random value | | tst.js:117:15:117:55 | Math.fl ... 00_000) | tst.js:117:26:117:38 | Math.random() | tst.js:117:15:117:55 | Math.fl ... 00_000) | Cryptographically insecure $@ in a security context. | tst.js:117:26:117:38 | Math.random() | random value | | tst.js:118:23:118:63 | Math.fl ... 00_000) | tst.js:118:34:118:46 | Math.random() | tst.js:118:23:118:63 | Math.fl ... 00_000) | Cryptographically insecure $@ in a security context. | tst.js:118:34:118:46 | Math.random() | random value | +| tst.js:120:16:120:28 | Math.random() | tst.js:120:16:120:28 | Math.random() | tst.js:120:16:120:28 | Math.random() | Cryptographically insecure $@ in a security context. | tst.js:120:16:120:28 | Math.random() | random value | +| tst.js:121:18:121:30 | Math.random() | tst.js:121:18:121:30 | Math.random() | tst.js:121:18:121:30 | Math.random() | Cryptographically insecure $@ in a security context. | tst.js:121:18:121:30 | Math.random() | random value | diff --git a/javascript/ql/test/query-tests/Security/CWE-338/tst.js b/javascript/ql/test/query-tests/Security/CWE-338/tst.js index 6a1abf1403c..77393b8983c 100644 --- a/javascript/ql/test/query-tests/Security/CWE-338/tst.js +++ b/javascript/ql/test/query-tests/Security/CWE-338/tst.js @@ -116,4 +116,7 @@ function uid() { var sessionUid = Math.floor(Math.random() * 4_000_000_000); // NOT OK var uid = Math.floor(Math.random() * 4_000_000_000); // NOT OK var my_nice_uid = Math.floor(Math.random() * 4_000_000_000); // NOT OK + var liquid = Math.random(); // OK + var UUID = Math.random(); // NOT OK + var MY_UID = Math.random(); // NOK OK } \ No newline at end of file diff --git a/python/ql/src/semmle/python/security/internal/SensitiveDataHeuristics.qll b/python/ql/src/semmle/python/security/internal/SensitiveDataHeuristics.qll index 9a3a306c159..589c37120b9 100644 --- a/python/ql/src/semmle/python/security/internal/SensitiveDataHeuristics.qll +++ b/python/ql/src/semmle/python/security/internal/SensitiveDataHeuristics.qll @@ -59,7 +59,7 @@ module HeuristicNames { string maybeAccountInfo() { result = "(?is).*acc(ou)?nt.*" or result = "(?is).*(puid|username|userid).*" or - result = "(?is).*(u|^|_|[a-z(?=U)])(uid).*" + result = "(?s).*([uU]|^|_|[a-z](?=U))([uU][iI][dD]).*" } /** From 406fb1e383dcf3408b43fc752bf608bdc3b68fda Mon Sep 17 00:00:00 2001 From: Ethan P <56270045+ethanpalm@users.noreply.github.com> Date: Thu, 13 May 2021 17:29:34 -0400 Subject: [PATCH 35/74] Update with Go custom build options --- .../codeql-cli/creating-codeql-databases.rst | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/codeql/codeql-cli/creating-codeql-databases.rst b/docs/codeql/codeql-cli/creating-codeql-databases.rst index 4f7212050df..a41727e1956 100644 --- a/docs/codeql/codeql-cli/creating-codeql-databases.rst +++ b/docs/codeql/codeql-cli/creating-codeql-databases.rst @@ -165,13 +165,14 @@ build steps, you may need to explicitly define each step in the command line. .. pull-quote:: Creating databases for Go - For Go, you should always use the CodeQL autobuilder. Install the Go - toolchain (version 1.11 or later) and, if there are dependencies, the - appropriate dependency manager (such as `dep + For Go, install the Go toolchain (version 1.11 or later) and, if there + are dependencies, the appropriate dependency manager (such as `dep `__). - Do not specify any build commands, as you will override the autobuilder - invocation, which will create an empty database. + The Go autobuilder attempts to automatically detect Go code in a repository, + and only runs build scripts in an attempt to fetch dependencies. To force + CodeQL to use your build script, set the environment variable + `CODEQL_EXTRACTOR_GO_BUILD_TRACING=on` or pass a command. Specifying build commands ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -200,6 +201,10 @@ commands that you can specify for compiled languages. codeql database create csharp-database --language=csharp --command='dotnet build /p:UseSharedCompilation=false /t:rebuild' +- Go project built using a custom build script:: + + CODEQL_EXTRACTOR_GO_BUILD_TRACING=on codeql database create go-database --language=go --command='./scripts/build.sh' + - Java project built using Gradle:: codeql database create java-database --language=java --command='gradle clean test' From 498c99e26ca556e466f00015a6203f3ce64a5362 Mon Sep 17 00:00:00 2001 From: haby0 Date: Fri, 14 May 2021 16:31:59 +0800 Subject: [PATCH 36/74] Add left value, Add return expression tracing flow --- .../CWE/CWE-601/SpringUrlRedirect.qll | 70 +++++++++++++++++-- .../CWE-601/SpringUrlRedirect.expected | 8 +++ .../security/CWE-601/SpringUrlRedirect.java | 18 ++++- 3 files changed, 90 insertions(+), 6 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll index 1ab5f3cd0b1..866eaae1c34 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll +++ b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll @@ -18,20 +18,48 @@ class StartsWithSanitizer extends DataFlow::BarrierGuard { } /** - * A concatenate expression using the string `redirect:` on the left. + * A concatenate expression using the string `redirect:` or `ajaxredirect:` or `forward:` on the left. * * E.g: `"redirect:" + redirectUrl` */ class RedirectBuilderExpr extends AddExpr { RedirectBuilderExpr() { - this.getLeftOperand().(CompileTimeConstantExpr).getStringValue() = "redirect:" + this.getLeftOperand().(CompileTimeConstantExpr).getStringValue() in [ + "redirect:", "ajaxredirect:", "forward:" + ] + } +} + +/** + * A call to `StringBuilder.append` or `StringBuffer.append` method, and the parameter value is + * `"redirect:"` or `"ajaxredirect:"` or `"forward:"`. + * + * E.g: `StringBuilder.append("redirect:")` + */ +class RedirectAppendCall extends MethodAccess { + RedirectAppendCall() { + this.getMethod().hasName("append") and + this.getMethod().getDeclaringType() instanceof StringBuildingType and + this.getArgument(0).(CompileTimeConstantExpr).getStringValue() in [ + "redirect:", "ajaxredirect:", "forward:" + ] } } /** A URL redirection sink from spring controller method. */ class SpringUrlRedirectSink extends DataFlow::Node { SpringUrlRedirectSink() { - exists(RedirectBuilderExpr rbe | rbe.getRightOperand() = this.asExpr()) + exists(RedirectBuilderExpr rbe | + rbe.getRightOperand() = this.asExpr() and + exists(RedirectBuilderFlowConfig rbfc | rbfc.hasFlow(exprNode(rbe), _)) + ) + or + exists(MethodAccess ma, RedirectAppendCall rac | + DataFlow2::localExprFlow(rac.getQualifier(), ma.getQualifier()) and + ma.getMethod().hasName("append") and + ma.getArgument(0) = this.asExpr() and + exists(RedirectBuilderFlowConfig rbfc | rbfc.hasFlow(exprNode(ma.getQualifier()), _)) + ) or exists(MethodAccess ma | ma.getMethod().hasName("setUrl") and @@ -50,8 +78,40 @@ class SpringUrlRedirectSink extends DataFlow::Node { or exists(ClassInstanceExpr cie | cie.getConstructedType().hasQualifiedName("org.springframework.web.servlet", "ModelAndView") and - cie.getArgument(0) = this.asExpr() and - exists(RedirectBuilderExpr rbe | rbe.getRightOperand() = this.asExpr()) + exists(RedirectBuilderExpr rbe | + rbe = cie.getArgument(0) and rbe.getRightOperand() = this.asExpr() + ) + ) + } +} + +/** A data flow configuration tracing flow from redirect builder expression to spring controller method return expression. */ +private class RedirectBuilderFlowConfig extends DataFlow2::Configuration { + RedirectBuilderFlowConfig() { this = "RedirectBuilderFlowConfig" } + + override predicate isSource(DataFlow::Node src) { + exists(RedirectBuilderExpr rbe | rbe = src.asExpr()) + or + exists(MethodAccess ma, RedirectAppendCall rac | + DataFlow2::localExprFlow(rac.getQualifier(), ma.getQualifier()) and + ma.getMethod().hasName("append") and + ma.getQualifier() = src.asExpr() + ) + } + + override predicate isSink(DataFlow::Node sink) { + exists(ReturnStmt rs, SpringRequestMappingMethod sqmm | + rs.getResult() = sink.asExpr() and + sqmm.getBody().getAStmt() = rs + ) + } + + override predicate isAdditionalFlowStep(Node prod, Node succ) { + exists(MethodAccess ma | + ma.getMethod().hasName("toString") and + ma.getMethod().getDeclaringType() instanceof StringBuildingType and + ma.getQualifier() = prod.asExpr() and + ma = succ.asExpr() ) } } diff --git a/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.expected b/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.expected index fee0598bbee..26b8acd7770 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.expected +++ b/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.expected @@ -3,6 +3,8 @@ edges | SpringUrlRedirect.java:20:24:20:41 | redirectUrl : String | SpringUrlRedirect.java:21:36:21:46 | redirectUrl | | SpringUrlRedirect.java:26:30:26:47 | redirectUrl : String | SpringUrlRedirect.java:27:44:27:54 | redirectUrl | | SpringUrlRedirect.java:32:30:32:47 | redirectUrl : String | SpringUrlRedirect.java:33:47:33:57 | redirectUrl | +| SpringUrlRedirect.java:37:24:37:41 | redirectUrl : String | SpringUrlRedirect.java:40:29:40:39 | redirectUrl | +| SpringUrlRedirect.java:45:24:45:41 | redirectUrl : String | SpringUrlRedirect.java:48:30:48:40 | redirectUrl | nodes | SpringUrlRedirect.java:13:30:13:47 | redirectUrl : String | semmle.label | redirectUrl : String | | SpringUrlRedirect.java:15:19:15:29 | redirectUrl | semmle.label | redirectUrl | @@ -12,8 +14,14 @@ nodes | SpringUrlRedirect.java:27:44:27:54 | redirectUrl | semmle.label | redirectUrl | | SpringUrlRedirect.java:32:30:32:47 | redirectUrl : String | semmle.label | redirectUrl : String | | SpringUrlRedirect.java:33:47:33:57 | redirectUrl | semmle.label | redirectUrl | +| SpringUrlRedirect.java:37:24:37:41 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:40:29:40:39 | redirectUrl | semmle.label | redirectUrl | +| SpringUrlRedirect.java:45:24:45:41 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:48:30:48:40 | redirectUrl | semmle.label | redirectUrl | #select | SpringUrlRedirect.java:15:19:15:29 | redirectUrl | SpringUrlRedirect.java:13:30:13:47 | redirectUrl : String | SpringUrlRedirect.java:15:19:15:29 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:13:30:13:47 | redirectUrl | user-provided value | | SpringUrlRedirect.java:21:36:21:46 | redirectUrl | SpringUrlRedirect.java:20:24:20:41 | redirectUrl : String | SpringUrlRedirect.java:21:36:21:46 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:20:24:20:41 | redirectUrl | user-provided value | | SpringUrlRedirect.java:27:44:27:54 | redirectUrl | SpringUrlRedirect.java:26:30:26:47 | redirectUrl : String | SpringUrlRedirect.java:27:44:27:54 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:26:30:26:47 | redirectUrl | user-provided value | | SpringUrlRedirect.java:33:47:33:57 | redirectUrl | SpringUrlRedirect.java:32:30:32:47 | redirectUrl : String | SpringUrlRedirect.java:33:47:33:57 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:32:30:32:47 | redirectUrl | user-provided value | +| SpringUrlRedirect.java:40:29:40:39 | redirectUrl | SpringUrlRedirect.java:37:24:37:41 | redirectUrl : String | SpringUrlRedirect.java:40:29:40:39 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:37:24:37:41 | redirectUrl | user-provided value | +| SpringUrlRedirect.java:48:30:48:40 | redirectUrl | SpringUrlRedirect.java:45:24:45:41 | redirectUrl : String | SpringUrlRedirect.java:48:30:48:40 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:45:24:45:41 | redirectUrl | user-provided value | diff --git a/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.java b/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.java index 1438b0a63a1..5124d8cd8c6 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.java +++ b/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.java @@ -34,6 +34,22 @@ public class SpringUrlRedirect { } @GetMapping("url5") + public String bad5(String redirectUrl) { + StringBuffer stringBuffer = new StringBuffer(); + stringBuffer.append("redirect:"); + stringBuffer.append(redirectUrl); + return stringBuffer.toString(); + } + + @GetMapping("url6") + public String bad6(String redirectUrl) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("redirect:"); + stringBuilder.append(redirectUrl); + return stringBuilder.toString(); + } + + @GetMapping("url7") public RedirectView good1(String redirectUrl) { RedirectView rv = new RedirectView(); if (redirectUrl.startsWith(VALID_REDIRECT)){ @@ -44,7 +60,7 @@ public class SpringUrlRedirect { return rv; } - @GetMapping("url6") + @GetMapping("url8") public ModelAndView good2(String token) { String url = "/edit?token=" + token; return new ModelAndView("redirect:" + url); From f378513ea39f33b15879653e489653d719633910 Mon Sep 17 00:00:00 2001 From: Robin Neatherway Date: Fri, 14 May 2021 11:19:20 +0100 Subject: [PATCH 37/74] Add lines-of-code tags This is a proposed method for advertising which queries are measuring the lines of code in a project in a more robust manner than inspecting the rule id. Note that the python "LinesOfUserCode" query should _not_ have this property, as otherwise the results of the two queries will be summed. --- cpp/ql/src/Summary/LinesOfCode.ql | 1 + csharp/ql/src/Metrics/Summaries/LinesOfCode.ql | 1 + java/ql/src/Metrics/Summaries/LinesOfCode.ql | 1 + javascript/ql/src/Summary/LinesOfCode.ql | 1 + python/ql/src/Summary/LinesOfCode.ql | 1 + 5 files changed, 5 insertions(+) diff --git a/cpp/ql/src/Summary/LinesOfCode.ql b/cpp/ql/src/Summary/LinesOfCode.ql index 3b2aa2ac4c9..2d816b349e8 100644 --- a/cpp/ql/src/Summary/LinesOfCode.ql +++ b/cpp/ql/src/Summary/LinesOfCode.ql @@ -4,6 +4,7 @@ * @description The total number of lines of C/C++ code across all files, including system headers, libraries, and auto-generated files. This is a useful metric of the size of a database. For all files that were seen during the build, this query counts the lines of code, excluding whitespace or comments. * @kind metric * @tags summary + * lines-of-code */ import cpp diff --git a/csharp/ql/src/Metrics/Summaries/LinesOfCode.ql b/csharp/ql/src/Metrics/Summaries/LinesOfCode.ql index 9156a5e4a7f..e93e3c7416f 100644 --- a/csharp/ql/src/Metrics/Summaries/LinesOfCode.ql +++ b/csharp/ql/src/Metrics/Summaries/LinesOfCode.ql @@ -4,6 +4,7 @@ * @description The total number of lines of code across all files. This is a useful metric of the size of a database. For all files that were seen during the build, this query counts the lines of code, excluding whitespace or comments. * @kind metric * @tags summary + * lines-of-code */ import csharp diff --git a/java/ql/src/Metrics/Summaries/LinesOfCode.ql b/java/ql/src/Metrics/Summaries/LinesOfCode.ql index d6db7c6ee6b..c622f8b08ba 100644 --- a/java/ql/src/Metrics/Summaries/LinesOfCode.ql +++ b/java/ql/src/Metrics/Summaries/LinesOfCode.ql @@ -6,6 +6,7 @@ * or comments. * @kind metric * @tags summary + * lines-of-code */ import java diff --git a/javascript/ql/src/Summary/LinesOfCode.ql b/javascript/ql/src/Summary/LinesOfCode.ql index 9f89e0e2163..cadf0a9cf8f 100644 --- a/javascript/ql/src/Summary/LinesOfCode.ql +++ b/javascript/ql/src/Summary/LinesOfCode.ql @@ -4,6 +4,7 @@ * @description The total number of lines of JavaScript or TypeScript code across all files checked into the repository, except in `node_modules`. This is a useful metric of the size of a database. For all files that were seen during extraction, this query counts the lines of code, excluding whitespace or comments. * @kind metric * @tags summary + * lines-of-code */ import javascript diff --git a/python/ql/src/Summary/LinesOfCode.ql b/python/ql/src/Summary/LinesOfCode.ql index d9bfc4f872c..ad0b77730de 100644 --- a/python/ql/src/Summary/LinesOfCode.ql +++ b/python/ql/src/Summary/LinesOfCode.ql @@ -5,6 +5,7 @@ * database. This query counts the lines of code, excluding whitespace or comments. * @kind metric * @tags summary + * lines-of-code * @id py/summary/lines-of-code */ From 5031b73f3509e4d1c1ce21586dd1262502177670 Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Fri, 14 May 2021 13:43:20 +0200 Subject: [PATCH 38/74] C++: Add barrier to cpp/uncontrolled-allocation-size that blocks flow when overflow isn't possible. --- .../CWE/CWE-190/TaintedAllocationSize.ql | 16 ++++++++++ .../TaintedAllocationSize.expected | 30 ------------------- .../semmle/TaintedAllocationSize/test.cpp | 6 ++-- 3 files changed, 19 insertions(+), 33 deletions(-) diff --git a/cpp/ql/src/Security/CWE/CWE-190/TaintedAllocationSize.ql b/cpp/ql/src/Security/CWE/CWE-190/TaintedAllocationSize.ql index cc2d52385c7..859d56ec2a0 100644 --- a/cpp/ql/src/Security/CWE/CWE-190/TaintedAllocationSize.ql +++ b/cpp/ql/src/Security/CWE/CWE-190/TaintedAllocationSize.ql @@ -12,6 +12,7 @@ */ import cpp +import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis import semmle.code.cpp.security.TaintTracking import TaintedWithPath @@ -27,6 +28,21 @@ predicate allocSink(Expr alloc, Expr tainted) { class TaintedAllocationSizeConfiguration extends TaintTrackingConfiguration { override predicate isSink(Element tainted) { allocSink(_, tainted) } + + override predicate isBarrier(Expr e) { + super.isBarrier(e) + or + // There can be two separate reasons for `convertedExprMightOverflow` not holding: + // 1. `e` really cannot overflow. + // 2. `e` isn't analyzable. + // If we didn't rule out case 2 we would place barriers on anything that isn't analyzable. + ( + e instanceof UnaryArithmeticOperation or + e instanceof BinaryArithmeticOperation or + e instanceof AssignArithmeticOperation + ) and + not convertedExprMightOverflow(e) + } } predicate taintedAllocSize( diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/TaintedAllocationSize.expected b/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/TaintedAllocationSize.expected index 25ff3162973..cdcf3aa6847 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/TaintedAllocationSize.expected +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/TaintedAllocationSize.expected @@ -53,10 +53,6 @@ edges | test.cpp:132:19:132:24 | call to getenv | test.cpp:134:10:134:27 | ... * ... | | test.cpp:132:19:132:32 | (const char *)... | test.cpp:134:10:134:27 | ... * ... | | test.cpp:132:19:132:32 | (const char *)... | test.cpp:134:10:134:27 | ... * ... | -| test.cpp:138:19:138:24 | call to getenv | test.cpp:142:11:142:28 | ... * ... | -| test.cpp:138:19:138:24 | call to getenv | test.cpp:142:11:142:28 | ... * ... | -| test.cpp:138:19:138:32 | (const char *)... | test.cpp:142:11:142:28 | ... * ... | -| test.cpp:138:19:138:32 | (const char *)... | test.cpp:142:11:142:28 | ... * ... | | test.cpp:201:9:201:42 | Store | test.cpp:231:9:231:24 | call to get_tainted_size | | test.cpp:201:9:201:42 | Store | test.cpp:231:9:231:24 | call to get_tainted_size | | test.cpp:201:14:201:19 | call to getenv | test.cpp:201:9:201:42 | Store | @@ -91,14 +87,6 @@ edges | test.cpp:295:18:295:21 | Chi | test.cpp:298:10:298:27 | ... * ... | | test.cpp:295:18:295:21 | Chi | test.cpp:298:10:298:27 | ... * ... | | test.cpp:295:18:295:21 | get_size output argument [array content] | test.cpp:295:18:295:21 | Chi | -| test.cpp:301:19:301:24 | call to getenv | test.cpp:305:11:305:28 | ... * ... | -| test.cpp:301:19:301:24 | call to getenv | test.cpp:305:11:305:28 | ... * ... | -| test.cpp:301:19:301:32 | (const char *)... | test.cpp:305:11:305:28 | ... * ... | -| test.cpp:301:19:301:32 | (const char *)... | test.cpp:305:11:305:28 | ... * ... | -| test.cpp:309:19:309:24 | call to getenv | test.cpp:314:10:314:27 | ... * ... | -| test.cpp:309:19:309:24 | call to getenv | test.cpp:314:10:314:27 | ... * ... | -| test.cpp:309:19:309:32 | (const char *)... | test.cpp:314:10:314:27 | ... * ... | -| test.cpp:309:19:309:32 | (const char *)... | test.cpp:314:10:314:27 | ... * ... | nodes | test.cpp:39:21:39:24 | argv | semmle.label | argv | | test.cpp:39:21:39:24 | argv | semmle.label | argv | @@ -149,11 +137,6 @@ nodes | test.cpp:134:10:134:27 | ... * ... | semmle.label | ... * ... | | test.cpp:134:10:134:27 | ... * ... | semmle.label | ... * ... | | test.cpp:134:10:134:27 | ... * ... | semmle.label | ... * ... | -| test.cpp:138:19:138:24 | call to getenv | semmle.label | call to getenv | -| test.cpp:138:19:138:32 | (const char *)... | semmle.label | (const char *)... | -| test.cpp:142:11:142:28 | ... * ... | semmle.label | ... * ... | -| test.cpp:142:11:142:28 | ... * ... | semmle.label | ... * ... | -| test.cpp:142:11:142:28 | ... * ... | semmle.label | ... * ... | | test.cpp:201:9:201:42 | Store | semmle.label | Store | | test.cpp:201:14:201:19 | call to getenv | semmle.label | call to getenv | | test.cpp:201:14:201:27 | (const char *)... | semmle.label | (const char *)... | @@ -196,16 +179,6 @@ nodes | test.cpp:298:10:298:27 | ... * ... | semmle.label | ... * ... | | test.cpp:298:10:298:27 | ... * ... | semmle.label | ... * ... | | test.cpp:298:10:298:27 | ... * ... | semmle.label | ... * ... | -| test.cpp:301:19:301:24 | call to getenv | semmle.label | call to getenv | -| test.cpp:301:19:301:32 | (const char *)... | semmle.label | (const char *)... | -| test.cpp:305:11:305:28 | ... * ... | semmle.label | ... * ... | -| test.cpp:305:11:305:28 | ... * ... | semmle.label | ... * ... | -| test.cpp:305:11:305:28 | ... * ... | semmle.label | ... * ... | -| test.cpp:309:19:309:24 | call to getenv | semmle.label | call to getenv | -| test.cpp:309:19:309:32 | (const char *)... | semmle.label | (const char *)... | -| test.cpp:314:10:314:27 | ... * ... | semmle.label | ... * ... | -| test.cpp:314:10:314:27 | ... * ... | semmle.label | ... * ... | -| test.cpp:314:10:314:27 | ... * ... | semmle.label | ... * ... | #select | test.cpp:42:31:42:36 | call to malloc | test.cpp:39:21:39:24 | argv | test.cpp:42:38:42:44 | tainted | This allocation size is derived from $@ and might overflow | test.cpp:39:21:39:24 | argv | user input (argv) | | test.cpp:43:31:43:36 | call to malloc | test.cpp:39:21:39:24 | argv | test.cpp:43:38:43:63 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:39:21:39:24 | argv | user input (argv) | @@ -216,7 +189,6 @@ nodes | test.cpp:79:9:79:29 | new[] | test.cpp:97:18:97:23 | buffer | test.cpp:79:18:79:28 | ... - ... | This allocation size is derived from $@ and might overflow | test.cpp:97:18:97:23 | buffer | user input (fread) | | test.cpp:127:17:127:22 | call to malloc | test.cpp:123:18:123:23 | call to getenv | test.cpp:127:24:127:41 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:123:18:123:23 | call to getenv | user input (getenv) | | test.cpp:134:3:134:8 | call to malloc | test.cpp:132:19:132:24 | call to getenv | test.cpp:134:10:134:27 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:132:19:132:24 | call to getenv | user input (getenv) | -| test.cpp:142:4:142:9 | call to malloc | test.cpp:138:19:138:24 | call to getenv | test.cpp:142:11:142:28 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:138:19:138:24 | call to getenv | user input (getenv) | | test.cpp:215:14:215:19 | call to malloc | test.cpp:227:24:227:29 | call to getenv | test.cpp:215:21:215:21 | s | This allocation size is derived from $@ and might overflow | test.cpp:227:24:227:29 | call to getenv | user input (getenv) | | test.cpp:221:14:221:19 | call to malloc | test.cpp:227:24:227:29 | call to getenv | test.cpp:221:21:221:21 | s | This allocation size is derived from $@ and might overflow | test.cpp:227:24:227:29 | call to getenv | user input (getenv) | | test.cpp:229:2:229:7 | call to malloc | test.cpp:227:24:227:29 | call to getenv | test.cpp:229:9:229:18 | local_size | This allocation size is derived from $@ and might overflow | test.cpp:227:24:227:29 | call to getenv | user input (getenv) | @@ -224,5 +196,3 @@ nodes | test.cpp:253:4:253:9 | call to malloc | test.cpp:249:20:249:25 | call to getenv | test.cpp:253:11:253:29 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:249:20:249:25 | call to getenv | user input (getenv) | | test.cpp:281:4:281:9 | call to malloc | test.cpp:241:18:241:23 | call to getenv | test.cpp:281:11:281:28 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:241:18:241:23 | call to getenv | user input (getenv) | | test.cpp:298:3:298:8 | call to malloc | test.cpp:241:18:241:23 | call to getenv | test.cpp:298:10:298:27 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:241:18:241:23 | call to getenv | user input (getenv) | -| test.cpp:305:4:305:9 | call to malloc | test.cpp:301:19:301:24 | call to getenv | test.cpp:305:11:305:28 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:301:19:301:24 | call to getenv | user input (getenv) | -| test.cpp:314:3:314:8 | call to malloc | test.cpp:309:19:309:24 | call to getenv | test.cpp:314:10:314:27 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:309:19:309:24 | call to getenv | user input (getenv) | diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/test.cpp b/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/test.cpp index 943bc3b1214..d00dc10a445 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/test.cpp +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/test.cpp @@ -139,7 +139,7 @@ void more_bounded_tests() { if (size > 0) { - malloc(size * sizeof(int)); // BAD + malloc(size * sizeof(int)); // GOOD (overflow not possible) } } @@ -302,7 +302,7 @@ void equality_cases() { if ((size == 50) || (size == 100)) { - malloc(size * sizeof(int)); // GOOD [FALSE POSITIVE] + malloc(size * sizeof(int)); // GOOD } } { @@ -311,6 +311,6 @@ void equality_cases() { if (size != 50 && size != 100) return; - malloc(size * sizeof(int)); // GOOD [FALSE POSITIVE] + malloc(size * sizeof(int)); // GOOD } } From 1497fba6f249c757d0a37d832c49d712e879af34 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Fri, 14 May 2021 11:43:49 +0000 Subject: [PATCH 39/74] Remove the isAdditionalTaintStep predicate --- .../Security/CWE/CWE-094/JythonInjection.ql | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.ql index 9991c0901dc..0f23edc69f4 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.ql @@ -102,16 +102,6 @@ class CodeInjectionConfiguration extends TaintTracking::Configuration { override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } override predicate isSink(DataFlow::Node sink) { sink instanceof CodeInjectionSink } - - override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) { - // @RequestBody MyQueryObj query; interpreter.exec(query.getInterpreterCode()); - exists(MethodAccess ma | - ma.getMethod().getDeclaringType().getASubtype*() instanceof SpringUntrustedDataType and - not ma.getMethod().getDeclaringType() instanceof TypeObject and - ma.getQualifier() = node1.asExpr() and - ma = node2.asExpr() - ) - } } from DataFlow::PathNode source, DataFlow::PathNode sink, CodeInjectionConfiguration conf From c1d41b31695edc664e2b3d8b1ca10cac3dda721b Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Fri, 14 May 2021 13:47:23 +0200 Subject: [PATCH 40/74] C++: Add false positive result from pointer-difference expressions. --- .../TaintedAllocationSize.expected | 14 ++++++++++++++ .../CWE-190/semmle/TaintedAllocationSize/test.cpp | 9 +++++++++ 2 files changed, 23 insertions(+) diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/TaintedAllocationSize.expected b/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/TaintedAllocationSize.expected index cdcf3aa6847..a96865cae60 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/TaintedAllocationSize.expected +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/TaintedAllocationSize.expected @@ -87,6 +87,12 @@ edges | test.cpp:295:18:295:21 | Chi | test.cpp:298:10:298:27 | ... * ... | | test.cpp:295:18:295:21 | Chi | test.cpp:298:10:298:27 | ... * ... | | test.cpp:295:18:295:21 | get_size output argument [array content] | test.cpp:295:18:295:21 | Chi | +| test.cpp:321:15:321:20 | call to getenv | test.cpp:324:9:324:14 | (size_t)... | +| test.cpp:321:15:321:20 | call to getenv | test.cpp:324:9:324:14 | (size_t)... | +| test.cpp:321:15:321:20 | call to getenv | test.cpp:324:9:324:14 | offset | +| test.cpp:321:15:321:20 | call to getenv | test.cpp:324:9:324:14 | offset | +| test.cpp:321:15:321:20 | call to getenv | test.cpp:324:9:324:14 | offset | +| test.cpp:321:15:321:20 | call to getenv | test.cpp:324:9:324:14 | offset | nodes | test.cpp:39:21:39:24 | argv | semmle.label | argv | | test.cpp:39:21:39:24 | argv | semmle.label | argv | @@ -179,6 +185,13 @@ nodes | test.cpp:298:10:298:27 | ... * ... | semmle.label | ... * ... | | test.cpp:298:10:298:27 | ... * ... | semmle.label | ... * ... | | test.cpp:298:10:298:27 | ... * ... | semmle.label | ... * ... | +| test.cpp:321:15:321:20 | call to getenv | semmle.label | call to getenv | +| test.cpp:321:15:321:20 | call to getenv | semmle.label | call to getenv | +| test.cpp:324:9:324:14 | (size_t)... | semmle.label | (size_t)... | +| test.cpp:324:9:324:14 | (size_t)... | semmle.label | (size_t)... | +| test.cpp:324:9:324:14 | offset | semmle.label | offset | +| test.cpp:324:9:324:14 | offset | semmle.label | offset | +| test.cpp:324:9:324:14 | offset | semmle.label | offset | #select | test.cpp:42:31:42:36 | call to malloc | test.cpp:39:21:39:24 | argv | test.cpp:42:38:42:44 | tainted | This allocation size is derived from $@ and might overflow | test.cpp:39:21:39:24 | argv | user input (argv) | | test.cpp:43:31:43:36 | call to malloc | test.cpp:39:21:39:24 | argv | test.cpp:43:38:43:63 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:39:21:39:24 | argv | user input (argv) | @@ -196,3 +209,4 @@ nodes | test.cpp:253:4:253:9 | call to malloc | test.cpp:249:20:249:25 | call to getenv | test.cpp:253:11:253:29 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:249:20:249:25 | call to getenv | user input (getenv) | | test.cpp:281:4:281:9 | call to malloc | test.cpp:241:18:241:23 | call to getenv | test.cpp:281:11:281:28 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:241:18:241:23 | call to getenv | user input (getenv) | | test.cpp:298:3:298:8 | call to malloc | test.cpp:241:18:241:23 | call to getenv | test.cpp:298:10:298:27 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:241:18:241:23 | call to getenv | user input (getenv) | +| test.cpp:324:2:324:7 | call to malloc | test.cpp:321:15:321:20 | call to getenv | test.cpp:324:9:324:14 | offset | This allocation size is derived from $@ and might overflow | test.cpp:321:15:321:20 | call to getenv | user input (getenv) | diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/test.cpp b/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/test.cpp index d00dc10a445..0d630ac99cb 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/test.cpp +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/test.cpp @@ -314,3 +314,12 @@ void equality_cases() { malloc(size * sizeof(int)); // GOOD } } + +char * strstr(char *, const char *); + +void ptr_diff_case() { + char* user = getenv("USER"); + char* admin_begin_pos = strstr(user, "ADMIN"); + int offset = admin_begin_pos ? user - admin_begin_pos : 0; + malloc(offset); // GOOD [FALSE POSITIVE] +} \ No newline at end of file From 2d0a56128d8f10acade04f99c73493671186d298 Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Fri, 14 May 2021 13:49:48 +0200 Subject: [PATCH 41/74] C++: Prevent flow out of pointer-difference expressions. --- .../CWE/CWE-190/TaintedAllocationSize.ql | 6 +++ .../TaintedAllocationSize.expected | 48 ------------------- .../semmle/TaintedAllocationSize/test.cpp | 4 +- 3 files changed, 8 insertions(+), 50 deletions(-) diff --git a/cpp/ql/src/Security/CWE/CWE-190/TaintedAllocationSize.ql b/cpp/ql/src/Security/CWE/CWE-190/TaintedAllocationSize.ql index 859d56ec2a0..75cac365a1a 100644 --- a/cpp/ql/src/Security/CWE/CWE-190/TaintedAllocationSize.ql +++ b/cpp/ql/src/Security/CWE/CWE-190/TaintedAllocationSize.ql @@ -42,6 +42,12 @@ class TaintedAllocationSizeConfiguration extends TaintTrackingConfiguration { e instanceof AssignArithmeticOperation ) and not convertedExprMightOverflow(e) + or + // Subtracting two pointers is either well-defined (and the result will likely be small), or + // terribly undefined and dangerous. Here, we assume that the programmer has ensured that the + // result is well-defined (i.e., the two pointers point to the same object), and thus the result + // will likely be small. + e = any(PointerDiffExpr diff).getAnOperand() } } diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/TaintedAllocationSize.expected b/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/TaintedAllocationSize.expected index a96865cae60..234efe2c35b 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/TaintedAllocationSize.expected +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/TaintedAllocationSize.expected @@ -27,24 +27,6 @@ edges | test.cpp:39:21:39:24 | argv | test.cpp:52:35:52:60 | ... * ... | | test.cpp:39:21:39:24 | argv | test.cpp:52:35:52:60 | ... * ... | | test.cpp:39:21:39:24 | argv | test.cpp:52:35:52:60 | ... * ... | -| test.cpp:75:25:75:29 | start | test.cpp:79:18:79:28 | ... - ... | -| test.cpp:75:25:75:29 | start | test.cpp:79:18:79:28 | ... - ... | -| test.cpp:75:38:75:40 | end | test.cpp:79:18:79:28 | ... - ... | -| test.cpp:75:38:75:40 | end | test.cpp:79:18:79:28 | ... - ... | -| test.cpp:97:18:97:23 | buffer | test.cpp:100:4:100:15 | buffer | -| test.cpp:97:18:97:23 | buffer | test.cpp:100:17:100:22 | buffer indirection | -| test.cpp:97:18:97:23 | buffer | test.cpp:101:4:101:15 | ... + ... | -| test.cpp:97:18:97:23 | buffer | test.cpp:101:4:101:15 | buffer | -| test.cpp:97:18:97:23 | fread output argument | test.cpp:100:4:100:15 | buffer | -| test.cpp:97:18:97:23 | fread output argument | test.cpp:100:17:100:22 | buffer indirection | -| test.cpp:97:18:97:23 | fread output argument | test.cpp:101:4:101:15 | ... + ... | -| test.cpp:97:18:97:23 | fread output argument | test.cpp:101:4:101:15 | buffer | -| test.cpp:100:4:100:15 | buffer | test.cpp:100:17:100:22 | processData1 output argument | -| test.cpp:100:17:100:22 | buffer indirection | test.cpp:100:17:100:22 | processData1 output argument | -| test.cpp:100:17:100:22 | processData1 output argument | test.cpp:101:4:101:15 | ... + ... | -| test.cpp:100:17:100:22 | processData1 output argument | test.cpp:101:4:101:15 | buffer | -| test.cpp:101:4:101:15 | ... + ... | test.cpp:75:38:75:40 | end | -| test.cpp:101:4:101:15 | buffer | test.cpp:75:25:75:29 | start | | test.cpp:123:18:123:23 | call to getenv | test.cpp:127:24:127:41 | ... * ... | | test.cpp:123:18:123:23 | call to getenv | test.cpp:127:24:127:41 | ... * ... | | test.cpp:123:18:123:31 | (const char *)... | test.cpp:127:24:127:41 | ... * ... | @@ -87,12 +69,6 @@ edges | test.cpp:295:18:295:21 | Chi | test.cpp:298:10:298:27 | ... * ... | | test.cpp:295:18:295:21 | Chi | test.cpp:298:10:298:27 | ... * ... | | test.cpp:295:18:295:21 | get_size output argument [array content] | test.cpp:295:18:295:21 | Chi | -| test.cpp:321:15:321:20 | call to getenv | test.cpp:324:9:324:14 | (size_t)... | -| test.cpp:321:15:321:20 | call to getenv | test.cpp:324:9:324:14 | (size_t)... | -| test.cpp:321:15:321:20 | call to getenv | test.cpp:324:9:324:14 | offset | -| test.cpp:321:15:321:20 | call to getenv | test.cpp:324:9:324:14 | offset | -| test.cpp:321:15:321:20 | call to getenv | test.cpp:324:9:324:14 | offset | -| test.cpp:321:15:321:20 | call to getenv | test.cpp:324:9:324:14 | offset | nodes | test.cpp:39:21:39:24 | argv | semmle.label | argv | | test.cpp:39:21:39:24 | argv | semmle.label | argv | @@ -118,21 +94,6 @@ nodes | test.cpp:52:35:52:60 | ... * ... | semmle.label | ... * ... | | test.cpp:52:35:52:60 | ... * ... | semmle.label | ... * ... | | test.cpp:52:35:52:60 | ... * ... | semmle.label | ... * ... | -| test.cpp:64:25:64:30 | *buffer | semmle.label | *buffer | -| test.cpp:64:25:64:30 | *buffer | semmle.label | *buffer | -| test.cpp:64:25:64:30 | buffer | semmle.label | buffer | -| test.cpp:75:25:75:29 | start | semmle.label | start | -| test.cpp:75:38:75:40 | end | semmle.label | end | -| test.cpp:79:18:79:28 | ... - ... | semmle.label | ... - ... | -| test.cpp:79:18:79:28 | ... - ... | semmle.label | ... - ... | -| test.cpp:79:18:79:28 | ... - ... | semmle.label | ... - ... | -| test.cpp:97:18:97:23 | buffer | semmle.label | buffer | -| test.cpp:97:18:97:23 | fread output argument | semmle.label | fread output argument | -| test.cpp:100:4:100:15 | buffer | semmle.label | buffer | -| test.cpp:100:17:100:22 | buffer indirection | semmle.label | buffer indirection | -| test.cpp:100:17:100:22 | processData1 output argument | semmle.label | processData1 output argument | -| test.cpp:101:4:101:15 | ... + ... | semmle.label | ... + ... | -| test.cpp:101:4:101:15 | buffer | semmle.label | buffer | | test.cpp:123:18:123:23 | call to getenv | semmle.label | call to getenv | | test.cpp:123:18:123:31 | (const char *)... | semmle.label | (const char *)... | | test.cpp:127:24:127:41 | ... * ... | semmle.label | ... * ... | @@ -185,13 +146,6 @@ nodes | test.cpp:298:10:298:27 | ... * ... | semmle.label | ... * ... | | test.cpp:298:10:298:27 | ... * ... | semmle.label | ... * ... | | test.cpp:298:10:298:27 | ... * ... | semmle.label | ... * ... | -| test.cpp:321:15:321:20 | call to getenv | semmle.label | call to getenv | -| test.cpp:321:15:321:20 | call to getenv | semmle.label | call to getenv | -| test.cpp:324:9:324:14 | (size_t)... | semmle.label | (size_t)... | -| test.cpp:324:9:324:14 | (size_t)... | semmle.label | (size_t)... | -| test.cpp:324:9:324:14 | offset | semmle.label | offset | -| test.cpp:324:9:324:14 | offset | semmle.label | offset | -| test.cpp:324:9:324:14 | offset | semmle.label | offset | #select | test.cpp:42:31:42:36 | call to malloc | test.cpp:39:21:39:24 | argv | test.cpp:42:38:42:44 | tainted | This allocation size is derived from $@ and might overflow | test.cpp:39:21:39:24 | argv | user input (argv) | | test.cpp:43:31:43:36 | call to malloc | test.cpp:39:21:39:24 | argv | test.cpp:43:38:43:63 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:39:21:39:24 | argv | user input (argv) | @@ -199,7 +153,6 @@ nodes | test.cpp:48:25:48:30 | call to malloc | test.cpp:39:21:39:24 | argv | test.cpp:48:32:48:35 | size | This allocation size is derived from $@ and might overflow | test.cpp:39:21:39:24 | argv | user input (argv) | | test.cpp:49:17:49:30 | new[] | test.cpp:39:21:39:24 | argv | test.cpp:49:26:49:29 | size | This allocation size is derived from $@ and might overflow | test.cpp:39:21:39:24 | argv | user input (argv) | | test.cpp:52:21:52:27 | call to realloc | test.cpp:39:21:39:24 | argv | test.cpp:52:35:52:60 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:39:21:39:24 | argv | user input (argv) | -| test.cpp:79:9:79:29 | new[] | test.cpp:97:18:97:23 | buffer | test.cpp:79:18:79:28 | ... - ... | This allocation size is derived from $@ and might overflow | test.cpp:97:18:97:23 | buffer | user input (fread) | | test.cpp:127:17:127:22 | call to malloc | test.cpp:123:18:123:23 | call to getenv | test.cpp:127:24:127:41 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:123:18:123:23 | call to getenv | user input (getenv) | | test.cpp:134:3:134:8 | call to malloc | test.cpp:132:19:132:24 | call to getenv | test.cpp:134:10:134:27 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:132:19:132:24 | call to getenv | user input (getenv) | | test.cpp:215:14:215:19 | call to malloc | test.cpp:227:24:227:29 | call to getenv | test.cpp:215:21:215:21 | s | This allocation size is derived from $@ and might overflow | test.cpp:227:24:227:29 | call to getenv | user input (getenv) | @@ -209,4 +162,3 @@ nodes | test.cpp:253:4:253:9 | call to malloc | test.cpp:249:20:249:25 | call to getenv | test.cpp:253:11:253:29 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:249:20:249:25 | call to getenv | user input (getenv) | | test.cpp:281:4:281:9 | call to malloc | test.cpp:241:18:241:23 | call to getenv | test.cpp:281:11:281:28 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:241:18:241:23 | call to getenv | user input (getenv) | | test.cpp:298:3:298:8 | call to malloc | test.cpp:241:18:241:23 | call to getenv | test.cpp:298:10:298:27 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:241:18:241:23 | call to getenv | user input (getenv) | -| test.cpp:324:2:324:7 | call to malloc | test.cpp:321:15:321:20 | call to getenv | test.cpp:324:9:324:14 | offset | This allocation size is derived from $@ and might overflow | test.cpp:321:15:321:20 | call to getenv | user input (getenv) | diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/test.cpp b/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/test.cpp index 0d630ac99cb..57eb5b91a07 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/test.cpp +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/test.cpp @@ -76,7 +76,7 @@ void processData2(char *start, char *end) { char *copy; - copy = new char[end - start]; // GOOD [FALSE POSITIVE] + copy = new char[end - start]; // GOOD // ... @@ -321,5 +321,5 @@ void ptr_diff_case() { char* user = getenv("USER"); char* admin_begin_pos = strstr(user, "ADMIN"); int offset = admin_begin_pos ? user - admin_begin_pos : 0; - malloc(offset); // GOOD [FALSE POSITIVE] + malloc(offset); // GOOD } \ No newline at end of file From 58dde68b10d74d6d7ed4b7d206c1fe319b4e15ac Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Fri, 14 May 2021 14:16:00 +0200 Subject: [PATCH 42/74] C++: Add change-note. --- cpp/change-notes/2021-05-14-uncontrolled-allocation-size.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 cpp/change-notes/2021-05-14-uncontrolled-allocation-size.md diff --git a/cpp/change-notes/2021-05-14-uncontrolled-allocation-size.md b/cpp/change-notes/2021-05-14-uncontrolled-allocation-size.md new file mode 100644 index 00000000000..6f0c9d6fa98 --- /dev/null +++ b/cpp/change-notes/2021-05-14-uncontrolled-allocation-size.md @@ -0,0 +1,2 @@ +lgtm +* The "Tainted allocation size" query (cpp/uncontrolled-allocation-size) has been improved to produce fewer false positives. From 4cf695b5ab0a8a50e5143f298e8e1ff3e7ec3c30 Mon Sep 17 00:00:00 2001 From: Ethan Palm <56270045+ethanpalm@users.noreply.github.com> Date: Fri, 14 May 2021 10:00:17 -0400 Subject: [PATCH 43/74] specify ``--command`` option Co-authored-by: intrigus-lgtm <60750685+intrigus-lgtm@users.noreply.github.com> --- docs/codeql/codeql-cli/creating-codeql-databases.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/codeql/codeql-cli/creating-codeql-databases.rst b/docs/codeql/codeql-cli/creating-codeql-databases.rst index a41727e1956..8810e3deb9d 100644 --- a/docs/codeql/codeql-cli/creating-codeql-databases.rst +++ b/docs/codeql/codeql-cli/creating-codeql-databases.rst @@ -172,7 +172,7 @@ build steps, you may need to explicitly define each step in the command line. The Go autobuilder attempts to automatically detect Go code in a repository, and only runs build scripts in an attempt to fetch dependencies. To force CodeQL to use your build script, set the environment variable - `CODEQL_EXTRACTOR_GO_BUILD_TRACING=on` or pass a command. + `CODEQL_EXTRACTOR_GO_BUILD_TRACING=on` or use the ``--command`` option. Specifying build commands ~~~~~~~~~~~~~~~~~~~~~~~~~ From 6dd30ee5e2a03fdffc8b7cb8bf23689bce2813b7 Mon Sep 17 00:00:00 2001 From: Ethan Palm <56270045+ethanpalm@users.noreply.github.com> Date: Fri, 14 May 2021 14:00:33 -0400 Subject: [PATCH 44/74] clarify options for tracing Co-authored-by: Chris Smowton --- docs/codeql/codeql-cli/creating-codeql-databases.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/codeql/codeql-cli/creating-codeql-databases.rst b/docs/codeql/codeql-cli/creating-codeql-databases.rst index 8810e3deb9d..9cdd02f78fc 100644 --- a/docs/codeql/codeql-cli/creating-codeql-databases.rst +++ b/docs/codeql/codeql-cli/creating-codeql-databases.rst @@ -171,8 +171,9 @@ build steps, you may need to explicitly define each step in the command line. The Go autobuilder attempts to automatically detect Go code in a repository, and only runs build scripts in an attempt to fetch dependencies. To force - CodeQL to use your build script, set the environment variable - `CODEQL_EXTRACTOR_GO_BUILD_TRACING=on` or use the ``--command`` option. + CodeQL to limit extraction to the files compiled by your build script, set the environment variable + `CODEQL_EXTRACTOR_GO_BUILD_TRACING=on` or use the ``--command`` option to specify a + build command. Specifying build commands ~~~~~~~~~~~~~~~~~~~~~~~~~ From 0e99d5e379041cdffcaab2da4e57218648d1fd96 Mon Sep 17 00:00:00 2001 From: Ethan P <56270045+ethanpalm@users.noreply.github.com> Date: Fri, 14 May 2021 14:05:55 -0400 Subject: [PATCH 45/74] Add examples of both tracing mechanisms --- docs/codeql/codeql-cli/creating-codeql-databases.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/codeql/codeql-cli/creating-codeql-databases.rst b/docs/codeql/codeql-cli/creating-codeql-databases.rst index 9cdd02f78fc..8658da9a1d4 100644 --- a/docs/codeql/codeql-cli/creating-codeql-databases.rst +++ b/docs/codeql/codeql-cli/creating-codeql-databases.rst @@ -202,9 +202,12 @@ commands that you can specify for compiled languages. codeql database create csharp-database --language=csharp --command='dotnet build /p:UseSharedCompilation=false /t:rebuild' +- Go project built using the ``COEQL_EXTRACTOR_GO_BUILD_TRACING=on`` environment variable:: + CODEQL_EXTRACTOR_GO_BUILD_TRACING=on codeql database create go-database --language=go + - Go project built using a custom build script:: - CODEQL_EXTRACTOR_GO_BUILD_TRACING=on codeql database create go-database --language=go --command='./scripts/build.sh' + codeql database create go-database --language=go --command='./scripts/build.sh' - Java project built using Gradle:: From 58c746e42b174c1ee4465569e832c7a7cb735d03 Mon Sep 17 00:00:00 2001 From: Ethan P <56270045+ethanpalm@users.noreply.github.com> Date: Fri, 14 May 2021 14:09:07 -0400 Subject: [PATCH 46/74] fix formatting --- docs/codeql/codeql-cli/creating-codeql-databases.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/codeql/codeql-cli/creating-codeql-databases.rst b/docs/codeql/codeql-cli/creating-codeql-databases.rst index 8658da9a1d4..1552a077e24 100644 --- a/docs/codeql/codeql-cli/creating-codeql-databases.rst +++ b/docs/codeql/codeql-cli/creating-codeql-databases.rst @@ -203,6 +203,7 @@ commands that you can specify for compiled languages. codeql database create csharp-database --language=csharp --command='dotnet build /p:UseSharedCompilation=false /t:rebuild' - Go project built using the ``COEQL_EXTRACTOR_GO_BUILD_TRACING=on`` environment variable:: + CODEQL_EXTRACTOR_GO_BUILD_TRACING=on codeql database create go-database --language=go - Go project built using a custom build script:: From 31091c66c19fdb401967decc596621fa68db8d0a Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Mon, 17 May 2021 08:06:06 +0200 Subject: [PATCH 47/74] C++: Add a test containing a guarded long. --- .../TaintedAllocationSize.expected | 332 +++++++++--------- .../semmle/TaintedAllocationSize/test.cpp | 10 + 2 files changed, 181 insertions(+), 161 deletions(-) diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/TaintedAllocationSize.expected b/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/TaintedAllocationSize.expected index 234efe2c35b..752e9165c07 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/TaintedAllocationSize.expected +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/TaintedAllocationSize.expected @@ -1,164 +1,174 @@ edges -| test.cpp:39:21:39:24 | argv | test.cpp:42:38:42:44 | (size_t)... | -| test.cpp:39:21:39:24 | argv | test.cpp:42:38:42:44 | (size_t)... | -| test.cpp:39:21:39:24 | argv | test.cpp:42:38:42:44 | tainted | -| test.cpp:39:21:39:24 | argv | test.cpp:42:38:42:44 | tainted | -| test.cpp:39:21:39:24 | argv | test.cpp:42:38:42:44 | tainted | -| test.cpp:39:21:39:24 | argv | test.cpp:42:38:42:44 | tainted | -| test.cpp:39:21:39:24 | argv | test.cpp:43:38:43:63 | ... * ... | -| test.cpp:39:21:39:24 | argv | test.cpp:43:38:43:63 | ... * ... | -| test.cpp:39:21:39:24 | argv | test.cpp:43:38:43:63 | ... * ... | -| test.cpp:39:21:39:24 | argv | test.cpp:43:38:43:63 | ... * ... | -| test.cpp:39:21:39:24 | argv | test.cpp:45:38:45:63 | ... + ... | -| test.cpp:39:21:39:24 | argv | test.cpp:45:38:45:63 | ... + ... | -| test.cpp:39:21:39:24 | argv | test.cpp:45:38:45:63 | ... + ... | -| test.cpp:39:21:39:24 | argv | test.cpp:45:38:45:63 | ... + ... | -| test.cpp:39:21:39:24 | argv | test.cpp:48:32:48:35 | (size_t)... | -| test.cpp:39:21:39:24 | argv | test.cpp:48:32:48:35 | (size_t)... | -| test.cpp:39:21:39:24 | argv | test.cpp:48:32:48:35 | size | -| test.cpp:39:21:39:24 | argv | test.cpp:48:32:48:35 | size | -| test.cpp:39:21:39:24 | argv | test.cpp:48:32:48:35 | size | -| test.cpp:39:21:39:24 | argv | test.cpp:48:32:48:35 | size | -| test.cpp:39:21:39:24 | argv | test.cpp:49:26:49:29 | size | -| test.cpp:39:21:39:24 | argv | test.cpp:49:26:49:29 | size | -| test.cpp:39:21:39:24 | argv | test.cpp:49:26:49:29 | size | -| test.cpp:39:21:39:24 | argv | test.cpp:49:26:49:29 | size | -| test.cpp:39:21:39:24 | argv | test.cpp:52:35:52:60 | ... * ... | -| test.cpp:39:21:39:24 | argv | test.cpp:52:35:52:60 | ... * ... | -| test.cpp:39:21:39:24 | argv | test.cpp:52:35:52:60 | ... * ... | -| test.cpp:39:21:39:24 | argv | test.cpp:52:35:52:60 | ... * ... | -| test.cpp:123:18:123:23 | call to getenv | test.cpp:127:24:127:41 | ... * ... | -| test.cpp:123:18:123:23 | call to getenv | test.cpp:127:24:127:41 | ... * ... | -| test.cpp:123:18:123:31 | (const char *)... | test.cpp:127:24:127:41 | ... * ... | -| test.cpp:123:18:123:31 | (const char *)... | test.cpp:127:24:127:41 | ... * ... | -| test.cpp:132:19:132:24 | call to getenv | test.cpp:134:10:134:27 | ... * ... | -| test.cpp:132:19:132:24 | call to getenv | test.cpp:134:10:134:27 | ... * ... | -| test.cpp:132:19:132:32 | (const char *)... | test.cpp:134:10:134:27 | ... * ... | -| test.cpp:132:19:132:32 | (const char *)... | test.cpp:134:10:134:27 | ... * ... | -| test.cpp:201:9:201:42 | Store | test.cpp:231:9:231:24 | call to get_tainted_size | -| test.cpp:201:9:201:42 | Store | test.cpp:231:9:231:24 | call to get_tainted_size | -| test.cpp:201:14:201:19 | call to getenv | test.cpp:201:9:201:42 | Store | -| test.cpp:201:14:201:27 | (const char *)... | test.cpp:201:9:201:42 | Store | -| test.cpp:214:23:214:23 | s | test.cpp:215:21:215:21 | s | -| test.cpp:214:23:214:23 | s | test.cpp:215:21:215:21 | s | -| test.cpp:220:21:220:21 | s | test.cpp:221:21:221:21 | s | -| test.cpp:220:21:220:21 | s | test.cpp:221:21:221:21 | s | -| test.cpp:227:24:227:29 | call to getenv | test.cpp:229:9:229:18 | (size_t)... | -| test.cpp:227:24:227:29 | call to getenv | test.cpp:229:9:229:18 | local_size | -| test.cpp:227:24:227:29 | call to getenv | test.cpp:229:9:229:18 | local_size | -| test.cpp:227:24:227:29 | call to getenv | test.cpp:235:2:235:9 | local_size | -| test.cpp:227:24:227:29 | call to getenv | test.cpp:237:2:237:8 | local_size | -| test.cpp:227:24:227:37 | (const char *)... | test.cpp:229:9:229:18 | (size_t)... | -| test.cpp:227:24:227:37 | (const char *)... | test.cpp:229:9:229:18 | local_size | -| test.cpp:227:24:227:37 | (const char *)... | test.cpp:229:9:229:18 | local_size | -| test.cpp:227:24:227:37 | (const char *)... | test.cpp:235:2:235:9 | local_size | -| test.cpp:227:24:227:37 | (const char *)... | test.cpp:237:2:237:8 | local_size | -| test.cpp:235:2:235:9 | local_size | test.cpp:214:23:214:23 | s | -| test.cpp:237:2:237:8 | local_size | test.cpp:220:21:220:21 | s | -| test.cpp:241:2:241:32 | Chi [array content] | test.cpp:279:17:279:20 | get_size output argument [array content] | -| test.cpp:241:2:241:32 | Chi [array content] | test.cpp:295:18:295:21 | get_size output argument [array content] | -| test.cpp:241:18:241:23 | call to getenv | test.cpp:241:2:241:32 | Chi [array content] | -| test.cpp:241:18:241:31 | (const char *)... | test.cpp:241:2:241:32 | Chi [array content] | -| test.cpp:249:20:249:25 | call to getenv | test.cpp:253:11:253:29 | ... * ... | -| test.cpp:249:20:249:25 | call to getenv | test.cpp:253:11:253:29 | ... * ... | -| test.cpp:249:20:249:33 | (const char *)... | test.cpp:253:11:253:29 | ... * ... | -| test.cpp:249:20:249:33 | (const char *)... | test.cpp:253:11:253:29 | ... * ... | -| test.cpp:279:17:279:20 | Chi | test.cpp:281:11:281:28 | ... * ... | -| test.cpp:279:17:279:20 | Chi | test.cpp:281:11:281:28 | ... * ... | -| test.cpp:279:17:279:20 | get_size output argument [array content] | test.cpp:279:17:279:20 | Chi | -| test.cpp:295:18:295:21 | Chi | test.cpp:298:10:298:27 | ... * ... | -| test.cpp:295:18:295:21 | Chi | test.cpp:298:10:298:27 | ... * ... | -| test.cpp:295:18:295:21 | get_size output argument [array content] | test.cpp:295:18:295:21 | Chi | +| test.cpp:40:21:40:24 | argv | test.cpp:43:38:43:44 | (size_t)... | +| test.cpp:40:21:40:24 | argv | test.cpp:43:38:43:44 | (size_t)... | +| test.cpp:40:21:40:24 | argv | test.cpp:43:38:43:44 | tainted | +| test.cpp:40:21:40:24 | argv | test.cpp:43:38:43:44 | tainted | +| test.cpp:40:21:40:24 | argv | test.cpp:43:38:43:44 | tainted | +| test.cpp:40:21:40:24 | argv | test.cpp:43:38:43:44 | tainted | +| test.cpp:40:21:40:24 | argv | test.cpp:44:38:44:63 | ... * ... | +| test.cpp:40:21:40:24 | argv | test.cpp:44:38:44:63 | ... * ... | +| test.cpp:40:21:40:24 | argv | test.cpp:44:38:44:63 | ... * ... | +| test.cpp:40:21:40:24 | argv | test.cpp:44:38:44:63 | ... * ... | +| test.cpp:40:21:40:24 | argv | test.cpp:46:38:46:63 | ... + ... | +| test.cpp:40:21:40:24 | argv | test.cpp:46:38:46:63 | ... + ... | +| test.cpp:40:21:40:24 | argv | test.cpp:46:38:46:63 | ... + ... | +| test.cpp:40:21:40:24 | argv | test.cpp:46:38:46:63 | ... + ... | +| test.cpp:40:21:40:24 | argv | test.cpp:49:32:49:35 | (size_t)... | +| test.cpp:40:21:40:24 | argv | test.cpp:49:32:49:35 | (size_t)... | +| test.cpp:40:21:40:24 | argv | test.cpp:49:32:49:35 | size | +| test.cpp:40:21:40:24 | argv | test.cpp:49:32:49:35 | size | +| test.cpp:40:21:40:24 | argv | test.cpp:49:32:49:35 | size | +| test.cpp:40:21:40:24 | argv | test.cpp:49:32:49:35 | size | +| test.cpp:40:21:40:24 | argv | test.cpp:50:26:50:29 | size | +| test.cpp:40:21:40:24 | argv | test.cpp:50:26:50:29 | size | +| test.cpp:40:21:40:24 | argv | test.cpp:50:26:50:29 | size | +| test.cpp:40:21:40:24 | argv | test.cpp:50:26:50:29 | size | +| test.cpp:40:21:40:24 | argv | test.cpp:53:35:53:60 | ... * ... | +| test.cpp:40:21:40:24 | argv | test.cpp:53:35:53:60 | ... * ... | +| test.cpp:40:21:40:24 | argv | test.cpp:53:35:53:60 | ... * ... | +| test.cpp:40:21:40:24 | argv | test.cpp:53:35:53:60 | ... * ... | +| test.cpp:124:18:124:23 | call to getenv | test.cpp:128:24:128:41 | ... * ... | +| test.cpp:124:18:124:23 | call to getenv | test.cpp:128:24:128:41 | ... * ... | +| test.cpp:124:18:124:31 | (const char *)... | test.cpp:128:24:128:41 | ... * ... | +| test.cpp:124:18:124:31 | (const char *)... | test.cpp:128:24:128:41 | ... * ... | +| test.cpp:133:19:133:24 | call to getenv | test.cpp:135:10:135:27 | ... * ... | +| test.cpp:133:19:133:24 | call to getenv | test.cpp:135:10:135:27 | ... * ... | +| test.cpp:133:19:133:32 | (const char *)... | test.cpp:135:10:135:27 | ... * ... | +| test.cpp:133:19:133:32 | (const char *)... | test.cpp:135:10:135:27 | ... * ... | +| test.cpp:148:20:148:25 | call to getenv | test.cpp:152:11:152:28 | ... * ... | +| test.cpp:148:20:148:25 | call to getenv | test.cpp:152:11:152:28 | ... * ... | +| test.cpp:148:20:148:33 | (const char *)... | test.cpp:152:11:152:28 | ... * ... | +| test.cpp:148:20:148:33 | (const char *)... | test.cpp:152:11:152:28 | ... * ... | +| test.cpp:211:9:211:42 | Store | test.cpp:241:9:241:24 | call to get_tainted_size | +| test.cpp:211:9:211:42 | Store | test.cpp:241:9:241:24 | call to get_tainted_size | +| test.cpp:211:14:211:19 | call to getenv | test.cpp:211:9:211:42 | Store | +| test.cpp:211:14:211:27 | (const char *)... | test.cpp:211:9:211:42 | Store | +| test.cpp:224:23:224:23 | s | test.cpp:225:21:225:21 | s | +| test.cpp:224:23:224:23 | s | test.cpp:225:21:225:21 | s | +| test.cpp:230:21:230:21 | s | test.cpp:231:21:231:21 | s | +| test.cpp:230:21:230:21 | s | test.cpp:231:21:231:21 | s | +| test.cpp:237:24:237:29 | call to getenv | test.cpp:239:9:239:18 | (size_t)... | +| test.cpp:237:24:237:29 | call to getenv | test.cpp:239:9:239:18 | local_size | +| test.cpp:237:24:237:29 | call to getenv | test.cpp:239:9:239:18 | local_size | +| test.cpp:237:24:237:29 | call to getenv | test.cpp:245:2:245:9 | local_size | +| test.cpp:237:24:237:29 | call to getenv | test.cpp:247:2:247:8 | local_size | +| test.cpp:237:24:237:37 | (const char *)... | test.cpp:239:9:239:18 | (size_t)... | +| test.cpp:237:24:237:37 | (const char *)... | test.cpp:239:9:239:18 | local_size | +| test.cpp:237:24:237:37 | (const char *)... | test.cpp:239:9:239:18 | local_size | +| test.cpp:237:24:237:37 | (const char *)... | test.cpp:245:2:245:9 | local_size | +| test.cpp:237:24:237:37 | (const char *)... | test.cpp:247:2:247:8 | local_size | +| test.cpp:245:2:245:9 | local_size | test.cpp:224:23:224:23 | s | +| test.cpp:247:2:247:8 | local_size | test.cpp:230:21:230:21 | s | +| test.cpp:251:2:251:32 | Chi [array content] | test.cpp:289:17:289:20 | get_size output argument [array content] | +| test.cpp:251:2:251:32 | Chi [array content] | test.cpp:305:18:305:21 | get_size output argument [array content] | +| test.cpp:251:18:251:23 | call to getenv | test.cpp:251:2:251:32 | Chi [array content] | +| test.cpp:251:18:251:31 | (const char *)... | test.cpp:251:2:251:32 | Chi [array content] | +| test.cpp:259:20:259:25 | call to getenv | test.cpp:263:11:263:29 | ... * ... | +| test.cpp:259:20:259:25 | call to getenv | test.cpp:263:11:263:29 | ... * ... | +| test.cpp:259:20:259:33 | (const char *)... | test.cpp:263:11:263:29 | ... * ... | +| test.cpp:259:20:259:33 | (const char *)... | test.cpp:263:11:263:29 | ... * ... | +| test.cpp:289:17:289:20 | Chi | test.cpp:291:11:291:28 | ... * ... | +| test.cpp:289:17:289:20 | Chi | test.cpp:291:11:291:28 | ... * ... | +| test.cpp:289:17:289:20 | get_size output argument [array content] | test.cpp:289:17:289:20 | Chi | +| test.cpp:305:18:305:21 | Chi | test.cpp:308:10:308:27 | ... * ... | +| test.cpp:305:18:305:21 | Chi | test.cpp:308:10:308:27 | ... * ... | +| test.cpp:305:18:305:21 | get_size output argument [array content] | test.cpp:305:18:305:21 | Chi | nodes -| test.cpp:39:21:39:24 | argv | semmle.label | argv | -| test.cpp:39:21:39:24 | argv | semmle.label | argv | -| test.cpp:42:38:42:44 | (size_t)... | semmle.label | (size_t)... | -| test.cpp:42:38:42:44 | (size_t)... | semmle.label | (size_t)... | -| test.cpp:42:38:42:44 | tainted | semmle.label | tainted | -| test.cpp:42:38:42:44 | tainted | semmle.label | tainted | -| test.cpp:42:38:42:44 | tainted | semmle.label | tainted | -| test.cpp:43:38:43:63 | ... * ... | semmle.label | ... * ... | -| test.cpp:43:38:43:63 | ... * ... | semmle.label | ... * ... | -| test.cpp:43:38:43:63 | ... * ... | semmle.label | ... * ... | -| test.cpp:45:38:45:63 | ... + ... | semmle.label | ... + ... | -| test.cpp:45:38:45:63 | ... + ... | semmle.label | ... + ... | -| test.cpp:45:38:45:63 | ... + ... | semmle.label | ... + ... | -| test.cpp:48:32:48:35 | (size_t)... | semmle.label | (size_t)... | -| test.cpp:48:32:48:35 | (size_t)... | semmle.label | (size_t)... | -| test.cpp:48:32:48:35 | size | semmle.label | size | -| test.cpp:48:32:48:35 | size | semmle.label | size | -| test.cpp:48:32:48:35 | size | semmle.label | size | -| test.cpp:49:26:49:29 | size | semmle.label | size | -| test.cpp:49:26:49:29 | size | semmle.label | size | -| test.cpp:49:26:49:29 | size | semmle.label | size | -| test.cpp:52:35:52:60 | ... * ... | semmle.label | ... * ... | -| test.cpp:52:35:52:60 | ... * ... | semmle.label | ... * ... | -| test.cpp:52:35:52:60 | ... * ... | semmle.label | ... * ... | -| test.cpp:123:18:123:23 | call to getenv | semmle.label | call to getenv | -| test.cpp:123:18:123:31 | (const char *)... | semmle.label | (const char *)... | -| test.cpp:127:24:127:41 | ... * ... | semmle.label | ... * ... | -| test.cpp:127:24:127:41 | ... * ... | semmle.label | ... * ... | -| test.cpp:127:24:127:41 | ... * ... | semmle.label | ... * ... | -| test.cpp:132:19:132:24 | call to getenv | semmle.label | call to getenv | -| test.cpp:132:19:132:32 | (const char *)... | semmle.label | (const char *)... | -| test.cpp:134:10:134:27 | ... * ... | semmle.label | ... * ... | -| test.cpp:134:10:134:27 | ... * ... | semmle.label | ... * ... | -| test.cpp:134:10:134:27 | ... * ... | semmle.label | ... * ... | -| test.cpp:201:9:201:42 | Store | semmle.label | Store | -| test.cpp:201:14:201:19 | call to getenv | semmle.label | call to getenv | -| test.cpp:201:14:201:27 | (const char *)... | semmle.label | (const char *)... | -| test.cpp:214:23:214:23 | s | semmle.label | s | -| test.cpp:215:21:215:21 | s | semmle.label | s | -| test.cpp:215:21:215:21 | s | semmle.label | s | -| test.cpp:215:21:215:21 | s | semmle.label | s | -| test.cpp:220:21:220:21 | s | semmle.label | s | -| test.cpp:221:21:221:21 | s | semmle.label | s | -| test.cpp:221:21:221:21 | s | semmle.label | s | -| test.cpp:221:21:221:21 | s | semmle.label | s | -| test.cpp:227:24:227:29 | call to getenv | semmle.label | call to getenv | -| test.cpp:227:24:227:37 | (const char *)... | semmle.label | (const char *)... | -| test.cpp:229:9:229:18 | (size_t)... | semmle.label | (size_t)... | -| test.cpp:229:9:229:18 | (size_t)... | semmle.label | (size_t)... | -| test.cpp:229:9:229:18 | local_size | semmle.label | local_size | -| test.cpp:229:9:229:18 | local_size | semmle.label | local_size | -| test.cpp:229:9:229:18 | local_size | semmle.label | local_size | -| test.cpp:231:9:231:24 | call to get_tainted_size | semmle.label | call to get_tainted_size | -| test.cpp:231:9:231:24 | call to get_tainted_size | semmle.label | call to get_tainted_size | -| test.cpp:231:9:231:24 | call to get_tainted_size | semmle.label | call to get_tainted_size | -| test.cpp:235:2:235:9 | local_size | semmle.label | local_size | -| test.cpp:237:2:237:8 | local_size | semmle.label | local_size | -| test.cpp:241:2:241:32 | Chi [array content] | semmle.label | Chi [array content] | -| test.cpp:241:2:241:32 | ChiPartial | semmle.label | ChiPartial | -| test.cpp:241:18:241:23 | call to getenv | semmle.label | call to getenv | -| test.cpp:241:18:241:31 | (const char *)... | semmle.label | (const char *)... | -| test.cpp:249:20:249:25 | call to getenv | semmle.label | call to getenv | -| test.cpp:249:20:249:33 | (const char *)... | semmle.label | (const char *)... | -| test.cpp:253:11:253:29 | ... * ... | semmle.label | ... * ... | -| test.cpp:253:11:253:29 | ... * ... | semmle.label | ... * ... | -| test.cpp:253:11:253:29 | ... * ... | semmle.label | ... * ... | -| test.cpp:279:17:279:20 | Chi | semmle.label | Chi | -| test.cpp:279:17:279:20 | get_size output argument [array content] | semmle.label | get_size output argument [array content] | -| test.cpp:281:11:281:28 | ... * ... | semmle.label | ... * ... | -| test.cpp:281:11:281:28 | ... * ... | semmle.label | ... * ... | -| test.cpp:281:11:281:28 | ... * ... | semmle.label | ... * ... | -| test.cpp:295:18:295:21 | Chi | semmle.label | Chi | -| test.cpp:295:18:295:21 | get_size output argument [array content] | semmle.label | get_size output argument [array content] | -| test.cpp:298:10:298:27 | ... * ... | semmle.label | ... * ... | -| test.cpp:298:10:298:27 | ... * ... | semmle.label | ... * ... | -| test.cpp:298:10:298:27 | ... * ... | semmle.label | ... * ... | +| test.cpp:40:21:40:24 | argv | semmle.label | argv | +| test.cpp:40:21:40:24 | argv | semmle.label | argv | +| test.cpp:43:38:43:44 | (size_t)... | semmle.label | (size_t)... | +| test.cpp:43:38:43:44 | (size_t)... | semmle.label | (size_t)... | +| test.cpp:43:38:43:44 | tainted | semmle.label | tainted | +| test.cpp:43:38:43:44 | tainted | semmle.label | tainted | +| test.cpp:43:38:43:44 | tainted | semmle.label | tainted | +| test.cpp:44:38:44:63 | ... * ... | semmle.label | ... * ... | +| test.cpp:44:38:44:63 | ... * ... | semmle.label | ... * ... | +| test.cpp:44:38:44:63 | ... * ... | semmle.label | ... * ... | +| test.cpp:46:38:46:63 | ... + ... | semmle.label | ... + ... | +| test.cpp:46:38:46:63 | ... + ... | semmle.label | ... + ... | +| test.cpp:46:38:46:63 | ... + ... | semmle.label | ... + ... | +| test.cpp:49:32:49:35 | (size_t)... | semmle.label | (size_t)... | +| test.cpp:49:32:49:35 | (size_t)... | semmle.label | (size_t)... | +| test.cpp:49:32:49:35 | size | semmle.label | size | +| test.cpp:49:32:49:35 | size | semmle.label | size | +| test.cpp:49:32:49:35 | size | semmle.label | size | +| test.cpp:50:26:50:29 | size | semmle.label | size | +| test.cpp:50:26:50:29 | size | semmle.label | size | +| test.cpp:50:26:50:29 | size | semmle.label | size | +| test.cpp:53:35:53:60 | ... * ... | semmle.label | ... * ... | +| test.cpp:53:35:53:60 | ... * ... | semmle.label | ... * ... | +| test.cpp:53:35:53:60 | ... * ... | semmle.label | ... * ... | +| test.cpp:124:18:124:23 | call to getenv | semmle.label | call to getenv | +| test.cpp:124:18:124:31 | (const char *)... | semmle.label | (const char *)... | +| test.cpp:128:24:128:41 | ... * ... | semmle.label | ... * ... | +| test.cpp:128:24:128:41 | ... * ... | semmle.label | ... * ... | +| test.cpp:128:24:128:41 | ... * ... | semmle.label | ... * ... | +| test.cpp:133:19:133:24 | call to getenv | semmle.label | call to getenv | +| test.cpp:133:19:133:32 | (const char *)... | semmle.label | (const char *)... | +| test.cpp:135:10:135:27 | ... * ... | semmle.label | ... * ... | +| test.cpp:135:10:135:27 | ... * ... | semmle.label | ... * ... | +| test.cpp:135:10:135:27 | ... * ... | semmle.label | ... * ... | +| test.cpp:148:20:148:25 | call to getenv | semmle.label | call to getenv | +| test.cpp:148:20:148:33 | (const char *)... | semmle.label | (const char *)... | +| test.cpp:152:11:152:28 | ... * ... | semmle.label | ... * ... | +| test.cpp:152:11:152:28 | ... * ... | semmle.label | ... * ... | +| test.cpp:152:11:152:28 | ... * ... | semmle.label | ... * ... | +| test.cpp:211:9:211:42 | Store | semmle.label | Store | +| test.cpp:211:14:211:19 | call to getenv | semmle.label | call to getenv | +| test.cpp:211:14:211:27 | (const char *)... | semmle.label | (const char *)... | +| test.cpp:224:23:224:23 | s | semmle.label | s | +| test.cpp:225:21:225:21 | s | semmle.label | s | +| test.cpp:225:21:225:21 | s | semmle.label | s | +| test.cpp:225:21:225:21 | s | semmle.label | s | +| test.cpp:230:21:230:21 | s | semmle.label | s | +| test.cpp:231:21:231:21 | s | semmle.label | s | +| test.cpp:231:21:231:21 | s | semmle.label | s | +| test.cpp:231:21:231:21 | s | semmle.label | s | +| test.cpp:237:24:237:29 | call to getenv | semmle.label | call to getenv | +| test.cpp:237:24:237:37 | (const char *)... | semmle.label | (const char *)... | +| test.cpp:239:9:239:18 | (size_t)... | semmle.label | (size_t)... | +| test.cpp:239:9:239:18 | (size_t)... | semmle.label | (size_t)... | +| test.cpp:239:9:239:18 | local_size | semmle.label | local_size | +| test.cpp:239:9:239:18 | local_size | semmle.label | local_size | +| test.cpp:239:9:239:18 | local_size | semmle.label | local_size | +| test.cpp:241:9:241:24 | call to get_tainted_size | semmle.label | call to get_tainted_size | +| test.cpp:241:9:241:24 | call to get_tainted_size | semmle.label | call to get_tainted_size | +| test.cpp:241:9:241:24 | call to get_tainted_size | semmle.label | call to get_tainted_size | +| test.cpp:245:2:245:9 | local_size | semmle.label | local_size | +| test.cpp:247:2:247:8 | local_size | semmle.label | local_size | +| test.cpp:251:2:251:32 | Chi [array content] | semmle.label | Chi [array content] | +| test.cpp:251:2:251:32 | ChiPartial | semmle.label | ChiPartial | +| test.cpp:251:18:251:23 | call to getenv | semmle.label | call to getenv | +| test.cpp:251:18:251:31 | (const char *)... | semmle.label | (const char *)... | +| test.cpp:259:20:259:25 | call to getenv | semmle.label | call to getenv | +| test.cpp:259:20:259:33 | (const char *)... | semmle.label | (const char *)... | +| test.cpp:263:11:263:29 | ... * ... | semmle.label | ... * ... | +| test.cpp:263:11:263:29 | ... * ... | semmle.label | ... * ... | +| test.cpp:263:11:263:29 | ... * ... | semmle.label | ... * ... | +| test.cpp:289:17:289:20 | Chi | semmle.label | Chi | +| test.cpp:289:17:289:20 | get_size output argument [array content] | semmle.label | get_size output argument [array content] | +| test.cpp:291:11:291:28 | ... * ... | semmle.label | ... * ... | +| test.cpp:291:11:291:28 | ... * ... | semmle.label | ... * ... | +| test.cpp:291:11:291:28 | ... * ... | semmle.label | ... * ... | +| test.cpp:305:18:305:21 | Chi | semmle.label | Chi | +| test.cpp:305:18:305:21 | get_size output argument [array content] | semmle.label | get_size output argument [array content] | +| test.cpp:308:10:308:27 | ... * ... | semmle.label | ... * ... | +| test.cpp:308:10:308:27 | ... * ... | semmle.label | ... * ... | +| test.cpp:308:10:308:27 | ... * ... | semmle.label | ... * ... | #select -| test.cpp:42:31:42:36 | call to malloc | test.cpp:39:21:39:24 | argv | test.cpp:42:38:42:44 | tainted | This allocation size is derived from $@ and might overflow | test.cpp:39:21:39:24 | argv | user input (argv) | -| test.cpp:43:31:43:36 | call to malloc | test.cpp:39:21:39:24 | argv | test.cpp:43:38:43:63 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:39:21:39:24 | argv | user input (argv) | -| test.cpp:45:31:45:36 | call to malloc | test.cpp:39:21:39:24 | argv | test.cpp:45:38:45:63 | ... + ... | This allocation size is derived from $@ and might overflow | test.cpp:39:21:39:24 | argv | user input (argv) | -| test.cpp:48:25:48:30 | call to malloc | test.cpp:39:21:39:24 | argv | test.cpp:48:32:48:35 | size | This allocation size is derived from $@ and might overflow | test.cpp:39:21:39:24 | argv | user input (argv) | -| test.cpp:49:17:49:30 | new[] | test.cpp:39:21:39:24 | argv | test.cpp:49:26:49:29 | size | This allocation size is derived from $@ and might overflow | test.cpp:39:21:39:24 | argv | user input (argv) | -| test.cpp:52:21:52:27 | call to realloc | test.cpp:39:21:39:24 | argv | test.cpp:52:35:52:60 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:39:21:39:24 | argv | user input (argv) | -| test.cpp:127:17:127:22 | call to malloc | test.cpp:123:18:123:23 | call to getenv | test.cpp:127:24:127:41 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:123:18:123:23 | call to getenv | user input (getenv) | -| test.cpp:134:3:134:8 | call to malloc | test.cpp:132:19:132:24 | call to getenv | test.cpp:134:10:134:27 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:132:19:132:24 | call to getenv | user input (getenv) | -| test.cpp:215:14:215:19 | call to malloc | test.cpp:227:24:227:29 | call to getenv | test.cpp:215:21:215:21 | s | This allocation size is derived from $@ and might overflow | test.cpp:227:24:227:29 | call to getenv | user input (getenv) | -| test.cpp:221:14:221:19 | call to malloc | test.cpp:227:24:227:29 | call to getenv | test.cpp:221:21:221:21 | s | This allocation size is derived from $@ and might overflow | test.cpp:227:24:227:29 | call to getenv | user input (getenv) | -| test.cpp:229:2:229:7 | call to malloc | test.cpp:227:24:227:29 | call to getenv | test.cpp:229:9:229:18 | local_size | This allocation size is derived from $@ and might overflow | test.cpp:227:24:227:29 | call to getenv | user input (getenv) | -| test.cpp:231:2:231:7 | call to malloc | test.cpp:201:14:201:19 | call to getenv | test.cpp:231:9:231:24 | call to get_tainted_size | This allocation size is derived from $@ and might overflow | test.cpp:201:14:201:19 | call to getenv | user input (getenv) | -| test.cpp:253:4:253:9 | call to malloc | test.cpp:249:20:249:25 | call to getenv | test.cpp:253:11:253:29 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:249:20:249:25 | call to getenv | user input (getenv) | -| test.cpp:281:4:281:9 | call to malloc | test.cpp:241:18:241:23 | call to getenv | test.cpp:281:11:281:28 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:241:18:241:23 | call to getenv | user input (getenv) | -| test.cpp:298:3:298:8 | call to malloc | test.cpp:241:18:241:23 | call to getenv | test.cpp:298:10:298:27 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:241:18:241:23 | call to getenv | user input (getenv) | +| test.cpp:43:31:43:36 | call to malloc | test.cpp:40:21:40:24 | argv | test.cpp:43:38:43:44 | tainted | This allocation size is derived from $@ and might overflow | test.cpp:40:21:40:24 | argv | user input (argv) | +| test.cpp:44:31:44:36 | call to malloc | test.cpp:40:21:40:24 | argv | test.cpp:44:38:44:63 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:40:21:40:24 | argv | user input (argv) | +| test.cpp:46:31:46:36 | call to malloc | test.cpp:40:21:40:24 | argv | test.cpp:46:38:46:63 | ... + ... | This allocation size is derived from $@ and might overflow | test.cpp:40:21:40:24 | argv | user input (argv) | +| test.cpp:49:25:49:30 | call to malloc | test.cpp:40:21:40:24 | argv | test.cpp:49:32:49:35 | size | This allocation size is derived from $@ and might overflow | test.cpp:40:21:40:24 | argv | user input (argv) | +| test.cpp:50:17:50:30 | new[] | test.cpp:40:21:40:24 | argv | test.cpp:50:26:50:29 | size | This allocation size is derived from $@ and might overflow | test.cpp:40:21:40:24 | argv | user input (argv) | +| test.cpp:53:21:53:27 | call to realloc | test.cpp:40:21:40:24 | argv | test.cpp:53:35:53:60 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:40:21:40:24 | argv | user input (argv) | +| test.cpp:128:17:128:22 | call to malloc | test.cpp:124:18:124:23 | call to getenv | test.cpp:128:24:128:41 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:124:18:124:23 | call to getenv | user input (getenv) | +| test.cpp:135:3:135:8 | call to malloc | test.cpp:133:19:133:24 | call to getenv | test.cpp:135:10:135:27 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:133:19:133:24 | call to getenv | user input (getenv) | +| test.cpp:152:4:152:9 | call to malloc | test.cpp:148:20:148:25 | call to getenv | test.cpp:152:11:152:28 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:148:20:148:25 | call to getenv | user input (getenv) | +| test.cpp:225:14:225:19 | call to malloc | test.cpp:237:24:237:29 | call to getenv | test.cpp:225:21:225:21 | s | This allocation size is derived from $@ and might overflow | test.cpp:237:24:237:29 | call to getenv | user input (getenv) | +| test.cpp:231:14:231:19 | call to malloc | test.cpp:237:24:237:29 | call to getenv | test.cpp:231:21:231:21 | s | This allocation size is derived from $@ and might overflow | test.cpp:237:24:237:29 | call to getenv | user input (getenv) | +| test.cpp:239:2:239:7 | call to malloc | test.cpp:237:24:237:29 | call to getenv | test.cpp:239:9:239:18 | local_size | This allocation size is derived from $@ and might overflow | test.cpp:237:24:237:29 | call to getenv | user input (getenv) | +| test.cpp:241:2:241:7 | call to malloc | test.cpp:211:14:211:19 | call to getenv | test.cpp:241:9:241:24 | call to get_tainted_size | This allocation size is derived from $@ and might overflow | test.cpp:211:14:211:19 | call to getenv | user input (getenv) | +| test.cpp:263:4:263:9 | call to malloc | test.cpp:259:20:259:25 | call to getenv | test.cpp:263:11:263:29 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:259:20:259:25 | call to getenv | user input (getenv) | +| test.cpp:291:4:291:9 | call to malloc | test.cpp:251:18:251:23 | call to getenv | test.cpp:291:11:291:28 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:251:18:251:23 | call to getenv | user input (getenv) | +| test.cpp:308:3:308:8 | call to malloc | test.cpp:251:18:251:23 | call to getenv | test.cpp:308:10:308:27 | ... * ... | This allocation size is derived from $@ and might overflow | test.cpp:251:18:251:23 | call to getenv | user input (getenv) | diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/test.cpp b/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/test.cpp index 57eb5b91a07..b11a136ed24 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/test.cpp +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-190/semmle/TaintedAllocationSize/test.cpp @@ -7,6 +7,7 @@ void *malloc(size_t size); void *realloc(void *ptr, size_t size); void free(void *ptr); int atoi(const char *nptr); +long atol(const char *nptr); struct MyStruct { char data[256]; @@ -143,6 +144,15 @@ void more_bounded_tests() { } } + { + long size = atol(getenv("USER")); + + if (size > 0) + { + malloc(size * sizeof(int)); // BAD + } + } + { int size = atoi(getenv("USER")); From b142ecb1dbc73e269b912719ca41f9c3684e56e5 Mon Sep 17 00:00:00 2001 From: Tom Hvitved Date: Mon, 17 May 2021 10:33:06 +0200 Subject: [PATCH 48/74] C#: Address review comment --- csharp/autobuilder/Semmle.Autobuild.CSharp/DotNetRule.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/csharp/autobuilder/Semmle.Autobuild.CSharp/DotNetRule.cs b/csharp/autobuilder/Semmle.Autobuild.CSharp/DotNetRule.cs index 955775d0f66..3d7a1168e30 100644 --- a/csharp/autobuilder/Semmle.Autobuild.CSharp/DotNetRule.cs +++ b/csharp/autobuilder/Semmle.Autobuild.CSharp/DotNetRule.cs @@ -236,12 +236,6 @@ namespace Semmle.Autobuild.CSharp /// /// Gets the `dotnet build` script. - /// - /// The CLR tracer only works on .NET Core >= 3 on Linux and macOS (see - /// https://github.com/dotnet/coreclr/issues/19622), so in case we are - /// running on an older .NET Core, we disable shared compilation (and - /// hence the need for CLR tracing), by adding a - /// `/p:UseSharedCompilation=false` argument. /// private static BuildScript GetBuildScript(Autobuilder builder, string? dotNetPath, IDictionary? environment, string projOrSln) { From 77c93dcf26fa083cc7fffc8e6b53a8bc59c169e1 Mon Sep 17 00:00:00 2001 From: Anders Schack-Mulligen Date: Mon, 17 May 2021 10:35:04 +0200 Subject: [PATCH 49/74] Make private --- .../code/java/frameworks/jackson/JacksonSerializability.qll | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll b/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll index 50266c377f8..6cda84512a0 100644 --- a/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll +++ b/java/ql/src/semmle/code/java/frameworks/jackson/JacksonSerializability.qll @@ -156,7 +156,7 @@ class JacksonDeserializableField extends DeserializableField { } /** A call to a field that may be deserialized using the Jackson JSON framework. */ -class JacksonDeserializableFieldAccess extends FieldAccess { +private class JacksonDeserializableFieldAccess extends FieldAccess { JacksonDeserializableFieldAccess() { getField() instanceof JacksonDeserializableField } } @@ -164,7 +164,7 @@ class JacksonDeserializableFieldAccess extends FieldAccess { * When an object is deserialized by the Jackson JSON framework using a tainted input source, * the fields that the framework deserialized are themselves tainted input data. */ -class JacksonDeserializedTaintStep extends AdditionalTaintStep { +private class JacksonDeserializedTaintStep extends AdditionalTaintStep { override predicate step(DataFlow::Node node1, DataFlow::Node node2) { DataFlow::getFieldQualifier(node2.asExpr().(JacksonDeserializableFieldAccess)) = node1 } From e652d8771c343c819d62d1901593185af2506d25 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Mon, 17 May 2021 20:34:16 +0000 Subject: [PATCH 50/74] Update method name and qldoc --- .../Security/CWE/CWE-094/JythonInjection.ql | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.ql index 0f23edc69f4..06b077446e2 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.ql @@ -52,7 +52,7 @@ class LoadClassMethod extends Method { * Holds if `ma` is a call to a class-loading method, and `sink` is the byte array * representing the class to be loaded. */ -predicate loadClass(MethodAccess ma, Expr sink) { +predicate loadsClass(MethodAccess ma, Expr sink) { exists(Method m, int i | m = ma.getMethod() | m instanceof LoadClassMethod and m.getParameter(i).getType() instanceof Array and // makeClass(java.lang.String name, byte[] data, ...) @@ -85,17 +85,21 @@ predicate compile(MethodAccess ma, Expr sink) { class CodeInjectionSink extends DataFlow::ExprNode { CodeInjectionSink() { runCode(_, this.getExpr()) or - loadClass(_, this.getExpr()) or + loadsClass(_, this.getExpr()) or compile(_, this.getExpr()) } MethodAccess getMethodAccess() { runCode(result, this.getExpr()) or - loadClass(result, this.getExpr()) or + loadsClass(result, this.getExpr()) or compile(result, this.getExpr()) } } +/** + * A taint configuration for tracking flow from `RemoteFlowSource` to a Jython method call + * `CodeInjectionSink` that executes injected code. + */ class CodeInjectionConfiguration extends TaintTracking::Configuration { CodeInjectionConfiguration() { this = "CodeInjectionConfiguration" } From a0cd551bae9ae005c59657ef3463817e226cfe29 Mon Sep 17 00:00:00 2001 From: haby0 Date: Tue, 18 May 2021 11:05:10 +0800 Subject: [PATCH 51/74] Add filtering of String.format --- .../Security/CWE/CWE-601/SpringUrlRedirect.ql | 10 +++ .../CWE/CWE-601/SpringUrlRedirect.qll | 61 +------------------ .../CWE-601/SpringUrlRedirect.expected | 8 +++ .../security/CWE-601/SpringUrlRedirect.java | 17 +++++- 4 files changed, 37 insertions(+), 59 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql index 138bce57ac9..5c8a8ea78d8 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql @@ -33,6 +33,16 @@ class SpringUrlRedirectFlowConfig extends TaintTracking::Configuration { ae.getRightOperand() = node.asExpr() and not ae instanceof RedirectBuilderExpr ) + or + exists(MethodAccess ma, int index | + ma.getMethod().hasName("format") and + ma.getMethod().getDeclaringType() instanceof TypeString and + ma.getArgument(index) = node.asExpr() and + ( + index != 0 and + not ma.getArgument(0).(CompileTimeConstantExpr).getStringValue().regexpMatch("^%s.*") + ) + ) } } diff --git a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll index 866eaae1c34..84bb5d9adf8 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll +++ b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll @@ -51,14 +51,14 @@ class SpringUrlRedirectSink extends DataFlow::Node { SpringUrlRedirectSink() { exists(RedirectBuilderExpr rbe | rbe.getRightOperand() = this.asExpr() and - exists(RedirectBuilderFlowConfig rbfc | rbfc.hasFlow(exprNode(rbe), _)) + any(SpringRequestMappingMethod sqmm).polyCalls*(this.getEnclosingCallable()) ) or exists(MethodAccess ma, RedirectAppendCall rac | DataFlow2::localExprFlow(rac.getQualifier(), ma.getQualifier()) and ma.getMethod().hasName("append") and ma.getArgument(0) = this.asExpr() and - exists(RedirectBuilderFlowConfig rbfc | rbfc.hasFlow(exprNode(ma.getQualifier()), _)) + any(SpringRequestMappingMethod sqmm).polyCalls*(this.getEnclosingCallable()) ) or exists(MethodAccess ma | @@ -66,8 +66,7 @@ class SpringUrlRedirectSink extends DataFlow::Node { ma.getMethod() .getDeclaringType() .hasQualifiedName("org.springframework.web.servlet.view", "AbstractUrlBasedView") and - ma.getArgument(0) = this.asExpr() and - exists(RedirectViewFlowConfig rvfc | rvfc.hasFlowToExpr(ma.getQualifier())) + ma.getArgument(0) = this.asExpr() ) or exists(ClassInstanceExpr cie | @@ -84,57 +83,3 @@ class SpringUrlRedirectSink extends DataFlow::Node { ) } } - -/** A data flow configuration tracing flow from redirect builder expression to spring controller method return expression. */ -private class RedirectBuilderFlowConfig extends DataFlow2::Configuration { - RedirectBuilderFlowConfig() { this = "RedirectBuilderFlowConfig" } - - override predicate isSource(DataFlow::Node src) { - exists(RedirectBuilderExpr rbe | rbe = src.asExpr()) - or - exists(MethodAccess ma, RedirectAppendCall rac | - DataFlow2::localExprFlow(rac.getQualifier(), ma.getQualifier()) and - ma.getMethod().hasName("append") and - ma.getQualifier() = src.asExpr() - ) - } - - override predicate isSink(DataFlow::Node sink) { - exists(ReturnStmt rs, SpringRequestMappingMethod sqmm | - rs.getResult() = sink.asExpr() and - sqmm.getBody().getAStmt() = rs - ) - } - - override predicate isAdditionalFlowStep(Node prod, Node succ) { - exists(MethodAccess ma | - ma.getMethod().hasName("toString") and - ma.getMethod().getDeclaringType() instanceof StringBuildingType and - ma.getQualifier() = prod.asExpr() and - ma = succ.asExpr() - ) - } -} - -/** A data flow configuration tracing flow from RedirectView object to calling setUrl method. */ -private class RedirectViewFlowConfig extends DataFlow2::Configuration { - RedirectViewFlowConfig() { this = "RedirectViewFlowConfig" } - - override predicate isSource(DataFlow::Node src) { - exists(ClassInstanceExpr cie | - cie.getConstructedType() - .hasQualifiedName("org.springframework.web.servlet.view", "RedirectView") and - cie = src.asExpr() - ) - } - - override predicate isSink(DataFlow::Node sink) { - exists(MethodAccess ma | - ma.getMethod().hasName("setUrl") and - ma.getMethod() - .getDeclaringType() - .hasQualifiedName("org.springframework.web.servlet.view", "AbstractUrlBasedView") and - ma.getQualifier() = sink.asExpr() - ) - } -} diff --git a/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.expected b/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.expected index 26b8acd7770..bcf5e892e1b 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.expected +++ b/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.expected @@ -5,6 +5,8 @@ edges | SpringUrlRedirect.java:32:30:32:47 | redirectUrl : String | SpringUrlRedirect.java:33:47:33:57 | redirectUrl | | SpringUrlRedirect.java:37:24:37:41 | redirectUrl : String | SpringUrlRedirect.java:40:29:40:39 | redirectUrl | | SpringUrlRedirect.java:45:24:45:41 | redirectUrl : String | SpringUrlRedirect.java:48:30:48:40 | redirectUrl | +| SpringUrlRedirect.java:53:24:53:41 | redirectUrl : String | SpringUrlRedirect.java:54:30:54:66 | format(...) | +| SpringUrlRedirect.java:58:24:58:41 | redirectUrl : String | SpringUrlRedirect.java:59:30:59:76 | format(...) | nodes | SpringUrlRedirect.java:13:30:13:47 | redirectUrl : String | semmle.label | redirectUrl : String | | SpringUrlRedirect.java:15:19:15:29 | redirectUrl | semmle.label | redirectUrl | @@ -18,6 +20,10 @@ nodes | SpringUrlRedirect.java:40:29:40:39 | redirectUrl | semmle.label | redirectUrl | | SpringUrlRedirect.java:45:24:45:41 | redirectUrl : String | semmle.label | redirectUrl : String | | SpringUrlRedirect.java:48:30:48:40 | redirectUrl | semmle.label | redirectUrl | +| SpringUrlRedirect.java:53:24:53:41 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:54:30:54:66 | format(...) | semmle.label | format(...) | +| SpringUrlRedirect.java:58:24:58:41 | redirectUrl : String | semmle.label | redirectUrl : String | +| SpringUrlRedirect.java:59:30:59:76 | format(...) | semmle.label | format(...) | #select | SpringUrlRedirect.java:15:19:15:29 | redirectUrl | SpringUrlRedirect.java:13:30:13:47 | redirectUrl : String | SpringUrlRedirect.java:15:19:15:29 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:13:30:13:47 | redirectUrl | user-provided value | | SpringUrlRedirect.java:21:36:21:46 | redirectUrl | SpringUrlRedirect.java:20:24:20:41 | redirectUrl : String | SpringUrlRedirect.java:21:36:21:46 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:20:24:20:41 | redirectUrl | user-provided value | @@ -25,3 +31,5 @@ nodes | SpringUrlRedirect.java:33:47:33:57 | redirectUrl | SpringUrlRedirect.java:32:30:32:47 | redirectUrl : String | SpringUrlRedirect.java:33:47:33:57 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:32:30:32:47 | redirectUrl | user-provided value | | SpringUrlRedirect.java:40:29:40:39 | redirectUrl | SpringUrlRedirect.java:37:24:37:41 | redirectUrl : String | SpringUrlRedirect.java:40:29:40:39 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:37:24:37:41 | redirectUrl | user-provided value | | SpringUrlRedirect.java:48:30:48:40 | redirectUrl | SpringUrlRedirect.java:45:24:45:41 | redirectUrl : String | SpringUrlRedirect.java:48:30:48:40 | redirectUrl | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:45:24:45:41 | redirectUrl | user-provided value | +| SpringUrlRedirect.java:54:30:54:66 | format(...) | SpringUrlRedirect.java:53:24:53:41 | redirectUrl : String | SpringUrlRedirect.java:54:30:54:66 | format(...) | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:53:24:53:41 | redirectUrl | user-provided value | +| SpringUrlRedirect.java:59:30:59:76 | format(...) | SpringUrlRedirect.java:58:24:58:41 | redirectUrl : String | SpringUrlRedirect.java:59:30:59:76 | format(...) | Potentially untrusted URL redirection due to $@. | SpringUrlRedirect.java:58:24:58:41 | redirectUrl | user-provided value | diff --git a/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.java b/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.java index 5124d8cd8c6..f3958cba102 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.java +++ b/java/ql/test/experimental/query-tests/security/CWE-601/SpringUrlRedirect.java @@ -50,6 +50,16 @@ public class SpringUrlRedirect { } @GetMapping("url7") + public String bad7(String redirectUrl) { + return "redirect:" + String.format("%s/?aaa", redirectUrl); + } + + @GetMapping("url8") + public String bad8(String redirectUrl, String token) { + return "redirect:" + String.format(redirectUrl + "?token=%s", token); + } + + @GetMapping("url9") public RedirectView good1(String redirectUrl) { RedirectView rv = new RedirectView(); if (redirectUrl.startsWith(VALID_REDIRECT)){ @@ -60,9 +70,14 @@ public class SpringUrlRedirect { return rv; } - @GetMapping("url8") + @GetMapping("url10") public ModelAndView good2(String token) { String url = "/edit?token=" + token; return new ModelAndView("redirect:" + url); } + + @GetMapping("url11") + public String good3(String status) { + return "redirect:" + String.format("/stories/search/criteria?status=%s", status); + } } From 1435ac715ac6d7f3f5255c7069e305620cc6afd2 Mon Sep 17 00:00:00 2001 From: Erik Krogh Kristensen Date: Tue, 18 May 2021 12:46:34 +0200 Subject: [PATCH 52/74] add support for the clone library --- javascript/change-notes/2021-05-18-clone.md | 4 ++++ javascript/ql/src/javascript.qll | 1 + .../src/semmle/javascript/frameworks/Clone.qll | 15 +++++++++++++++ .../CWE-079/ReflectedXss/ReflectedXss.expected | 17 +++++++++++++++++ .../ReflectedXssWithCustomSanitizer.expected | 2 ++ .../Security/CWE-079/ReflectedXss/tst2.js | 14 ++++++++++++++ 6 files changed, 53 insertions(+) create mode 100644 javascript/change-notes/2021-05-18-clone.md create mode 100644 javascript/ql/src/semmle/javascript/frameworks/Clone.qll diff --git a/javascript/change-notes/2021-05-18-clone.md b/javascript/change-notes/2021-05-18-clone.md new file mode 100644 index 00000000000..42d52d72a3d --- /dev/null +++ b/javascript/change-notes/2021-05-18-clone.md @@ -0,0 +1,4 @@ +lgtm,codescanning +* The dataflow libraries now model dataflow in the `clone` library. + Affected packages are + [clone](https://npmjs.com/package/clone) diff --git a/javascript/ql/src/javascript.qll b/javascript/ql/src/javascript.qll index 95db948bbd4..a46313f47f0 100644 --- a/javascript/ql/src/javascript.qll +++ b/javascript/ql/src/javascript.qll @@ -78,6 +78,7 @@ import semmle.javascript.frameworks.ComposedFunctions import semmle.javascript.frameworks.Classnames import semmle.javascript.frameworks.ClassValidator import semmle.javascript.frameworks.ClientRequests +import semmle.javascript.frameworks.Clone import semmle.javascript.frameworks.ClosureLibrary import semmle.javascript.frameworks.CookieLibraries import semmle.javascript.frameworks.Credentials diff --git a/javascript/ql/src/semmle/javascript/frameworks/Clone.qll b/javascript/ql/src/semmle/javascript/frameworks/Clone.qll new file mode 100644 index 00000000000..5aabee438c5 --- /dev/null +++ b/javascript/ql/src/semmle/javascript/frameworks/Clone.qll @@ -0,0 +1,15 @@ +/** + * Provides a dataflow-step for the `clone` package. + */ + +import javascript +private import semmle.javascript.dataflow.internal.PreCallGraphStep + +private class CloneStep extends PreCallGraphStep { + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { + exists(DataFlow::CallNode call | call = DataFlow::moduleImport("clone").getACall() | + pred = call.getArgument(0) and + succ = call + ) + } +} diff --git a/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXss.expected b/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXss.expected index f8ceeb45b62..e8d093325ad 100644 --- a/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXss.expected +++ b/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXss.expected @@ -174,6 +174,14 @@ nodes | tst2.js:18:12:18:12 | p | | tst2.js:21:14:21:14 | p | | tst2.js:21:14:21:14 | p | +| tst2.js:30:7:30:24 | p | +| tst2.js:30:9:30:9 | p | +| tst2.js:30:9:30:9 | p | +| tst2.js:33:11:33:11 | p | +| tst2.js:36:12:36:12 | p | +| tst2.js:36:12:36:12 | p | +| tst2.js:37:12:37:18 | other.p | +| tst2.js:37:12:37:18 | other.p | edges | ReflectedXss.js:8:33:8:45 | req.params.id | ReflectedXss.js:8:14:8:45 | "Unknow ... rams.id | | ReflectedXss.js:8:33:8:45 | req.params.id | ReflectedXss.js:8:14:8:45 | "Unknow ... rams.id | @@ -318,6 +326,13 @@ edges | tst2.js:14:7:14:24 | p | tst2.js:21:14:21:14 | p | | tst2.js:14:9:14:9 | p | tst2.js:14:7:14:24 | p | | tst2.js:14:9:14:9 | p | tst2.js:14:7:14:24 | p | +| tst2.js:30:7:30:24 | p | tst2.js:33:11:33:11 | p | +| tst2.js:30:7:30:24 | p | tst2.js:36:12:36:12 | p | +| tst2.js:30:7:30:24 | p | tst2.js:36:12:36:12 | p | +| tst2.js:30:9:30:9 | p | tst2.js:30:7:30:24 | p | +| tst2.js:30:9:30:9 | p | tst2.js:30:7:30:24 | p | +| tst2.js:33:11:33:11 | p | tst2.js:37:12:37:18 | other.p | +| tst2.js:33:11:33:11 | p | tst2.js:37:12:37:18 | other.p | #select | ReflectedXss.js:8:14:8:45 | "Unknow ... rams.id | ReflectedXss.js:8:33:8:45 | req.params.id | ReflectedXss.js:8:14:8:45 | "Unknow ... rams.id | Cross-site scripting vulnerability due to $@. | ReflectedXss.js:8:33:8:45 | req.params.id | user-provided value | | ReflectedXss.js:17:12:17:39 | "Unknow ... rams.id | ReflectedXss.js:17:31:17:39 | params.id | ReflectedXss.js:17:12:17:39 | "Unknow ... rams.id | Cross-site scripting vulnerability due to $@. | ReflectedXss.js:17:31:17:39 | params.id | user-provided value | @@ -359,3 +374,5 @@ edges | tst2.js:8:12:8:12 | r | tst2.js:6:12:6:15 | q: r | tst2.js:8:12:8:12 | r | Cross-site scripting vulnerability due to $@. | tst2.js:6:12:6:15 | q: r | user-provided value | | tst2.js:18:12:18:12 | p | tst2.js:14:9:14:9 | p | tst2.js:18:12:18:12 | p | Cross-site scripting vulnerability due to $@. | tst2.js:14:9:14:9 | p | user-provided value | | tst2.js:21:14:21:14 | p | tst2.js:14:9:14:9 | p | tst2.js:21:14:21:14 | p | Cross-site scripting vulnerability due to $@. | tst2.js:14:9:14:9 | p | user-provided value | +| tst2.js:36:12:36:12 | p | tst2.js:30:9:30:9 | p | tst2.js:36:12:36:12 | p | Cross-site scripting vulnerability due to $@. | tst2.js:30:9:30:9 | p | user-provided value | +| tst2.js:37:12:37:18 | other.p | tst2.js:30:9:30:9 | p | tst2.js:37:12:37:18 | other.p | Cross-site scripting vulnerability due to $@. | tst2.js:30:9:30:9 | p | user-provided value | diff --git a/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXssWithCustomSanitizer.expected b/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXssWithCustomSanitizer.expected index 719a82171a8..8ddc55dde36 100644 --- a/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXssWithCustomSanitizer.expected +++ b/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXssWithCustomSanitizer.expected @@ -37,3 +37,5 @@ | tst2.js:8:12:8:12 | r | Cross-site scripting vulnerability due to $@. | tst2.js:6:12:6:15 | q: r | user-provided value | | tst2.js:18:12:18:12 | p | Cross-site scripting vulnerability due to $@. | tst2.js:14:9:14:9 | p | user-provided value | | tst2.js:21:14:21:14 | p | Cross-site scripting vulnerability due to $@. | tst2.js:14:9:14:9 | p | user-provided value | +| tst2.js:36:12:36:12 | p | Cross-site scripting vulnerability due to $@. | tst2.js:30:9:30:9 | p | user-provided value | +| tst2.js:37:12:37:18 | other.p | Cross-site scripting vulnerability due to $@. | tst2.js:30:9:30:9 | p | user-provided value | diff --git a/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/tst2.js b/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/tst2.js index 521b6b20a7c..034b0791217 100644 --- a/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/tst2.js +++ b/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/tst2.js @@ -22,3 +22,17 @@ app.get('/bar', function(req, res) { else res.send(p); // OK }); + + +const clone = require('clone'); + +app.get('/baz', function(req, res) { + let { p } = req.params; + + var obj = {}; + obj.p = p; + var other = clone(obj); + + res.send(p); // NOT OK + res.send(other.p); // NOT OK +}); \ No newline at end of file From caf5f4d605bbdcb67fa65de5b65f9288a39d8ef4 Mon Sep 17 00:00:00 2001 From: haby0 Date: Tue, 18 May 2021 19:10:03 +0800 Subject: [PATCH 53/74] modified comment --- .../src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql | 2 ++ .../src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql index 5c8a8ea78d8..210067fde2a 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql @@ -29,6 +29,8 @@ class SpringUrlRedirectFlowConfig extends TaintTracking::Configuration { override predicate isSanitizer(DataFlow::Node node) { // Exclude the case where the left side of the concatenated string is not `redirect:`. // E.g: `String url = "/path?token=" + request.getParameter("token");` + // Note this is quite a broad sanitizer (it will also sanitize the right-hand side of `url = "http://" + request.getParameter("token")`); + // Consider making this stricter in future. exists(AddExpr ae | ae.getRightOperand() = node.asExpr() and not ae instanceof RedirectBuilderExpr diff --git a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll index 84bb5d9adf8..6ae87a37bcc 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll +++ b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll @@ -5,7 +5,7 @@ import semmle.code.java.dataflow.DataFlow2 import semmle.code.java.dataflow.TaintTracking import semmle.code.java.frameworks.spring.SpringController -class StartsWithSanitizer extends DataFlow::BarrierGuard { +private class StartsWithSanitizer extends DataFlow::BarrierGuard { StartsWithSanitizer() { this.(MethodAccess).getMethod().hasName("startsWith") and this.(MethodAccess).getMethod().getDeclaringType() instanceof TypeString and From 06514a2bb6c3577269c312910e1d3c15764a6347 Mon Sep 17 00:00:00 2001 From: Erik Krogh Kristensen Date: Tue, 18 May 2021 13:16:41 +0200 Subject: [PATCH 54/74] move clone model to Extend.qll --- javascript/ql/src/javascript.qll | 1 - javascript/ql/src/semmle/javascript/Extend.qll | 14 ++++++++++++++ .../ql/src/semmle/javascript/frameworks/Clone.qll | 15 --------------- 3 files changed, 14 insertions(+), 16 deletions(-) delete mode 100644 javascript/ql/src/semmle/javascript/frameworks/Clone.qll diff --git a/javascript/ql/src/javascript.qll b/javascript/ql/src/javascript.qll index a46313f47f0..95db948bbd4 100644 --- a/javascript/ql/src/javascript.qll +++ b/javascript/ql/src/javascript.qll @@ -78,7 +78,6 @@ import semmle.javascript.frameworks.ComposedFunctions import semmle.javascript.frameworks.Classnames import semmle.javascript.frameworks.ClassValidator import semmle.javascript.frameworks.ClientRequests -import semmle.javascript.frameworks.Clone import semmle.javascript.frameworks.ClosureLibrary import semmle.javascript.frameworks.CookieLibraries import semmle.javascript.frameworks.Credentials diff --git a/javascript/ql/src/semmle/javascript/Extend.qll b/javascript/ql/src/semmle/javascript/Extend.qll index 9b7255983b9..d50054fc4ee 100644 --- a/javascript/ql/src/semmle/javascript/Extend.qll +++ b/javascript/ql/src/semmle/javascript/Extend.qll @@ -174,3 +174,17 @@ private class ExtendCallTaintStep extends TaintTracking::SharedTaintStep { ) } } + +private import semmle.javascript.dataflow.internal.PreCallGraphStep + +/** + * A step for the `clone` package. + */ +private class CloneStep extends PreCallGraphStep { + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { + exists(DataFlow::CallNode call | call = DataFlow::moduleImport("clone").getACall() | + pred = call.getArgument(0) and + succ = call + ) + } +} diff --git a/javascript/ql/src/semmle/javascript/frameworks/Clone.qll b/javascript/ql/src/semmle/javascript/frameworks/Clone.qll deleted file mode 100644 index 5aabee438c5..00000000000 --- a/javascript/ql/src/semmle/javascript/frameworks/Clone.qll +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Provides a dataflow-step for the `clone` package. - */ - -import javascript -private import semmle.javascript.dataflow.internal.PreCallGraphStep - -private class CloneStep extends PreCallGraphStep { - override predicate step(DataFlow::Node pred, DataFlow::Node succ) { - exists(DataFlow::CallNode call | call = DataFlow::moduleImport("clone").getACall() | - pred = call.getArgument(0) and - succ = call - ) - } -} From e46de44473791493bd73068dabced3be868da819 Mon Sep 17 00:00:00 2001 From: haby0 Date: Tue, 18 May 2021 19:56:32 +0800 Subject: [PATCH 55/74] Solve errors caused by private ownership --- .../Security/CWE/CWE-601/SpringUrlRedirect.ql | 12 ++++++++++++ .../Security/CWE/CWE-601/SpringUrlRedirect.qll | 12 ------------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql index 210067fde2a..b02bd3e4c30 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql @@ -15,6 +15,18 @@ import SpringUrlRedirect import semmle.code.java.dataflow.FlowSources import DataFlow::PathGraph +private class StartsWithSanitizer extends DataFlow::BarrierGuard { + StartsWithSanitizer() { + this.(MethodAccess).getMethod().hasName("startsWith") and + this.(MethodAccess).getMethod().getDeclaringType() instanceof TypeString and + this.(MethodAccess).getMethod().getNumberOfParameters() = 1 + } + + override predicate checks(Expr e, boolean branch) { + e = this.(MethodAccess).getQualifier() and branch = true + } +} + class SpringUrlRedirectFlowConfig extends TaintTracking::Configuration { SpringUrlRedirectFlowConfig() { this = "SpringUrlRedirectFlowConfig" } diff --git a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll index 6ae87a37bcc..4a86640d4d4 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll +++ b/java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.qll @@ -5,18 +5,6 @@ import semmle.code.java.dataflow.DataFlow2 import semmle.code.java.dataflow.TaintTracking import semmle.code.java.frameworks.spring.SpringController -private class StartsWithSanitizer extends DataFlow::BarrierGuard { - StartsWithSanitizer() { - this.(MethodAccess).getMethod().hasName("startsWith") and - this.(MethodAccess).getMethod().getDeclaringType() instanceof TypeString and - this.(MethodAccess).getMethod().getNumberOfParameters() = 1 - } - - override predicate checks(Expr e, boolean branch) { - e = this.(MethodAccess).getQualifier() and branch = true - } -} - /** * A concatenate expression using the string `redirect:` or `ajaxredirect:` or `forward:` on the left. * From 2a0721b2aeae25d99e1c5805311b8b6e46a735af Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Tue, 18 May 2021 12:18:14 +0000 Subject: [PATCH 56/74] Optimize the sink and update method name --- .../Security/CWE/CWE-094/JythonInjection.ql | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.ql index 06b077446e2..c44eee23602 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-094/JythonInjection.ql @@ -33,7 +33,7 @@ class BytecodeLoader extends RefType { } /** Holds if a Jython expression if evaluated, compiled or executed. */ -predicate runCode(MethodAccess ma, Expr sink) { +predicate runsCode(MethodAccess ma, Expr sink) { exists(Method m | m = ma.getMethod() | m instanceof InterpretExprMethod and sink = ma.getArgument(0) @@ -83,17 +83,15 @@ predicate compile(MethodAccess ma, Expr sink) { /** An expression loaded by Jython. */ class CodeInjectionSink extends DataFlow::ExprNode { + MethodAccess methodAccess; + CodeInjectionSink() { - runCode(_, this.getExpr()) or - loadsClass(_, this.getExpr()) or - compile(_, this.getExpr()) + runsCode(methodAccess, this.getExpr()) or + loadsClass(methodAccess, this.getExpr()) or + compile(methodAccess, this.getExpr()) } - MethodAccess getMethodAccess() { - runCode(result, this.getExpr()) or - loadsClass(result, this.getExpr()) or - compile(result, this.getExpr()) - } + MethodAccess getMethodAccess() { result = methodAccess } } /** From 610e041e28a3c1d417e1e1c56fbccc7a2cb60896 Mon Sep 17 00:00:00 2001 From: Ethan Palm <56270045+ethanpalm@users.noreply.github.com> Date: Tue, 18 May 2021 11:42:08 -0400 Subject: [PATCH 57/74] Add reviewer feedback Co-authored-by: mc <42146119+mchammer01@users.noreply.github.com> --- docs/codeql/codeql-cli/creating-codeql-databases.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/codeql/codeql-cli/creating-codeql-databases.rst b/docs/codeql/codeql-cli/creating-codeql-databases.rst index 1552a077e24..637df58555b 100644 --- a/docs/codeql/codeql-cli/creating-codeql-databases.rst +++ b/docs/codeql/codeql-cli/creating-codeql-databases.rst @@ -169,7 +169,7 @@ build steps, you may need to explicitly define each step in the command line. are dependencies, the appropriate dependency manager (such as `dep `__). - The Go autobuilder attempts to automatically detect Go code in a repository, + The Go autobuilder attempts to automatically detect code written in Go in a repository, and only runs build scripts in an attempt to fetch dependencies. To force CodeQL to limit extraction to the files compiled by your build script, set the environment variable `CODEQL_EXTRACTOR_GO_BUILD_TRACING=on` or use the ``--command`` option to specify a From df9981de4fb2b01383dc1fc06d197f1b3b422e5b Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Tue, 18 May 2021 17:44:08 +0200 Subject: [PATCH 58/74] C++: Add testcases with false positives. --- .../semmle/tests/OverflowStatic.expected | 2 ++ .../CWE/CWE-119/semmle/tests/tests.cpp | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-119/semmle/tests/OverflowStatic.expected b/cpp/ql/test/query-tests/Security/CWE/CWE-119/semmle/tests/OverflowStatic.expected index ef334a73a2c..0e137bb29d7 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-119/semmle/tests/OverflowStatic.expected +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-119/semmle/tests/OverflowStatic.expected @@ -5,6 +5,8 @@ | tests.cpp:245:42:245:42 | 6 | Potential buffer-overflow: 'global_array_5' has size 5 not 6. | | tests.cpp:349:2:349:14 | access to array | Potential buffer-overflow: 'charArray' has size 10 but 'charArray[10]' is accessed here. | | tests.cpp:350:17:350:29 | access to array | Potential buffer-overflow: 'charArray' has size 10 but 'charArray[10]' is accessed here. | +| tests.cpp:594:4:594:12 | access to array | Potential buffer-overflow: counter 'k' <= 100 but 'buffer' has 16 elements. | +| tests.cpp:603:24:603:24 | n | Potential buffer-overflow: 'dest' has size 128 not 132. | | var_size_struct.cpp:54:5:54:14 | access to array | Potential buffer-overflow: 'str' has size 1 but 'str[1]' is accessed here. | | var_size_struct.cpp:55:5:55:14 | access to array | Potential buffer-overflow: 'str' has size 1 but 'str[1]' is accessed here. | | var_size_struct.cpp:103:39:103:41 | 129 | Potential buffer-overflow: 'str' has size 128 not 129. | diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-119/semmle/tests/tests.cpp b/cpp/ql/test/query-tests/Security/CWE/CWE-119/semmle/tests/tests.cpp index c4ccdfad8b3..d18d60a2ed6 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-119/semmle/tests/tests.cpp +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-119/semmle/tests/tests.cpp @@ -586,6 +586,23 @@ void test21(bool cond) if (ptr[-1] == 0) { return; } // GOOD: accesses buffer[1] } +void test22(bool b, const char* source) { + char buffer[16]; + int k; + for (k = 0; k <= 100; k++) { + if(k < 16) { + buffer[k] = 'x'; // GOOD [FALSE POSITIVE] + } + } + + char dest[128]; + int n = b ? 1024 : 132; + if (n >= 128) { + return; + } + memcpy(dest, source, n); // GOOD [FALSE POSITIVE] +} + int main(int argc, char *argv[]) { long long arr17[19]; @@ -609,6 +626,7 @@ int main(int argc, char *argv[]) test19(argc == 0); test20(); test21(argc == 0); + test22(argc == 0, argv[0]); return 0; } From 26c4a66dc407bd55b12d3afd83b80083708c85fc Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Tue, 18 May 2021 17:54:30 +0200 Subject: [PATCH 59/74] C++: Add range analysis to fix FPs. --- cpp/ql/src/Critical/OverflowStatic.ql | 16 ++++++++++++---- .../CWE-119/semmle/tests/OverflowStatic.expected | 2 -- .../Security/CWE/CWE-119/semmle/tests/tests.cpp | 4 ++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/cpp/ql/src/Critical/OverflowStatic.ql b/cpp/ql/src/Critical/OverflowStatic.ql index 833ee45499e..a7c1cb41cda 100644 --- a/cpp/ql/src/Critical/OverflowStatic.ql +++ b/cpp/ql/src/Critical/OverflowStatic.ql @@ -14,6 +14,7 @@ import cpp import semmle.code.cpp.commons.Buffer +import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis import LoopBounds private predicate staticBufferBase(VariableAccess access, Variable v) { @@ -51,6 +52,8 @@ predicate overflowOffsetInLoop(BufferAccess bufaccess, string msg) { loop.getStmt().getAChild*() = bufaccess.getEnclosingStmt() and loop.limit() >= bufaccess.bufferSize() and loop.counter().getAnAccess() = bufaccess.getArrayOffset() and + // Ensure that we don't have an upper bound on the array index that's less than the buffer size. + not upperBound(bufaccess.getArrayOffset().getFullyConverted()) < bufaccess.bufferSize() and msg = "Potential buffer-overflow: counter '" + loop.counter().toString() + "' <= " + loop.limit().toString() + " but '" + bufaccess.buffer().getName() + "' has " + @@ -94,10 +97,15 @@ class CallWithBufferSize extends FunctionCall { } int statedSizeValue() { - exists(Expr statedSizeSrc | - DataFlow::localExprFlow(statedSizeSrc, statedSizeExpr()) and - result = statedSizeSrc.getValue().toInt() - ) + // `upperBound(e)` defaults to `exprMaxVal(e)` when `e` isn't analyzable. So to get a meaningful + // result in this case we pick the minimum value obtainable from dataflow and range analysis. + result = + upperBound(statedSizeExpr()) + .minimum(any(Expr statedSizeSrc | + DataFlow::localExprFlow(statedSizeSrc, statedSizeExpr()) + | + statedSizeSrc.getValue().toInt() + )) } } diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-119/semmle/tests/OverflowStatic.expected b/cpp/ql/test/query-tests/Security/CWE/CWE-119/semmle/tests/OverflowStatic.expected index 0e137bb29d7..ef334a73a2c 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-119/semmle/tests/OverflowStatic.expected +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-119/semmle/tests/OverflowStatic.expected @@ -5,8 +5,6 @@ | tests.cpp:245:42:245:42 | 6 | Potential buffer-overflow: 'global_array_5' has size 5 not 6. | | tests.cpp:349:2:349:14 | access to array | Potential buffer-overflow: 'charArray' has size 10 but 'charArray[10]' is accessed here. | | tests.cpp:350:17:350:29 | access to array | Potential buffer-overflow: 'charArray' has size 10 but 'charArray[10]' is accessed here. | -| tests.cpp:594:4:594:12 | access to array | Potential buffer-overflow: counter 'k' <= 100 but 'buffer' has 16 elements. | -| tests.cpp:603:24:603:24 | n | Potential buffer-overflow: 'dest' has size 128 not 132. | | var_size_struct.cpp:54:5:54:14 | access to array | Potential buffer-overflow: 'str' has size 1 but 'str[1]' is accessed here. | | var_size_struct.cpp:55:5:55:14 | access to array | Potential buffer-overflow: 'str' has size 1 but 'str[1]' is accessed here. | | var_size_struct.cpp:103:39:103:41 | 129 | Potential buffer-overflow: 'str' has size 128 not 129. | diff --git a/cpp/ql/test/query-tests/Security/CWE/CWE-119/semmle/tests/tests.cpp b/cpp/ql/test/query-tests/Security/CWE/CWE-119/semmle/tests/tests.cpp index d18d60a2ed6..4f8bcc0fa59 100644 --- a/cpp/ql/test/query-tests/Security/CWE/CWE-119/semmle/tests/tests.cpp +++ b/cpp/ql/test/query-tests/Security/CWE/CWE-119/semmle/tests/tests.cpp @@ -591,7 +591,7 @@ void test22(bool b, const char* source) { int k; for (k = 0; k <= 100; k++) { if(k < 16) { - buffer[k] = 'x'; // GOOD [FALSE POSITIVE] + buffer[k] = 'x'; // GOOD } } @@ -600,7 +600,7 @@ void test22(bool b, const char* source) { if (n >= 128) { return; } - memcpy(dest, source, n); // GOOD [FALSE POSITIVE] + memcpy(dest, source, n); // GOOD } int main(int argc, char *argv[]) From b0b5338359ce7da9525f705d77784545f6090c8e Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Thu, 29 Apr 2021 02:59:41 +0000 Subject: [PATCH 60/74] Rhino code injection --- .../Security/CWE/CWE-094/RhinoInjection.java | 39 ++ .../Security/CWE/CWE-094/RhinoInjection.qhelp | 51 ++ .../Security/CWE/CWE-094/RhinoInjection.ql | 17 + .../Security/CWE/CWE-094/RhinoInjection.qll | 56 ++ .../security/CWE-094/RhinoInjection.expected | 7 + .../security/CWE-094/RhinoInjection.qlref | 1 + .../security/CWE-094/RhinoServlet.java | 78 +++ .../query-tests/security/CWE-094/options | 2 +- .../org/mozilla/javascript/ClassShutter.java | 56 ++ .../org/mozilla/javascript/Context.java | 623 ++++++++++++++++++ .../mozilla/javascript/ContextFactory.java | 314 +++++++++ .../mozilla/javascript/RhinoException.java | 15 + .../org/mozilla/javascript/Scriptable.java | 304 +++++++++ .../mozilla/javascript/ScriptableObject.java | 27 + 14 files changed, 1589 insertions(+), 1 deletion(-) create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.java create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.qhelp create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.ql create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.qll create mode 100644 java/ql/test/experimental/query-tests/security/CWE-094/RhinoInjection.expected create mode 100644 java/ql/test/experimental/query-tests/security/CWE-094/RhinoInjection.qlref create mode 100644 java/ql/test/experimental/query-tests/security/CWE-094/RhinoServlet.java create mode 100644 java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/ClassShutter.java create mode 100644 java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/Context.java create mode 100644 java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/ContextFactory.java create mode 100644 java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/RhinoException.java create mode 100644 java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/Scriptable.java create mode 100644 java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/ScriptableObject.java diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.java b/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.java new file mode 100644 index 00000000000..4a87d5bc354 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.java @@ -0,0 +1,39 @@ +public class RhinoInjection extends HttpServlet { + + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/plain"); + String code = request.getParameter("code"); + Context ctx = Context.enter(); + try { + { + // BAD: allow arbitrary Java and JavaScript code to be executed + Scriptable scope = ctx.initStandardObjects(); + } + + { + // GOOD: enable the safe mode + Scriptable scope = ctx.initSafeStandardObjects(); + } + + { + // GOOD: enforce a constraint on allowed classes + Scriptable scope = ctx.initStandardObjects(); + ctx.setClassShutter(new ClassShutter() { + public boolean visibleToScripts(String className) { + if(className.startsWith("com.example.")) { + return true; + } + return false; + } + }); + } + + Object result = ctx.evaluateString(scope, code, "", 1, null); + response.getWriter().print(Context.toString(result)); + } catch(RhinoException ex) { + response.getWriter().println(ex.getMessage()); + } finally { + Context.exit(); + } + } +} diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.qhelp b/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.qhelp new file mode 100644 index 00000000000..73c252ed54c --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.qhelp @@ -0,0 +1,51 @@ + + + + +

    +Rhino is a JavaScript engine written fully in Java and managed by the Mozilla Foundation. +It serves as an embedded scripting engine inside Java applications which allows +Java-to-JavaScript interoperability and provides a seamless integration between the two +languages. If an expression is built using attacker-controlled data, and then evaluated in +a powerful context, it may allow the attacker to run arbitrary code. +

    +

    +Typically an expression is evaluated in the powerful context initialized with +initStandardObjects that allows an expression of arbitrary Java code to +execute in the JVM. +

    +
    + + +

    +In general, including user input in a Rhino expression should be avoided. +If user input must be included in the expression, it should be then evaluated in a safe +context that doesn't allow arbitrary code invocation. +

    +
    + + +

    +The following example shows two ways of using Rhino expression. In the 'BAD' case, +an unsafe context is initialized with initStandardObjects. In the 'GOOD' case, +a safe context is initialized with initSafeStandardObjects or +setClassShutter. +

    + +
    + + +
  • + Mozilla Rhino: + Rhino: JavaScript in Java +
  • +
  • + Rhino Sandbox: + A sandbox to execute JavaScript code with Rhino in Java. +
  • +
  • + GuardRails: + Code Injection +
  • +
    +
    \ No newline at end of file diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.ql new file mode 100644 index 00000000000..cac41244aa2 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.ql @@ -0,0 +1,17 @@ +/** + * @name Injection in Mozilla Rhino JavaScript Engine + * @description Evaluation of a user-controlled JavaScript or Java expression in Rhino + * JavaScript Engine may lead to remote code execution. + * @kind path-problem + * @id java/rhino-injection + * @tags security + * external/cwe/cwe-094 + */ + +import java +import RhinoInjection +import DataFlow::PathGraph + +from DataFlow::PathNode source, DataFlow::PathNode sink, RhinoInjectionConfig conf +where conf.hasFlowPath(source, sink) +select sink.getNode(), source, sink, "Rhino injection from $@.", source.getNode(), " user input" diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.qll b/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.qll new file mode 100644 index 00000000000..a560e340e85 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.qll @@ -0,0 +1,56 @@ +import java +import semmle.code.java.dataflow.FlowSources +import semmle.code.java.dataflow.TaintTracking + +/** The class `org.mozilla.javascript.Context`. */ +class Context extends RefType { + Context() { this.hasQualifiedName("org.mozilla.javascript", "Context") } +} + +/** + * A method that evaluates a Rhino expression. + */ +class EvaluateExpressionMethod extends Method { + EvaluateExpressionMethod() { + this.getDeclaringType().getAnAncestor*() instanceof Context and + ( + hasName("evaluateString") or + hasName("evaluateReader") + ) + } +} + +/** + * A taint-tracking configuration for unsafe user input that is used to evaluate + * a Rhino expression. + */ +class RhinoInjectionConfig extends TaintTracking::Configuration { + RhinoInjectionConfig() { this = "RhinoInjectionConfig" } + + override predicate isSource(DataFlow::Node source) { + source instanceof RemoteFlowSource + or + source instanceof LocalUserInput + } + + override predicate isSink(DataFlow::Node sink) { sink instanceof EvaluateExpressionSink } +} + +/** + * A sink for Rhino code injection vulnerabilities. + */ +class EvaluateExpressionSink extends DataFlow::ExprNode { + EvaluateExpressionSink() { + exists(MethodAccess ea, EvaluateExpressionMethod m | m = ea.getMethod() | + this.asExpr() = ea.getArgument(1) and // The second argument is the JavaScript or Java input + not exists(MethodAccess ca | + ( + ca.getMethod().hasName("initSafeStandardObjects") // safe mode + or + ca.getMethod().hasName("setClassShutter") // `ClassShutter` constraint is enforced + ) and + ea.getQualifier() = ca.getQualifier().(VarAccess).getVariable().getAnAccess() + ) + ) + } +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/RhinoInjection.expected b/java/ql/test/experimental/query-tests/security/CWE-094/RhinoInjection.expected new file mode 100644 index 00000000000..4d2736c8230 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-094/RhinoInjection.expected @@ -0,0 +1,7 @@ +edges +| RhinoServlet.java:25:23:25:50 | getParameter(...) : String | RhinoServlet.java:29:55:29:58 | code | +nodes +| RhinoServlet.java:25:23:25:50 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| RhinoServlet.java:29:55:29:58 | code | semmle.label | code | +#select +| RhinoServlet.java:29:55:29:58 | code | RhinoServlet.java:25:23:25:50 | getParameter(...) : String | RhinoServlet.java:29:55:29:58 | code | Rhino injection from $@. | RhinoServlet.java:25:23:25:50 | getParameter(...) | user input | diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/RhinoInjection.qlref b/java/ql/test/experimental/query-tests/security/CWE-094/RhinoInjection.qlref new file mode 100644 index 00000000000..e306dda44df --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-094/RhinoInjection.qlref @@ -0,0 +1 @@ +experimental/Security/CWE/CWE-094/RhinoInjection.ql \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/RhinoServlet.java b/java/ql/test/experimental/query-tests/security/CWE-094/RhinoServlet.java new file mode 100644 index 00000000000..f6f529785cc --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-094/RhinoServlet.java @@ -0,0 +1,78 @@ +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.mozilla.javascript.ClassShutter; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.RhinoException; + +/** + * Servlet implementation class RhinoServlet + */ +public class RhinoServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + public RhinoServlet() { + super(); + } + + // BAD: allow arbitrary Java and JavaScript code to be executed + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/plain"); + String code = request.getParameter("code"); + Context ctx = Context.enter(); + try { + Scriptable scope = ctx.initStandardObjects(); + Object result = ctx.evaluateString(scope, code, "", 1, null); + response.getWriter().print(Context.toString(result)); + } catch(RhinoException ex) { + response.getWriter().println(ex.getMessage()); + } finally { + Context.exit(); + } + } + + // GOOD: enable the safe mode + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/plain"); + String code = request.getParameter("code"); + Context ctx = Context.enter(); + try { + Scriptable scope = ctx.initSafeStandardObjects(); + Object result = ctx.evaluateString(scope, code, "", 1, null); + response.getWriter().print(Context.toString(result)); + } catch(RhinoException ex) { + response.getWriter().println(ex.getMessage()); + } finally { + Context.exit(); + } + } + + // GOOD: enforce a constraint on allowed classes + protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/plain"); + String code = request.getParameter("code"); + Context ctx = Context.enter(); + try { + Scriptable scope = ctx.initStandardObjects(); + ctx.setClassShutter(new ClassShutter() { + public boolean visibleToScripts(String className) { + if(className.startsWith("com.example.")) { + return true; + } + return false; + } + }); + + Object result = ctx.evaluateString(scope, code, "", 1, null); + response.getWriter().print(Context.toString(result)); + } catch(RhinoException ex) { + response.getWriter().println(ex.getMessage()); + } finally { + Context.exit(); + } + } +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/options b/java/ql/test/experimental/query-tests/security/CWE-094/options index 95bc9acaa08..48cc00e0a17 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-094/options +++ b/java/ql/test/experimental/query-tests/security/CWE-094/options @@ -1,2 +1,2 @@ -//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/springframework-5.2.3:${testdir}/../../../../stubs/mvel2-2.4.7:${testdir}/../../../../stubs/jsr223-api:${testdir}/../../../../stubs/apache-commons-jexl-2.1.1:${testdir}/../../../../stubs/apache-commons-jexl-3.1:${testdir}/../../../../stubs/scriptengine:${testdir}/../../../../stubs/java-ee-el:${testdir}/../../../../stubs/juel-2.2:${testdir}/../../../stubs/groovy-all-3.0.7:${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/jython-2.7.2 +//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/springframework-5.2.3:${testdir}/../../../../stubs/mvel2-2.4.7:${testdir}/../../../../stubs/jsr223-api:${testdir}/../../../../stubs/apache-commons-jexl-2.1.1:${testdir}/../../../../stubs/apache-commons-jexl-3.1:${testdir}/../../../../stubs/scriptengine:${testdir}/../../../../stubs/java-ee-el:${testdir}/../../../../stubs/juel-2.2:${testdir}/../../../stubs/groovy-all-3.0.7:${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/jython-2.7.2:${testdir}/../../../../experimental/stubs/rhino-1.7.13 diff --git a/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/ClassShutter.java b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/ClassShutter.java new file mode 100644 index 00000000000..f425e08e966 --- /dev/null +++ b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/ClassShutter.java @@ -0,0 +1,56 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// API class + +package org.mozilla.javascript; + +/** +Embeddings that wish to filter Java classes that are visible to scripts +through the LiveConnect, should implement this interface. + +@see Context#setClassShutter(ClassShutter) +@since 1.5 Release 4 +@author Norris Boyd +*/ + + public interface ClassShutter { + + /** + * Return true iff the Java class with the given name should be exposed + * to scripts. + *

    + * An embedding may filter which Java classes are exposed through + * LiveConnect to JavaScript scripts. + *

    + * Due to the fact that there is no package reflection in Java, + * this method will also be called with package names. There + * is no way for Rhino to tell if "Packages.a.b" is a package name + * or a class that doesn't exist. What Rhino does is attempt + * to load each segment of "Packages.a.b.c": It first attempts to + * load class "a", then attempts to load class "a.b", then + * finally attempts to load class "a.b.c". On a Rhino installation + * without any ClassShutter set, and without any of the + * above classes, the expression "Packages.a.b.c" will result in + * a [JavaPackage a.b.c] and not an error. + *

    + * With ClassShutter supplied, Rhino will first call + * visibleToScripts before attempting to look up the class name. If + * visibleToScripts returns false, the class name lookup is not + * performed and subsequent Rhino execution assumes the class is + * not present. So for "java.lang.System.out.println" the lookup + * of "java.lang.System" is skipped and thus Rhino assumes that + * "java.lang.System" doesn't exist. So then for "java.lang.System.out", + * Rhino attempts to load the class "java.lang.System.out" because + * it assumes that "java.lang.System" is a package name. + *

    + * @param fullClassName the full name of the class (including the package + * name, with '.' as a delimiter). For example the + * standard string class is "java.lang.String" + * @return whether or not to reveal this class to scripts + */ + public boolean visibleToScripts(String fullClassName); +} diff --git a/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/Context.java b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/Context.java new file mode 100644 index 00000000000..57d30a3ab25 --- /dev/null +++ b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/Context.java @@ -0,0 +1,623 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// API class + +package org.mozilla.javascript; + +import java.io.Closeable; +import java.io.IOException; +import java.io.Reader; +import java.util.Locale; + +/** + * This class represents the runtime context of an executing script. + * + * Before executing a script, an instance of Context must be created + * and associated with the thread that will be executing the script. + * The Context will be used to store information about the executing + * of the script such as the call stack. Contexts are associated with + * the current thread using the {@link #call(ContextAction)} + * or {@link #enter()} methods.

    + * + * Different forms of script execution are supported. Scripts may be + * evaluated from the source directly, or first compiled and then later + * executed. Interactive execution is also supported.

    + * + * Some aspects of script execution, such as type conversions and + * object creation, may be accessed directly through methods of + * Context. + * + * @see Scriptable + * @author Norris Boyd + * @author Brendan Eich + */ + +public class Context + implements Closeable +{ + /** + * Creates a new Context. The context will be associated with the {@link + * ContextFactory#getGlobal() global context factory}. + * + * Note that the Context must be associated with a thread before + * it can be used to execute a script. + * @deprecated this constructor is deprecated because it creates a + * dependency on a static singleton context factory. Use + * {@link ContextFactory#enter()} or + * {@link ContextFactory#call(ContextAction)} instead. If you subclass + * this class, consider using {@link #Context(ContextFactory)} constructor + * instead in the subclasses' constructors. + */ + @Deprecated + public Context() + { + } + + /** + * Creates a new context. Provided as a preferred super constructor for + * subclasses in place of the deprecated default public constructor. + * @param factory the context factory associated with this context (most + * likely, the one that created the context). Can not be null. The context + * features are inherited from the factory, and the context will also + * otherwise use its factory's services. + * @throws IllegalArgumentException if factory parameter is null. + */ + protected Context(ContextFactory factory) + { + } + + /** + * Get the current Context. + * + * The current Context is per-thread; this method looks up + * the Context associated with the current thread.

    + * + * @return the Context associated with the current thread, or + * null if no context is associated with the current + * thread. + * @see ContextFactory#enterContext() + * @see ContextFactory#call(ContextAction) + */ + public static Context getCurrentContext() + { + return null; + } + + /** + * Same as calling {@link ContextFactory#enterContext()} on the global + * ContextFactory instance. + * @return a Context associated with the current thread + * @see #getCurrentContext() + * @see #exit() + * @see #call(ContextAction) + */ + public static Context enter() + { + return null; + } + + /** + * Get a Context associated with the current thread, using + * the given Context if need be. + *

    + * The same as enter() except that cx + * is associated with the current thread and returned if + * the current thread has no associated context and cx + * is not associated with any other thread. + * @param cx a Context to associate with the thread if possible + * @return a Context associated with the current thread + * @deprecated use {@link ContextFactory#enterContext(Context)} instead as + * this method relies on usage of a static singleton "global" ContextFactory. + * @see ContextFactory#enterContext(Context) + * @see ContextFactory#call(ContextAction) + */ + @Deprecated + public static Context enter(Context cx) + { + return null; + } + + static final Context enter(Context cx, ContextFactory factory) + { + return null; + } + + /** + * Exit a block of code requiring a Context. + * + * Calling exit() will remove the association between + * the current thread and a Context if the prior call to + * {@link ContextFactory#enterContext()} on this thread newly associated a + * Context with this thread. Once the current thread no longer has an + * associated Context, it cannot be used to execute JavaScript until it is + * again associated with a Context. + * @see ContextFactory#enterContext() + */ + public static void exit() + { + } + + @Override + public void close() { + } + + /** + * Return {@link ContextFactory} instance used to create this Context. + */ + public final ContextFactory getFactory() + { + return null; + } + + /** + * Checks if this is a sealed Context. A sealed Context instance does not + * allow to modify any of its properties and will throw an exception + * on any such attempt. + * @see #seal(Object sealKey) + */ + public final boolean isSealed() + { + return false; + } + + /** + * Seal this Context object so any attempt to modify any of its properties + * including calling {@link #enter()} and {@link #exit()} methods will + * throw an exception. + *

    + * If sealKey is not null, calling + * {@link #unseal(Object sealKey)} with the same key unseals + * the object. If sealKey is null, unsealing is no longer possible. + * + * @see #isSealed() + * @see #unseal(Object) + */ + public final void seal(Object sealKey) + { + } + + /** + * Unseal previously sealed Context object. + * The sealKey argument should not be null and should match + * sealKey suplied with the last call to + * {@link #seal(Object)} or an exception will be thrown. + * + * @see #isSealed() + * @see #seal(Object sealKey) + */ + public final void unseal(Object sealKey) + { + } + + /** + * Get the current language version. + *

    + * The language version number affects JavaScript semantics as detailed + * in the overview documentation. + * + * @return an integer that is one of VERSION_1_0, VERSION_1_1, etc. + */ + public final int getLanguageVersion() + { + return -1; + } + + /** + * Set the language version. + * + *

    + * Setting the language version will affect functions and scripts compiled + * subsequently. See the overview documentation for version-specific + * behavior. + * + * @param version the version as specified by VERSION_1_0, VERSION_1_1, etc. + */ + public void setLanguageVersion(int version) + { + } + + public static boolean isValidLanguageVersion(int version) + { + return false; + } + + public static void checkLanguageVersion(int version) + { + } + + /** + * Get the implementation version. + * + *

    + * The implementation version is of the form + *

    +     *    "name langVer release relNum date"
    +     * 
    + * where name is the name of the product, langVer is + * the language version, relNum is the release number, and + * date is the release date for that specific + * release in the form "yyyy mm dd". + * + * @return a string that encodes the product, language version, release + * number, and date. + */ + public final String getImplementationVersion() { + return null; + } + + /** + * Initialize the standard objects. + * + * Creates instances of the standard objects and their constructors + * (Object, String, Number, Date, etc.), setting up 'scope' to act + * as a global object as in ECMA 15.1.

    + * + * This method must be called to initialize a scope before scripts + * can be evaluated in that scope.

    + * + * This method does not affect the Context it is called upon. + * + * @return the initialized scope + */ + public final ScriptableObject initStandardObjects() + { + return null; + } + + /** + * Initialize the standard objects, leaving out those that offer access directly + * to Java classes. This sets up "scope" to have access to all the standard + * JavaScript classes, but does not create global objects for any top-level + * Java packages. In addition, the "Packages," "JavaAdapter," and + * "JavaImporter" classes, and the "getClass" function, are not + * initialized. + * + * The result of this function is a scope that may be safely used in a "sandbox" + * environment where it is not desirable to give access to Java code from JavaScript. + * + * Creates instances of the standard objects and their constructors + * (Object, String, Number, Date, etc.), setting up 'scope' to act + * as a global object as in ECMA 15.1.

    + * + * This method must be called to initialize a scope before scripts + * can be evaluated in that scope.

    + * + * This method does not affect the Context it is called upon. + * + * @return the initialized scope + */ + public final ScriptableObject initSafeStandardObjects() + { + return null; + } + + /** + * Initialize the standard objects. + * + * Creates instances of the standard objects and their constructors + * (Object, String, Number, Date, etc.), setting up 'scope' to act + * as a global object as in ECMA 15.1.

    + * + * This method must be called to initialize a scope before scripts + * can be evaluated in that scope.

    + * + * This method does not affect the Context it is called upon. + * + * @param scope the scope to initialize, or null, in which case a new + * object will be created to serve as the scope + * @return the initialized scope. The method returns the value of the scope + * argument if it is not null or newly allocated scope object which + * is an instance {@link ScriptableObject}. + */ + public final Scriptable initStandardObjects(ScriptableObject scope) + { + return null; + } + + /** + * Initialize the standard objects, leaving out those that offer access directly + * to Java classes. This sets up "scope" to have access to all the standard + * JavaScript classes, but does not create global objects for any top-level + * Java packages. In addition, the "Packages," "JavaAdapter," and + * "JavaImporter" classes, and the "getClass" function, are not + * initialized. + * + * The result of this function is a scope that may be safely used in a "sandbox" + * environment where it is not desirable to give access to Java code from JavaScript. + * + * Creates instances of the standard objects and their constructors + * (Object, String, Number, Date, etc.), setting up 'scope' to act + * as a global object as in ECMA 15.1.

    + * + * This method must be called to initialize a scope before scripts + * can be evaluated in that scope.

    + * + * This method does not affect the Context it is called upon. + * + * @param scope the scope to initialize, or null, in which case a new + * object will be created to serve as the scope + * @return the initialized scope. The method returns the value of the scope + * argument if it is not null or newly allocated scope object which + * is an instance {@link ScriptableObject}. + */ + public final Scriptable initSafeStandardObjects(ScriptableObject scope) + { + return null; + } + + /** + * Initialize the standard objects. + * + * Creates instances of the standard objects and their constructors + * (Object, String, Number, Date, etc.), setting up 'scope' to act + * as a global object as in ECMA 15.1.

    + * + * This method must be called to initialize a scope before scripts + * can be evaluated in that scope.

    + * + * This method does not affect the Context it is called upon.

    + * + * This form of the method also allows for creating "sealed" standard + * objects. An object that is sealed cannot have properties added, changed, + * or removed. This is useful to create a "superglobal" that can be shared + * among several top-level objects. Note that sealing is not allowed in + * the current ECMA/ISO language specification, but is likely for + * the next version. + * + * @param scope the scope to initialize, or null, in which case a new + * object will be created to serve as the scope + * @param sealed whether or not to create sealed standard objects that + * cannot be modified. + * @return the initialized scope. The method returns the value of the scope + * argument if it is not null or newly allocated scope object. + * @since 1.4R3 + */ + public ScriptableObject initStandardObjects(ScriptableObject scope, + boolean sealed) + { + return null; + } + + /** + * Initialize the standard objects, leaving out those that offer access directly + * to Java classes. This sets up "scope" to have access to all the standard + * JavaScript classes, but does not create global objects for any top-level + * Java packages. In addition, the "Packages," "JavaAdapter," and + * "JavaImporter" classes, and the "getClass" function, are not + * initialized. + * + * The result of this function is a scope that may be safely used in a "sandbox" + * environment where it is not desirable to give access to Java code from JavaScript. + * + * Creates instances of the standard objects and their constructors + * (Object, String, Number, Date, etc.), setting up 'scope' to act + * as a global object as in ECMA 15.1.

    + * + * This method must be called to initialize a scope before scripts + * can be evaluated in that scope.

    + * + * This method does not affect the Context it is called upon.

    + * + * This form of the method also allows for creating "sealed" standard + * objects. An object that is sealed cannot have properties added, changed, + * or removed. This is useful to create a "superglobal" that can be shared + * among several top-level objects. Note that sealing is not allowed in + * the current ECMA/ISO language specification, but is likely for + * the next version. + * + * @param scope the scope to initialize, or null, in which case a new + * object will be created to serve as the scope + * @param sealed whether or not to create sealed standard objects that + * cannot be modified. + * @return the initialized scope. The method returns the value of the scope + * argument if it is not null or newly allocated scope object. + * @since 1.7.6 + */ + public ScriptableObject initSafeStandardObjects(ScriptableObject scope, + boolean sealed) + { + return null; + } + + /** + * Get the singleton object that represents the JavaScript Undefined value. + */ + public static Object getUndefinedValue() + { + return null; + } + + /** + * Evaluate a JavaScript source string. + * + * The provided source name and line number are used for error messages + * and for producing debug information. + * + * @param scope the scope to execute in + * @param source the JavaScript source + * @param sourceName a string describing the source, such as a filename + * @param lineno the starting line number + * @param securityDomain an arbitrary object that specifies security + * information about the origin or owner of the script. For + * implementations that don't care about security, this value + * may be null. + * @return the result of evaluating the string + * @see org.mozilla.javascript.SecurityController + */ + public final Object evaluateString(Scriptable scope, String source, + String sourceName, int lineno, + Object securityDomain) + { + return null; + } + + /** + * Evaluate a reader as JavaScript source. + * + * All characters of the reader are consumed. + * + * @param scope the scope to execute in + * @param in the Reader to get JavaScript source from + * @param sourceName a string describing the source, such as a filename + * @param lineno the starting line number + * @param securityDomain an arbitrary object that specifies security + * information about the origin or owner of the script. For + * implementations that don't care about security, this value + * may be null. + * @return the result of evaluating the source + * + * @exception IOException if an IOException was generated by the Reader + */ + public final Object evaluateReader(Scriptable scope, Reader in, + String sourceName, int lineno, + Object securityDomain) + throws IOException + { + return null; + } + + /** + * Convert the value to a JavaScript boolean value. + *

    + * See ECMA 9.2. + * + * @param value a JavaScript value + * @return the corresponding boolean value converted using + * the ECMA rules + */ + public static boolean toBoolean(Object value) + { + return false; + } + + /** + * Convert the value to a JavaScript Number value. + *

    + * Returns a Java double for the JavaScript Number. + *

    + * See ECMA 9.3. + * + * @param value a JavaScript value + * @return the corresponding double value converted using + * the ECMA rules + */ + public static double toNumber(Object value) + { + return -1; + } + + /** + * Convert the value to a JavaScript String value. + *

    + * See ECMA 9.8. + *

    + * @param value a JavaScript value + * @return the corresponding String value converted using + * the ECMA rules + */ + public static String toString(Object value) + { + return null; + } + + /** + * Convert the value to an JavaScript object value. + *

    + * Note that a scope must be provided to look up the constructors + * for Number, Boolean, and String. + *

    + * See ECMA 9.9. + *

    + * Additionally, arbitrary Java objects and classes will be + * wrapped in a Scriptable object with its Java fields and methods + * reflected as JavaScript properties of the object. + * + * @param value any Java object + * @param scope global scope containing constructors for Number, + * Boolean, and String + * @return new JavaScript object + */ + public static Scriptable toObject(Object value, Scriptable scope) + { + return null; + } + + /** + * Convenient method to convert java value to its closest representation + * in JavaScript. + *

    + * If value is an instance of String, Number, Boolean, Function or + * Scriptable, it is returned as it and will be treated as the corresponding + * JavaScript type of string, number, boolean, function and object. + *

    + * Note that for Number instances during any arithmetic operation in + * JavaScript the engine will always use the result of + * Number.doubleValue() resulting in a precision loss if + * the number can not fit into double. + *

    + * If value is an instance of Character, it will be converted to string of + * length 1 and its JavaScript type will be string. + *

    + * The rest of values will be wrapped as LiveConnect objects + * by calling {@link WrapFactory#wrap(Context cx, Scriptable scope, + * Object obj, Class staticType)} as in: + *

    +     *    Context cx = Context.getCurrentContext();
    +     *    return cx.getWrapFactory().wrap(cx, scope, value, null);
    +     * 
    + * + * @param value any Java object + * @param scope top scope object + * @return value suitable to pass to any API that takes JavaScript values. + */ + public static Object javaToJS(Object value, Scriptable scope) + { + return null; + } + + /** + * Convert a JavaScript value into the desired type. + * Uses the semantics defined with LiveConnect3 and throws an + * Illegal argument exception if the conversion cannot be performed. + * @param value the JavaScript value to convert + * @param desiredType the Java type to convert to. Primitive Java + * types are represented using the TYPE fields in the corresponding + * wrapper class in java.lang. + * @return the converted value + * @throws EvaluatorException if the conversion cannot be performed + */ + public static Object jsToJava(Object value, Class desiredType) + { + return null; + } + + /** + * Set the LiveConnect access filter for this context. + *

    {@link ClassShutter} may only be set if it is currently null. + * Otherwise a SecurityException is thrown. + * @param shutter a ClassShutter object + * @throws SecurityException if there is already a ClassShutter + * object for this Context + */ + public synchronized final void setClassShutter(ClassShutter shutter) + { + } + + final synchronized ClassShutter getClassShutter() + { + return null; + } + + public interface ClassShutterSetter { + public void setClassShutter(ClassShutter shutter); + public ClassShutter getClassShutter(); + } + + public final synchronized ClassShutterSetter getClassShutterSetter() { + return null; + } +} diff --git a/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/ContextFactory.java b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/ContextFactory.java new file mode 100644 index 00000000000..a7f83f2095d --- /dev/null +++ b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/ContextFactory.java @@ -0,0 +1,314 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// API class + +package org.mozilla.javascript; + +/** + * Factory class that Rhino runtime uses to create new {@link Context} + * instances. A ContextFactory can also notify listeners + * about context creation and release. + *

    + * When the Rhino runtime needs to create new {@link Context} instance during + * execution of {@link Context#enter()} or {@link Context}, it will call + * {@link #makeContext()} of the current global ContextFactory. + * See {@link #getGlobal()} and {@link #initGlobal(ContextFactory)}. + *

    + * It is also possible to use explicit ContextFactory instances for Context + * creation. This is useful to have a set of independent Rhino runtime + * instances under single JVM. See {@link #call(ContextAction)}. + *

    + * The following example demonstrates Context customization to terminate + * scripts running more then 10 seconds and to provide better compatibility + * with JavaScript code using MSIE-specific features. + *

    + * import org.mozilla.javascript.*;
    + *
    + * class MyFactory extends ContextFactory
    + * {
    + *
    + *     // Custom {@link Context} to store execution time.
    + *     private static class MyContext extends Context
    + *     {
    + *         long startTime;
    + *     }
    + *
    + *     static {
    + *         // Initialize GlobalFactory with custom factory
    + *         ContextFactory.initGlobal(new MyFactory());
    + *     }
    + *
    + *     // Override {@link #makeContext()}
    + *     protected Context makeContext()
    + *     {
    + *         MyContext cx = new MyContext();
    + *         // Make Rhino runtime to call observeInstructionCount
    + *         // each 10000 bytecode instructions
    + *         cx.setInstructionObserverThreshold(10000);
    + *         return cx;
    + *     }
    + *
    + *     // Override {@link #hasFeature(Context, int)}
    + *     public boolean hasFeature(Context cx, int featureIndex)
    + *     {
    + *         // Turn on maximum compatibility with MSIE scripts
    + *         switch (featureIndex) {
    + *             case {@link Context#FEATURE_NON_ECMA_GET_YEAR}:
    + *                 return true;
    + *
    + *             case {@link Context#FEATURE_MEMBER_EXPR_AS_FUNCTION_NAME}:
    + *                 return true;
    + *
    + *             case {@link Context#FEATURE_RESERVED_KEYWORD_AS_IDENTIFIER}:
    + *                 return true;
    + *
    + *             case {@link Context#FEATURE_PARENT_PROTO_PROPERTIES}:
    + *                 return false;
    + *         }
    + *         return super.hasFeature(cx, featureIndex);
    + *     }
    + *
    + *     // Override {@link #observeInstructionCount(Context, int)}
    + *     protected void observeInstructionCount(Context cx, int instructionCount)
    + *     {
    + *         MyContext mcx = (MyContext)cx;
    + *         long currentTime = System.currentTimeMillis();
    + *         if (currentTime - mcx.startTime > 10*1000) {
    + *             // More then 10 seconds from Context creation time:
    + *             // it is time to stop the script.
    + *             // Throw Error instance to ensure that script will never
    + *             // get control back through catch or finally.
    + *             throw new Error();
    + *         }
    + *     }
    + *
    + *     // Override {@link #doTopCall(Callable,
    +                               Context, Scriptable,
    +                               Scriptable, Object[])}
    + *     protected Object doTopCall(Callable callable,
    + *                                Context cx, Scriptable scope,
    + *                                Scriptable thisObj, Object[] args)
    + *     {
    + *         MyContext mcx = (MyContext)cx;
    + *         mcx.startTime = System.currentTimeMillis();
    + *
    + *         return super.doTopCall(callable, cx, scope, thisObj, args);
    + *     }
    + *
    + * }
    + * 
    + */ + +public class ContextFactory +{ + + /** + * Listener of {@link Context} creation and release events. + */ + public interface Listener + { + /** + * Notify about newly created {@link Context} object. + */ + public void contextCreated(Context cx); + + /** + * Notify that the specified {@link Context} instance is no longer + * associated with the current thread. + */ + public void contextReleased(Context cx); + } + + /** + * Get global ContextFactory. + * + * @see #hasExplicitGlobal() + * @see #initGlobal(ContextFactory) + */ + public static ContextFactory getGlobal() + { + return null; + } + + /** + * Check if global factory was set. + * Return true to indicate that {@link #initGlobal(ContextFactory)} was + * already called and false to indicate that the global factory was not + * explicitly set. + * + * @see #getGlobal() + * @see #initGlobal(ContextFactory) + */ + public static boolean hasExplicitGlobal() + { + return false; + } + + /** + * Set global ContextFactory. + * The method can only be called once. + * + * @see #getGlobal() + * @see #hasExplicitGlobal() + */ + public synchronized static void initGlobal(ContextFactory factory) + { + } + + public interface GlobalSetter { + public void setContextFactoryGlobal(ContextFactory factory); + public ContextFactory getContextFactoryGlobal(); + } + + public synchronized static GlobalSetter getGlobalSetter() { + return null; + } + + /** + * Create new {@link Context} instance to be associated with the current + * thread. + * This is a callback method used by Rhino to create {@link Context} + * instance when it is necessary to associate one with the current + * execution thread. makeContext() is allowed to call + * {@link Context#seal(Object)} on the result to prevent + * {@link Context} changes by hostile scripts or applets. + */ + protected Context makeContext() + { + return null; + } + + /** + * Implementation of {@link Context#hasFeature(int featureIndex)}. + * This can be used to customize {@link Context} without introducing + * additional subclasses. + */ + protected boolean hasFeature(Context cx, int featureIndex) + { + return false; + } + + /** + * Get ClassLoader to use when searching for Java classes. + * Unless it was explicitly initialized with + * {@link #initApplicationClassLoader(ClassLoader)} the method returns + * null to indicate that Thread.getContextClassLoader() should be used. + */ + public final ClassLoader getApplicationClassLoader() + { + return null; + } + + /** + * Set explicit class loader to use when searching for Java classes. + * + * @see #getApplicationClassLoader() + */ + public final void initApplicationClassLoader(ClassLoader loader) + { + } + + /** + * Checks if this is a sealed ContextFactory. + * @see #seal() + */ + public final boolean isSealed() + { + return false; + } + + /** + * Seal this ContextFactory so any attempt to modify it like to add or + * remove its listeners will throw an exception. + * @see #isSealed() + */ + public final void seal() + { + } + + /** + * Get a context associated with the current thread, creating one if need + * be. The Context stores the execution state of the JavaScript engine, so + * it is required that the context be entered before execution may begin. + * Once a thread has entered a Context, then getCurrentContext() may be + * called to find the context that is associated with the current thread. + *

    + * Calling enterContext() will return either the Context + * currently associated with the thread, or will create a new context and + * associate it with the current thread. Each call to + * enterContext() must have a matching call to + * {@link Context#exit()}. + *

    +     *      Context cx = contextFactory.enterContext();
    +     *      try {
    +     *          ...
    +     *          cx.evaluateString(...);
    +     *      } finally {
    +     *          Context.exit();
    +     *      }
    +     * 
    + * Instead of using enterContext(), exit() pair consider + * using {@link #call(ContextAction)} which guarantees proper association + * of Context instances with the current thread. + * With this method the above example becomes: + *
    +     *      ContextFactory.call(new ContextAction() {
    +     *          public Object run(Context cx) {
    +     *              ...
    +     *              cx.evaluateString(...);
    +     *              return null;
    +     *          }
    +     *      });
    +     * 
    + * @return a Context associated with the current thread + * @see Context#getCurrentContext() + * @see Context#exit() + * @see #call(ContextAction) + */ + public Context enterContext() + { + return null; + } + + /** + * @deprecated use {@link #enterContext()} instead + * @return a Context associated with the current thread + */ + @Deprecated + public final Context enter() + { + return null; + } + + /** + * @deprecated Use {@link Context#exit()} instead. + */ + @Deprecated + public final void exit() + { + } + + /** + * Get a Context associated with the current thread, using the given + * Context if need be. + *

    + * The same as enterContext() except that cx + * is associated with the current thread and returned if the current thread + * has no associated context and cx is not associated with any + * other thread. + * @param cx a Context to associate with the thread if possible + * @return a Context associated with the current thread + * @see #enterContext() + * @see #call(ContextAction) + * @throws IllegalStateException if cx is already associated + * with a different thread + */ + public final Context enterContext(Context cx) + { + return null; + } +} diff --git a/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/RhinoException.java b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/RhinoException.java new file mode 100644 index 00000000000..b11befb4a63 --- /dev/null +++ b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/RhinoException.java @@ -0,0 +1,15 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +package org.mozilla.javascript; + +/** + * The class of exceptions thrown by the JavaScript engine. + */ +public abstract class RhinoException extends RuntimeException +{ +} diff --git a/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/Scriptable.java b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/Scriptable.java new file mode 100644 index 00000000000..34616f7ad74 --- /dev/null +++ b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/Scriptable.java @@ -0,0 +1,304 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// API class + +package org.mozilla.javascript; + +/** + * This is interface that all objects in JavaScript must implement. + * The interface provides for the management of properties and for + * performing conversions. + *

    + * Host system implementors may find it easier to extend the ScriptableObject + * class rather than implementing Scriptable when writing host objects. + *

    + * There are many static methods defined in ScriptableObject that perform + * the multiple calls to the Scriptable interface needed in order to + * manipulate properties in prototype chains. + *

    + * + * @see org.mozilla.javascript.ScriptableObject + * @author Norris Boyd + * @author Nick Thompson + * @author Brendan Eich + */ + +public interface Scriptable { + + /** + * Get the name of the set of objects implemented by this Java class. + * This corresponds to the [[Class]] operation in ECMA and is used + * by Object.prototype.toString() in ECMA.

    + * See ECMA 8.6.2 and 15.2.4.2. + */ + public String getClassName(); + + /** + * Get a named property from the object. + * + * Looks property up in this object and returns the associated value + * if found. Returns NOT_FOUND if not found. + * Note that this method is not expected to traverse the prototype + * chain. This is different from the ECMA [[Get]] operation. + * + * Depending on the property selector, the runtime will call + * this method or the form of get that takes an + * integer: + * + * + * + * + * + * + * + * + * + * + *
    JavaScript codeJava code
    a.b a.get("b", a)
    a["foo"] a.get("foo", a)
    a[3] a.get(3, a)
    a["3"] a.get(3, a)
    a[3.0] a.get(3, a)
    a["3.0"] a.get("3.0", a)
    a[1.1] a.get("1.1", a)
    a[-4] a.get(-4, a)
    + *

    + * The values that may be returned are limited to the following: + *

      + *
    • java.lang.Boolean objects
    • + *
    • java.lang.String objects
    • + *
    • java.lang.Number objects
    • + *
    • org.mozilla.javascript.Scriptable objects
    • + *
    • null
    • + *
    • The value returned by Context.getUndefinedValue()
    • + *
    • NOT_FOUND
    • + *
    + * @param name the name of the property + * @param start the object in which the lookup began + * @return the value of the property (may be null), or NOT_FOUND + * @see org.mozilla.javascript.Context#getUndefinedValue + */ + public Object get(String name, Scriptable start); + + /** + * Get a property from the object selected by an integral index. + * + * Identical to get(String, Scriptable) except that + * an integral index is used to select the property. + * + * @param index the numeric index for the property + * @param start the object in which the lookup began + * @return the value of the property (may be null), or NOT_FOUND + * @see org.mozilla.javascript.Scriptable#get(String,Scriptable) + */ + public Object get(int index, Scriptable start); + + /** + * Indicates whether or not a named property is defined in an object. + * + * Does not traverse the prototype chain.

    + * + * The property is specified by a String name + * as defined for the get method.

    + * + * @param name the name of the property + * @param start the object in which the lookup began + * @return true if and only if the named property is found in the object + * @see org.mozilla.javascript.Scriptable#get(String, Scriptable) + * @see org.mozilla.javascript.ScriptableObject#getProperty(Scriptable, String) + */ + public boolean has(String name, Scriptable start); + + /** + * Indicates whether or not an indexed property is defined in an object. + * + * Does not traverse the prototype chain.

    + * + * The property is specified by an integral index + * as defined for the get method.

    + * + * @param index the numeric index for the property + * @param start the object in which the lookup began + * @return true if and only if the indexed property is found in the object + * @see org.mozilla.javascript.Scriptable#get(int, Scriptable) + * @see org.mozilla.javascript.ScriptableObject#getProperty(Scriptable, int) + */ + public boolean has(int index, Scriptable start); + + /** + * Sets a named property in this object. + *

    + * The property is specified by a string name + * as defined for get. + *

    + * The possible values that may be passed in are as defined for + * get. A class that implements this method may choose + * to ignore calls to set certain properties, in which case those + * properties are effectively read-only.

    + * For properties defined in a prototype chain, + * use putProperty in ScriptableObject.

    + * Note that if a property a is defined in the prototype p + * of an object o, then evaluating o.a = 23 will cause + * set to be called on the prototype p with + * o as the start parameter. + * To preserve JavaScript semantics, it is the Scriptable + * object's responsibility to modify o.

    + * This design allows properties to be defined in prototypes and implemented + * in terms of getters and setters of Java values without consuming slots + * in each instance. + *

    + * The values that may be set are limited to the following: + *

      + *
    • java.lang.Boolean objects
    • + *
    • java.lang.String objects
    • + *
    • java.lang.Number objects
    • + *
    • org.mozilla.javascript.Scriptable objects
    • + *
    • null
    • + *
    • The value returned by Context.getUndefinedValue()
    • + *

    + * Arbitrary Java objects may be wrapped in a Scriptable by first calling + * Context.toObject. This allows the property of a JavaScript + * object to contain an arbitrary Java object as a value.

    + * Note that has will be called by the runtime first before + * set is called to determine in which object the + * property is defined. + * Note that this method is not expected to traverse the prototype chain, + * which is different from the ECMA [[Put]] operation. + * @param name the name of the property + * @param start the object whose property is being set + * @param value value to set the property to + * @see org.mozilla.javascript.Scriptable#has(String, Scriptable) + * @see org.mozilla.javascript.Scriptable#get(String, Scriptable) + * @see org.mozilla.javascript.ScriptableObject#putProperty(Scriptable, String, Object) + * @see org.mozilla.javascript.Context#toObject(Object, Scriptable) + */ + public void put(String name, Scriptable start, Object value); + + /** + * Sets an indexed property in this object. + *

    + * The property is specified by an integral index + * as defined for get.

    + * + * Identical to put(String, Scriptable, Object) except that + * an integral index is used to select the property. + * + * @param index the numeric index for the property + * @param start the object whose property is being set + * @param value value to set the property to + * @see org.mozilla.javascript.Scriptable#has(int, Scriptable) + * @see org.mozilla.javascript.Scriptable#get(int, Scriptable) + * @see org.mozilla.javascript.ScriptableObject#putProperty(Scriptable, int, Object) + * @see org.mozilla.javascript.Context#toObject(Object, Scriptable) + */ + public void put(int index, Scriptable start, Object value); + + /** + * Removes a property from this object. + * This operation corresponds to the ECMA [[Delete]] except that + * the no result is returned. The runtime will guarantee that this + * method is called only if the property exists. After this method + * is called, the runtime will call Scriptable.has to see if the + * property has been removed in order to determine the boolean + * result of the delete operator as defined by ECMA 11.4.1. + *

    + * A property can be made permanent by ignoring calls to remove + * it.

    + * The property is specified by a String name + * as defined for get. + *

    + * To delete properties defined in a prototype chain, + * see deleteProperty in ScriptableObject. + * @param name the identifier for the property + * @see org.mozilla.javascript.Scriptable#get(String, Scriptable) + * @see org.mozilla.javascript.ScriptableObject#deleteProperty(Scriptable, String) + */ + public void delete(String name); + + /** + * Removes a property from this object. + * + * The property is specified by an integral index + * as defined for get. + *

    + * To delete properties defined in a prototype chain, + * see deleteProperty in ScriptableObject. + * + * Identical to delete(String) except that + * an integral index is used to select the property. + * + * @param index the numeric index for the property + * @see org.mozilla.javascript.Scriptable#get(int, Scriptable) + * @see org.mozilla.javascript.ScriptableObject#deleteProperty(Scriptable, int) + */ + public void delete(int index); + + /** + * Get the prototype of the object. + * @return the prototype + */ + public Scriptable getPrototype(); + + /** + * Set the prototype of the object. + * @param prototype the prototype to set + */ + public void setPrototype(Scriptable prototype); + + /** + * Get the parent scope of the object. + * @return the parent scope + */ + public Scriptable getParentScope(); + + /** + * Set the parent scope of the object. + * @param parent the parent scope to set + */ + public void setParentScope(Scriptable parent); + + /** + * Get an array of property ids. + * + * Not all property ids need be returned. Those properties + * whose ids are not returned are considered non-enumerable. + * + * @return an array of Objects. Each entry in the array is either + * a java.lang.String or a java.lang.Number + */ + public Object[] getIds(); + + /** + * Get the default value of the object with a given hint. + * The hints are String.class for type String, Number.class for type + * Number, Scriptable.class for type Object, and Boolean.class for + * type Boolean.

    + * + * A hint of null means "no hint". + * + * See ECMA 8.6.2.6. + * + * @param hint the type hint + * @return the default value + */ + public Object getDefaultValue(Class hint); + + /** + * The instanceof operator. + * + *

    + * The JavaScript code "lhs instanceof rhs" causes rhs.hasInstance(lhs) to + * be called. + * + *

    + * The return value is implementation dependent so that embedded host objects can + * return an appropriate value. See the JS 1.3 language documentation for more + * detail. + * + *

    This operator corresponds to the proposed EMCA [[HasInstance]] operator. + * + * @param instance The value that appeared on the LHS of the instanceof + * operator + * + * @return an implementation dependent value + */ + public boolean hasInstance(Scriptable instance); +} + diff --git a/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/ScriptableObject.java b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/ScriptableObject.java new file mode 100644 index 00000000000..298c4fc7fb0 --- /dev/null +++ b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/ScriptableObject.java @@ -0,0 +1,27 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// API class + +package org.mozilla.javascript; + +/** + * This is the default implementation of the Scriptable interface. This + * class provides convenient default behavior that makes it easier to + * define host objects. + *

    + * Various properties and methods of JavaScript objects can be conveniently + * defined using methods of ScriptableObject. + *

    + * Classes extending ScriptableObject must define the getClassName method. + * + * @see org.mozilla.javascript.Scriptable + * @author Norris Boyd + */ + +public abstract class ScriptableObject implements Scriptable +{ +} From 852bcfb5c7b35d98c99b0f4539f928fb8385058c Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Fri, 30 Apr 2021 03:46:23 +0000 Subject: [PATCH 61/74] Refactor the ScriptEngine query and the Rhino code injection query into one --- .../Security/CWE/CWE-094/RhinoInjection.qhelp | 51 ---------- .../Security/CWE/CWE-094/RhinoInjection.ql | 17 ---- .../Security/CWE/CWE-094/RhinoInjection.qll | 56 ----------- .../Security/CWE/CWE-094/ScriptEngine.qhelp | 26 ------ .../Security/CWE/CWE-094/ScriptEngine.ql | 51 ---------- .../CWE/CWE-094/ScriptInjection.qhelp | 49 ++++++++++ .../Security/CWE/CWE-094/ScriptInjection.ql | 93 +++++++++++++++++++ .../security/CWE-094/RhinoInjection.expected | 7 -- .../security/CWE-094/RhinoInjection.qlref | 1 - .../security/CWE-094/ScriptEngine.qlref | 1 - ...gine.expected => ScriptInjection.expected} | 12 ++- .../security/CWE-094/ScriptInjection.qlref | 1 + 12 files changed, 151 insertions(+), 214 deletions(-) delete mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.qhelp delete mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.ql delete mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.qll delete mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/ScriptEngine.qhelp delete mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/ScriptEngine.ql create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.qhelp create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-094/RhinoInjection.expected delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-094/RhinoInjection.qlref delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-094/ScriptEngine.qlref rename java/ql/test/experimental/query-tests/security/CWE-094/{ScriptEngine.expected => ScriptInjection.expected} (75%) create mode 100644 java/ql/test/experimental/query-tests/security/CWE-094/ScriptInjection.qlref diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.qhelp b/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.qhelp deleted file mode 100644 index 73c252ed54c..00000000000 --- a/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.qhelp +++ /dev/null @@ -1,51 +0,0 @@ - - - - -

    -Rhino is a JavaScript engine written fully in Java and managed by the Mozilla Foundation. -It serves as an embedded scripting engine inside Java applications which allows -Java-to-JavaScript interoperability and provides a seamless integration between the two -languages. If an expression is built using attacker-controlled data, and then evaluated in -a powerful context, it may allow the attacker to run arbitrary code. -

    -

    -Typically an expression is evaluated in the powerful context initialized with -initStandardObjects that allows an expression of arbitrary Java code to -execute in the JVM. -

    - - - -

    -In general, including user input in a Rhino expression should be avoided. -If user input must be included in the expression, it should be then evaluated in a safe -context that doesn't allow arbitrary code invocation. -

    -
    - - -

    -The following example shows two ways of using Rhino expression. In the 'BAD' case, -an unsafe context is initialized with initStandardObjects. In the 'GOOD' case, -a safe context is initialized with initSafeStandardObjects or -setClassShutter. -

    - -
    - - -
  • - Mozilla Rhino: - Rhino: JavaScript in Java -
  • -
  • - Rhino Sandbox: - A sandbox to execute JavaScript code with Rhino in Java. -
  • -
  • - GuardRails: - Code Injection -
  • -
    - \ No newline at end of file diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.ql deleted file mode 100644 index cac41244aa2..00000000000 --- a/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.ql +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @name Injection in Mozilla Rhino JavaScript Engine - * @description Evaluation of a user-controlled JavaScript or Java expression in Rhino - * JavaScript Engine may lead to remote code execution. - * @kind path-problem - * @id java/rhino-injection - * @tags security - * external/cwe/cwe-094 - */ - -import java -import RhinoInjection -import DataFlow::PathGraph - -from DataFlow::PathNode source, DataFlow::PathNode sink, RhinoInjectionConfig conf -where conf.hasFlowPath(source, sink) -select sink.getNode(), source, sink, "Rhino injection from $@.", source.getNode(), " user input" diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.qll b/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.qll deleted file mode 100644 index a560e340e85..00000000000 --- a/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.qll +++ /dev/null @@ -1,56 +0,0 @@ -import java -import semmle.code.java.dataflow.FlowSources -import semmle.code.java.dataflow.TaintTracking - -/** The class `org.mozilla.javascript.Context`. */ -class Context extends RefType { - Context() { this.hasQualifiedName("org.mozilla.javascript", "Context") } -} - -/** - * A method that evaluates a Rhino expression. - */ -class EvaluateExpressionMethod extends Method { - EvaluateExpressionMethod() { - this.getDeclaringType().getAnAncestor*() instanceof Context and - ( - hasName("evaluateString") or - hasName("evaluateReader") - ) - } -} - -/** - * A taint-tracking configuration for unsafe user input that is used to evaluate - * a Rhino expression. - */ -class RhinoInjectionConfig extends TaintTracking::Configuration { - RhinoInjectionConfig() { this = "RhinoInjectionConfig" } - - override predicate isSource(DataFlow::Node source) { - source instanceof RemoteFlowSource - or - source instanceof LocalUserInput - } - - override predicate isSink(DataFlow::Node sink) { sink instanceof EvaluateExpressionSink } -} - -/** - * A sink for Rhino code injection vulnerabilities. - */ -class EvaluateExpressionSink extends DataFlow::ExprNode { - EvaluateExpressionSink() { - exists(MethodAccess ea, EvaluateExpressionMethod m | m = ea.getMethod() | - this.asExpr() = ea.getArgument(1) and // The second argument is the JavaScript or Java input - not exists(MethodAccess ca | - ( - ca.getMethod().hasName("initSafeStandardObjects") // safe mode - or - ca.getMethod().hasName("setClassShutter") // `ClassShutter` constraint is enforced - ) and - ea.getQualifier() = ca.getQualifier().(VarAccess).getVariable().getAnAccess() - ) - ) - } -} diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptEngine.qhelp b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptEngine.qhelp deleted file mode 100644 index 74159c562c5..00000000000 --- a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptEngine.qhelp +++ /dev/null @@ -1,26 +0,0 @@ - - - - -

    The ScriptEngine API has been available since the release of Java 6. -It allows applications to interact with scripts written in languages such as JavaScript.

    -
    - - -

    Use "Cloudbees Rhino Sandbox" or sandboxing with SecurityManager or use graalvm instead.

    -
    - - -

    The following code could execute random JavaScript code

    - - -
    - - -
  • -CERT coding standard: ScriptEngine code injection -
  • -
    -
    diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptEngine.ql b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptEngine.ql deleted file mode 100644 index 5e52a61b5c3..00000000000 --- a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptEngine.ql +++ /dev/null @@ -1,51 +0,0 @@ -/** - * @name ScriptEngine evaluation - * @description Malicious Javascript code could cause arbitrary command execution at the OS level - * @kind path-problem - * @problem.severity error - * @precision high - * @id java/unsafe-eval - * @tags security - * external/cwe/cwe-094 - */ - -import java -import semmle.code.java.dataflow.FlowSources -import DataFlow::PathGraph - -class ScriptEngineMethod extends Method { - ScriptEngineMethod() { - this.getDeclaringType().getASupertype*().hasQualifiedName("javax.script", "ScriptEngine") and - this.hasName("eval") - } -} - -predicate scriptEngine(MethodAccess ma, Expr sink) { - exists(Method m | m = ma.getMethod() | - m instanceof ScriptEngineMethod and - sink = ma.getArgument(0) - ) -} - -class ScriptEngineSink extends DataFlow::ExprNode { - ScriptEngineSink() { scriptEngine(_, this.getExpr()) } - - MethodAccess getMethodAccess() { scriptEngine(result, this.getExpr()) } -} - -class ScriptEngineConfiguration extends TaintTracking::Configuration { - ScriptEngineConfiguration() { this = "ScriptEngineConfiguration" } - - override predicate isSource(DataFlow::Node source) { - source instanceof RemoteFlowSource - or - source instanceof LocalUserInput - } - - override predicate isSink(DataFlow::Node sink) { sink instanceof ScriptEngineSink } -} - -from DataFlow::PathNode source, DataFlow::PathNode sink, ScriptEngineConfiguration conf -where conf.hasFlowPath(source, sink) -select sink.getNode().(ScriptEngineSink).getMethodAccess(), source, sink, "ScriptEngine eval $@.", - source.getNode(), "user input" diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.qhelp b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.qhelp new file mode 100644 index 00000000000..ddc011fa658 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.qhelp @@ -0,0 +1,49 @@ + + + + +

    The JavaScript Engine API has been available since the release of Java 6, which allows + applications to interact with scripts written in languages such as JavaScript. It serves + as an embedded scripting engine inside Java applications which allows Java-to-JavaScript + interoperability and provides a seamless integration between the two languages. If an + expression is built using attacker-controlled data, and then evaluated in a powerful + context, it may allow the attacker to run arbitrary code.

    +
    + + +

    In general, including user input in a JavaScript Engine expression should be avoided. + If user input must be included in the expression, it should be then evaluated in a safe + context that doesn't allow arbitrary code invocation. Use "Cloudbees Rhino Sandbox" or + sandboxing with SecurityManager or use graalvm + instead.

    +
    + + +

    The following code could execute random JavaScript code in ScriptEngine

    + + + +

    The following example shows two ways of using Rhino expression. In the 'BAD' case, + an unsafe context is initialized with initStandardObjects that allows arbitrary + Java code to be executed. In the 'GOOD' case, a safe context is initialized with + initSafeStandardObjects or setClassShutter.

    + +
    + + +
  • +CERT coding standard: ScriptEngine code injection +
  • +
  • + Mozilla Rhino: Rhino: JavaScript in Java +
  • +
  • + Rhino Sandbox: A sandbox to execute JavaScript code with Rhino in Java +
  • +
  • + GuardRails: Code Injection +
  • +
    +
    diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql new file mode 100644 index 00000000000..35f73fd5270 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql @@ -0,0 +1,93 @@ +/** + * @name Injection in JavaScript Engine + * @description Evaluation of a user-controlled malicious JavaScript or Java expression in + * JavaScript Engine may lead to remote code execution. + * @kind path-problem + * @problem.severity error + * @precision high + * @id java/unsafe-eval + * @tags security + * external/cwe/cwe-094 + */ + +import java +import semmle.code.java.dataflow.FlowSources +import DataFlow::PathGraph + +class ScriptEngineMethod extends Method { + ScriptEngineMethod() { + this.getDeclaringType().getASupertype*().hasQualifiedName("javax.script", "ScriptEngine") and + this.hasName("eval") + } +} + +/** The context class `org.mozilla.javascript.Context` of Rhino JavaScript Engine. */ +class RhinoContext extends RefType { + RhinoContext() { this.hasQualifiedName("org.mozilla.javascript", "Context") } +} + +/** + * A method that evaluates a Rhino expression. + */ +class RhinoEvaluateExpressionMethod extends Method { + RhinoEvaluateExpressionMethod() { + this.getDeclaringType().getAnAncestor*() instanceof RhinoContext and + ( + hasName("evaluateString") or + hasName("evaluateReader") + ) + } +} + +predicate scriptEngine(MethodAccess ma, Expr sink) { + exists(Method m | m = ma.getMethod() | + m instanceof ScriptEngineMethod and + sink = ma.getArgument(0) + ) +} + +/** + * Holds if `ma` has Rhino code injection vulnerabilities. + */ +predicate evaluateRhinoExpression(MethodAccess ma, Expr sink) { + exists(RhinoEvaluateExpressionMethod m | m = ma.getMethod() | + sink = ma.getArgument(1) and // The second argument is the JavaScript or Java input + not exists(MethodAccess ca | + ( + ca.getMethod().hasName("initSafeStandardObjects") // safe mode + or + ca.getMethod().hasName("setClassShutter") // `ClassShutter` constraint is enforced + ) and + ma.getQualifier() = ca.getQualifier().(VarAccess).getVariable().getAnAccess() + ) + ) +} + +class ScriptInjectionSink extends DataFlow::ExprNode { + ScriptInjectionSink() { + scriptEngine(_, this.getExpr()) or + evaluateRhinoExpression(_, this.getExpr()) + } + + MethodAccess getMethodAccess() { + scriptEngine(result, this.getExpr()) or + evaluateRhinoExpression(result, this.getExpr()) + } +} + +class ScriptInjectionConfiguration extends TaintTracking::Configuration { + ScriptInjectionConfiguration() { this = "ScriptInjectionConfiguration" } + + override predicate isSource(DataFlow::Node source) { + source instanceof RemoteFlowSource + or + source instanceof LocalUserInput + } + + override predicate isSink(DataFlow::Node sink) { sink instanceof ScriptInjectionSink } +} + +from DataFlow::PathNode source, DataFlow::PathNode sink, ScriptInjectionConfiguration conf +where conf.hasFlowPath(source, sink) +select sink.getNode().(ScriptInjectionSink).getMethodAccess(), source, sink, + "JavaScript Engine evaluate $@.", source.getNode(), "user input" diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/RhinoInjection.expected b/java/ql/test/experimental/query-tests/security/CWE-094/RhinoInjection.expected deleted file mode 100644 index 4d2736c8230..00000000000 --- a/java/ql/test/experimental/query-tests/security/CWE-094/RhinoInjection.expected +++ /dev/null @@ -1,7 +0,0 @@ -edges -| RhinoServlet.java:25:23:25:50 | getParameter(...) : String | RhinoServlet.java:29:55:29:58 | code | -nodes -| RhinoServlet.java:25:23:25:50 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| RhinoServlet.java:29:55:29:58 | code | semmle.label | code | -#select -| RhinoServlet.java:29:55:29:58 | code | RhinoServlet.java:25:23:25:50 | getParameter(...) : String | RhinoServlet.java:29:55:29:58 | code | Rhino injection from $@. | RhinoServlet.java:25:23:25:50 | getParameter(...) | user input | diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/RhinoInjection.qlref b/java/ql/test/experimental/query-tests/security/CWE-094/RhinoInjection.qlref deleted file mode 100644 index e306dda44df..00000000000 --- a/java/ql/test/experimental/query-tests/security/CWE-094/RhinoInjection.qlref +++ /dev/null @@ -1 +0,0 @@ -experimental/Security/CWE/CWE-094/RhinoInjection.ql \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/ScriptEngine.qlref b/java/ql/test/experimental/query-tests/security/CWE-094/ScriptEngine.qlref deleted file mode 100644 index 9566c986778..00000000000 --- a/java/ql/test/experimental/query-tests/security/CWE-094/ScriptEngine.qlref +++ /dev/null @@ -1 +0,0 @@ -experimental/Security/CWE/CWE-094/ScriptEngine.ql diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/ScriptEngine.expected b/java/ql/test/experimental/query-tests/security/CWE-094/ScriptInjection.expected similarity index 75% rename from java/ql/test/experimental/query-tests/security/CWE-094/ScriptEngine.expected rename to java/ql/test/experimental/query-tests/security/CWE-094/ScriptInjection.expected index a65faafc6bd..452c2d70c57 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-094/ScriptEngine.expected +++ b/java/ql/test/experimental/query-tests/security/CWE-094/ScriptInjection.expected @@ -1,4 +1,5 @@ edges +| RhinoServlet.java:25:23:25:50 | getParameter(...) : String | RhinoServlet.java:29:55:29:58 | code | | ScriptEngineTest.java:8:44:8:55 | input : String | ScriptEngineTest.java:12:37:12:41 | input | | ScriptEngineTest.java:15:51:15:62 | input : String | ScriptEngineTest.java:19:31:19:35 | input | | ScriptEngineTest.java:23:58:23:69 | input : String | ScriptEngineTest.java:27:31:27:35 | input | @@ -12,6 +13,8 @@ edges | ScriptEngineTest.java:40:70:40:76 | ...[...] : String | ScriptEngineTest.java:23:58:23:69 | input : String | | ScriptEngineTest.java:41:58:41:64 | ...[...] : String | ScriptEngineTest.java:30:46:30:57 | input : String | nodes +| RhinoServlet.java:25:23:25:50 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| RhinoServlet.java:29:55:29:58 | code | semmle.label | code | | ScriptEngineTest.java:8:44:8:55 | input : String | semmle.label | input : String | | ScriptEngineTest.java:12:37:12:41 | input | semmle.label | input | | ScriptEngineTest.java:15:51:15:62 | input : String | semmle.label | input : String | @@ -26,7 +29,8 @@ nodes | ScriptEngineTest.java:40:70:40:76 | ...[...] : String | semmle.label | ...[...] : String | | ScriptEngineTest.java:41:58:41:64 | ...[...] : String | semmle.label | ...[...] : String | #select -| ScriptEngineTest.java:12:19:12:42 | eval(...) | ScriptEngineTest.java:37:26:37:38 | args : String[] | ScriptEngineTest.java:12:37:12:41 | input | ScriptEngine eval $@. | ScriptEngineTest.java:37:26:37:38 | args | user input | -| ScriptEngineTest.java:19:19:19:36 | eval(...) | ScriptEngineTest.java:37:26:37:38 | args : String[] | ScriptEngineTest.java:19:31:19:35 | input | ScriptEngine eval $@. | ScriptEngineTest.java:37:26:37:38 | args | user input | -| ScriptEngineTest.java:27:19:27:36 | eval(...) | ScriptEngineTest.java:37:26:37:38 | args : String[] | ScriptEngineTest.java:27:31:27:35 | input | ScriptEngine eval $@. | ScriptEngineTest.java:37:26:37:38 | args | user input | -| ScriptEngineTest.java:34:19:34:36 | eval(...) | ScriptEngineTest.java:37:26:37:38 | args : String[] | ScriptEngineTest.java:34:31:34:35 | input | ScriptEngine eval $@. | ScriptEngineTest.java:37:26:37:38 | args | user input | +| RhinoServlet.java:29:29:29:78 | evaluateString(...) | RhinoServlet.java:25:23:25:50 | getParameter(...) : String | RhinoServlet.java:29:55:29:58 | code | JavaScript Engine evaluate $@. | RhinoServlet.java:25:23:25:50 | getParameter(...) | user input | +| ScriptEngineTest.java:12:19:12:42 | eval(...) | ScriptEngineTest.java:37:26:37:38 | args : String[] | ScriptEngineTest.java:12:37:12:41 | input | JavaScript Engine evaluate $@. | ScriptEngineTest.java:37:26:37:38 | args | user input | +| ScriptEngineTest.java:19:19:19:36 | eval(...) | ScriptEngineTest.java:37:26:37:38 | args : String[] | ScriptEngineTest.java:19:31:19:35 | input | JavaScript Engine evaluate $@. | ScriptEngineTest.java:37:26:37:38 | args | user input | +| ScriptEngineTest.java:27:19:27:36 | eval(...) | ScriptEngineTest.java:37:26:37:38 | args : String[] | ScriptEngineTest.java:27:31:27:35 | input | JavaScript Engine evaluate $@. | ScriptEngineTest.java:37:26:37:38 | args | user input | +| ScriptEngineTest.java:34:19:34:36 | eval(...) | ScriptEngineTest.java:37:26:37:38 | args : String[] | ScriptEngineTest.java:34:31:34:35 | input | JavaScript Engine evaluate $@. | ScriptEngineTest.java:37:26:37:38 | args | user input | diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/ScriptInjection.qlref b/java/ql/test/experimental/query-tests/security/CWE-094/ScriptInjection.qlref new file mode 100644 index 00000000000..da2b4287d0c --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-094/ScriptInjection.qlref @@ -0,0 +1 @@ +experimental/Security/CWE/CWE-094/ScriptInjection.ql \ No newline at end of file From d664aa6d6add71d2b0d9287dcae665f8ea7df91f Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Wed, 5 May 2021 14:21:52 +0000 Subject: [PATCH 62/74] Include more scenarios and update qldoc --- .../CWE/CWE-094/ScriptInjection.qhelp | 15 ++- .../Security/CWE/CWE-094/ScriptInjection.ql | 86 +++++++++++--- .../security/CWE-094/RhinoServlet.java | 16 +++ .../security/CWE-094/ScriptEngineTest.java | 39 +++++- .../security/CWE-094/ScriptInjection.expected | 64 ++++++---- .../mozilla/javascript/CompilerEnvirons.java | 12 ++ .../org/mozilla/javascript/Context.java | 72 +++++++++++ .../javascript/DefiningClassLoader.java | 36 ++++++ .../org/mozilla/javascript/Function.java | 46 +++++++ .../javascript/GeneratedClassLoader.java | 34 ++++++ .../org/mozilla/javascript/Script.java | 41 +++++++ .../javascript/optimizer/ClassCompiler.java | 112 ++++++++++++++++++ .../scriptengine/javax/script/Bindings.java | 14 +++ .../scriptengine/javax/script/Compilable.java | 9 ++ .../javax/script/CompiledScript.java | 17 +++ .../javax/script/ScriptEngine.java | 2 + .../javax/script/ScriptEngineFactory.java | 7 +- .../nashorn/api/scripting/ClassFilter.java | 5 + .../api/scripting/NashornScriptEngine.java | 27 ++++- .../scripting/NashornScriptEngineFactory.java | 34 +++++- 20 files changed, 633 insertions(+), 55 deletions(-) create mode 100644 java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/CompilerEnvirons.java create mode 100644 java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/DefiningClassLoader.java create mode 100644 java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/Function.java create mode 100644 java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/GeneratedClassLoader.java create mode 100644 java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/Script.java create mode 100644 java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/optimizer/ClassCompiler.java create mode 100644 java/ql/test/stubs/scriptengine/javax/script/Bindings.java create mode 100644 java/ql/test/stubs/scriptengine/javax/script/Compilable.java create mode 100644 java/ql/test/stubs/scriptengine/javax/script/CompiledScript.java create mode 100644 java/ql/test/stubs/scriptengine/jdk/nashorn/api/scripting/ClassFilter.java diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.qhelp b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.qhelp index ddc011fa658..586304ebb7c 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.qhelp +++ b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.qhelp @@ -4,7 +4,7 @@ -

    The JavaScript Engine API has been available since the release of Java 6, which allows +

    The Java Scripting API has been available since the release of Java 6, which allows applications to interact with scripts written in languages such as JavaScript. It serves as an embedded scripting engine inside Java applications which allows Java-to-JavaScript interoperability and provides a seamless integration between the two languages. If an @@ -13,11 +13,11 @@ -

    In general, including user input in a JavaScript Engine expression should be avoided. +

    In general, including user input in a Java Script Engine expression should be avoided. If user input must be included in the expression, it should be then evaluated in a safe - context that doesn't allow arbitrary code invocation. Use "Cloudbees Rhino Sandbox" or - sandboxing with SecurityManager or use graalvm - instead.

    + context that doesn't allow arbitrary code invocation. Use "Cloudbees Rhino Sandbox" or + sandboxing with SecurityManager, which will be deprecated in a future release, or use + GraalVM instead.

    @@ -36,6 +36,9 @@
  • CERT coding standard: ScriptEngine code injection
  • +
  • +GraalVM: Secure by Default +
  • Mozilla Rhino: Rhino: JavaScript in Java
  • @@ -43,7 +46,7 @@ CERT coding standard: A sandbox to execute JavaScript code with Rhino in Java
  • - GuardRails: Code Injection + GuardRails: Code Injection
  • diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql index 35f73fd5270..d4f8cfa5370 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql @@ -1,7 +1,7 @@ /** - * @name Injection in JavaScript Engine + * @name Injection in Java Script Engine * @description Evaluation of a user-controlled malicious JavaScript or Java expression in - * JavaScript Engine may lead to remote code execution. + * Java Script Engine may lead to remote code execution. * @kind path-problem * @problem.severity error * @precision high @@ -14,31 +14,62 @@ import java import semmle.code.java.dataflow.FlowSources import DataFlow::PathGraph +/** A method of ScriptEngine that allows code injection. */ class ScriptEngineMethod extends Method { ScriptEngineMethod() { this.getDeclaringType().getASupertype*().hasQualifiedName("javax.script", "ScriptEngine") and this.hasName("eval") + or + this.getDeclaringType().getASupertype*().hasQualifiedName("javax.script", "Compilable") and + this.hasName("compile") + or + this.getDeclaringType().getASupertype*().hasQualifiedName("javax.script", "ScriptEngineFactory") and + this.hasName(["getProgram", "getMethodCallSyntax"]) } } -/** The context class `org.mozilla.javascript.Context` of Rhino JavaScript Engine. */ +/** The context class `org.mozilla.javascript.Context` of Rhino Java Script Engine. */ class RhinoContext extends RefType { RhinoContext() { this.hasQualifiedName("org.mozilla.javascript", "Context") } } -/** - * A method that evaluates a Rhino expression. - */ +/** A method that evaluates a Rhino expression with `org.mozilla.javascript.Context`. */ class RhinoEvaluateExpressionMethod extends Method { RhinoEvaluateExpressionMethod() { this.getDeclaringType().getAnAncestor*() instanceof RhinoContext and - ( - hasName("evaluateString") or - hasName("evaluateReader") - ) + this.hasName([ + "evaluateString", "evaluateReader", "compileFunction", "compileReader", "compileString" + ]) } } +/** + * A method that compiles a Rhino expression with + * `org.mozilla.javascript.optimizer.ClassCompiler`. + */ +class RhinoCompileClassMethod extends Method { + RhinoCompileClassMethod() { + this.getDeclaringType() + .getASupertype*() + .hasQualifiedName("org.mozilla.javascript.optimizer", "ClassCompiler") and + this.hasName("compileToClassFiles") + } +} + +/** + * A method that defines a Java class from a Rhino expression with + * `org.mozilla.javascript.GeneratedClassLoader`. + */ +class RhinoDefineClassMethod extends Method { + RhinoDefineClassMethod() { + this.getDeclaringType() + .getASupertype*() + .hasQualifiedName("org.mozilla.javascript", "GeneratedClassLoader") and + this.hasName("defineClass") + } +} + +/** Holds if `ma` is a method access of `ScriptEngineMethod`. */ predicate scriptEngine(MethodAccess ma, Expr sink) { exists(Method m | m = ma.getMethod() | m instanceof ScriptEngineMethod and @@ -47,11 +78,17 @@ predicate scriptEngine(MethodAccess ma, Expr sink) { } /** - * Holds if `ma` has Rhino code injection vulnerabilities. + * Holds if a Rhino expression evaluation method has the code injection vulnerability. */ predicate evaluateRhinoExpression(MethodAccess ma, Expr sink) { exists(RhinoEvaluateExpressionMethod m | m = ma.getMethod() | - sink = ma.getArgument(1) and // The second argument is the JavaScript or Java input + ( + sink = ma.getArgument(1) and // The second argument is the JavaScript or Java input + not ma.getMethod().getName() = "compileReader" + or + sink = ma.getArgument(0) and // The first argument is the input reader + ma.getMethod().getName() = "compileReader" + ) and not exists(MethodAccess ca | ( ca.getMethod().hasName("initSafeStandardObjects") // safe mode @@ -63,15 +100,34 @@ predicate evaluateRhinoExpression(MethodAccess ma, Expr sink) { ) } +/** + * Holds if a Rhino expression compilation method has the code injection vulnerability. + */ +predicate compileScript(MethodAccess ma, Expr sink) { + exists(RhinoCompileClassMethod m | m = ma.getMethod() | sink = ma.getArgument(0)) +} + +/** + * Holds if a Rhino class loading method has the code injection vulnerability. + */ +predicate defineClass(MethodAccess ma, Expr sink) { + exists(RhinoDefineClassMethod m | m = ma.getMethod() | sink = ma.getArgument(1)) +} + +/** A sink of script injection. */ class ScriptInjectionSink extends DataFlow::ExprNode { ScriptInjectionSink() { scriptEngine(_, this.getExpr()) or - evaluateRhinoExpression(_, this.getExpr()) + evaluateRhinoExpression(_, this.getExpr()) or + compileScript(_, this.getExpr()) or + defineClass(_, this.getExpr()) } MethodAccess getMethodAccess() { scriptEngine(result, this.getExpr()) or - evaluateRhinoExpression(result, this.getExpr()) + evaluateRhinoExpression(result, this.getExpr()) or + compileScript(result, this.getExpr()) or + defineClass(result, this.getExpr()) } } @@ -90,4 +146,4 @@ class ScriptInjectionConfiguration extends TaintTracking::Configuration { from DataFlow::PathNode source, DataFlow::PathNode sink, ScriptInjectionConfiguration conf where conf.hasFlowPath(source, sink) select sink.getNode().(ScriptInjectionSink).getMethodAccess(), source, sink, - "JavaScript Engine evaluate $@.", source.getNode(), "user input" + "Java Script Engine evaluate $@.", source.getNode(), "user input" diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/RhinoServlet.java b/java/ql/test/experimental/query-tests/security/CWE-094/RhinoServlet.java index f6f529785cc..2c863b6f62f 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-094/RhinoServlet.java +++ b/java/ql/test/experimental/query-tests/security/CWE-094/RhinoServlet.java @@ -5,9 +5,12 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.mozilla.javascript.ClassShutter; +import org.mozilla.javascript.CompilerEnvirons; import org.mozilla.javascript.Context; +import org.mozilla.javascript.DefiningClassLoader; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.RhinoException; +import org.mozilla.javascript.optimizer.ClassCompiler; /** * Servlet implementation class RhinoServlet @@ -75,4 +78,17 @@ public class RhinoServlet extends HttpServlet { Context.exit(); } } + + // BAD: allow arbitrary code to be compiled for subsequent execution + protected void doGet2(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String code = request.getParameter("code"); + ClassCompiler compiler = new ClassCompiler(new CompilerEnvirons()); + Object[] objs = compiler.compileToClassFiles(code, "/sourceLocation", 1, "mainClassName"); + } + + // BAD: allow arbitrary code to be loaded for subsequent execution + protected void doPost2(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String code = request.getParameter("code"); + Class clazz = new DefiningClassLoader().defineClass("Powerfunc", code.getBytes()); + } } diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/ScriptEngineTest.java b/java/ql/test/experimental/query-tests/security/CWE-094/ScriptEngineTest.java index e04ec615f30..42d9cad6e64 100755 --- a/java/ql/test/experimental/query-tests/security/CWE-094/ScriptEngineTest.java +++ b/java/ql/test/experimental/query-tests/security/CWE-094/ScriptEngineTest.java @@ -33,26 +33,53 @@ public class ScriptEngineTest { MyCustomScriptEngine engine = (MyCustomScriptEngine) factory.getScriptEngine(new String[] { "-scripting" }); Object result = engine.eval(input); } + + public void testScriptEngineCompilable(String input) throws ScriptException { + NashornScriptEngineFactory factory = new NashornScriptEngineFactory(); + Compilable engine = (Compilable) factory.getScriptEngine(new String[] { "-scripting" }); + CompiledScript script = engine.compile(input); + Object result = script.eval(); + } + + public void testScriptEngineGetProgram(String input) throws ScriptException { + ScriptEngineManager scriptEngineManager = new ScriptEngineManager(); + ScriptEngine engine = scriptEngineManager.getEngineByName("nashorn"); + String program = engine.getFactory().getProgram(input); + Object result = engine.eval(program); + } public static void main(String[] args) throws ScriptException { new ScriptEngineTest().testWithScriptEngineReference(args[0]); new ScriptEngineTest().testNashornWithScriptEngineReference(args[0]); new ScriptEngineTest().testNashornWithNashornScriptEngineReference(args[0]); new ScriptEngineTest().testCustomScriptEngineReference(args[0]); + new ScriptEngineTest().testScriptEngineCompilable(args[0]); + new ScriptEngineTest().testScriptEngineGetProgram(args[0]); } private static class MyCustomScriptEngine extends AbstractScriptEngine { - public Object eval(String var1) throws ScriptException { - return null; - } + public Object eval(String var1) throws ScriptException { return null; } + + @Override + public ScriptEngineFactory getFactory() { return null; } } private static class MyCustomFactory implements ScriptEngineFactory { public MyCustomFactory() { - } - - public ScriptEngine getScriptEngine() { return null; } + } + + @Override + public ScriptEngine getScriptEngine() { return null; } public ScriptEngine getScriptEngine(String... args) { return null; } + + @Override + public String getEngineName() { return null; } + + @Override + public String getMethodCallSyntax(final String obj, final String method, final String... args) { return null; } + + @Override + public String getProgram(final String... statements) { return null; } } } diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/ScriptInjection.expected b/java/ql/test/experimental/query-tests/security/CWE-094/ScriptInjection.expected index 452c2d70c57..0c191a4f579 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-094/ScriptInjection.expected +++ b/java/ql/test/experimental/query-tests/security/CWE-094/ScriptInjection.expected @@ -1,20 +1,32 @@ edges -| RhinoServlet.java:25:23:25:50 | getParameter(...) : String | RhinoServlet.java:29:55:29:58 | code | +| RhinoServlet.java:28:23:28:50 | getParameter(...) : String | RhinoServlet.java:32:55:32:58 | code | +| RhinoServlet.java:84:23:84:50 | getParameter(...) : String | RhinoServlet.java:86:54:86:57 | code | +| RhinoServlet.java:91:23:91:50 | getParameter(...) : String | RhinoServlet.java:92:74:92:88 | getBytes(...) | | ScriptEngineTest.java:8:44:8:55 | input : String | ScriptEngineTest.java:12:37:12:41 | input | | ScriptEngineTest.java:15:51:15:62 | input : String | ScriptEngineTest.java:19:31:19:35 | input | | ScriptEngineTest.java:23:58:23:69 | input : String | ScriptEngineTest.java:27:31:27:35 | input | | ScriptEngineTest.java:30:46:30:57 | input : String | ScriptEngineTest.java:34:31:34:35 | input | -| ScriptEngineTest.java:37:26:37:38 | args : String[] | ScriptEngineTest.java:38:56:38:62 | ...[...] : String | -| ScriptEngineTest.java:37:26:37:38 | args : String[] | ScriptEngineTest.java:39:63:39:69 | ...[...] : String | -| ScriptEngineTest.java:37:26:37:38 | args : String[] | ScriptEngineTest.java:40:70:40:76 | ...[...] : String | -| ScriptEngineTest.java:37:26:37:38 | args : String[] | ScriptEngineTest.java:41:58:41:64 | ...[...] : String | -| ScriptEngineTest.java:38:56:38:62 | ...[...] : String | ScriptEngineTest.java:8:44:8:55 | input : String | -| ScriptEngineTest.java:39:63:39:69 | ...[...] : String | ScriptEngineTest.java:15:51:15:62 | input : String | -| ScriptEngineTest.java:40:70:40:76 | ...[...] : String | ScriptEngineTest.java:23:58:23:69 | input : String | -| ScriptEngineTest.java:41:58:41:64 | ...[...] : String | ScriptEngineTest.java:30:46:30:57 | input : String | +| ScriptEngineTest.java:37:41:37:52 | input : String | ScriptEngineTest.java:40:42:40:46 | input | +| ScriptEngineTest.java:44:41:44:52 | input : String | ScriptEngineTest.java:47:51:47:55 | input | +| ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:52:56:52:62 | ...[...] : String | +| ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:53:63:53:69 | ...[...] : String | +| ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:54:70:54:76 | ...[...] : String | +| ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:55:58:55:64 | ...[...] : String | +| ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:56:53:56:59 | ...[...] : String | +| ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:57:53:57:59 | ...[...] : String | +| ScriptEngineTest.java:52:56:52:62 | ...[...] : String | ScriptEngineTest.java:8:44:8:55 | input : String | +| ScriptEngineTest.java:53:63:53:69 | ...[...] : String | ScriptEngineTest.java:15:51:15:62 | input : String | +| ScriptEngineTest.java:54:70:54:76 | ...[...] : String | ScriptEngineTest.java:23:58:23:69 | input : String | +| ScriptEngineTest.java:55:58:55:64 | ...[...] : String | ScriptEngineTest.java:30:46:30:57 | input : String | +| ScriptEngineTest.java:56:53:56:59 | ...[...] : String | ScriptEngineTest.java:37:41:37:52 | input : String | +| ScriptEngineTest.java:57:53:57:59 | ...[...] : String | ScriptEngineTest.java:44:41:44:52 | input : String | nodes -| RhinoServlet.java:25:23:25:50 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| RhinoServlet.java:29:55:29:58 | code | semmle.label | code | +| RhinoServlet.java:28:23:28:50 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| RhinoServlet.java:32:55:32:58 | code | semmle.label | code | +| RhinoServlet.java:84:23:84:50 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| RhinoServlet.java:86:54:86:57 | code | semmle.label | code | +| RhinoServlet.java:91:23:91:50 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| RhinoServlet.java:92:74:92:88 | getBytes(...) | semmle.label | getBytes(...) | | ScriptEngineTest.java:8:44:8:55 | input : String | semmle.label | input : String | | ScriptEngineTest.java:12:37:12:41 | input | semmle.label | input | | ScriptEngineTest.java:15:51:15:62 | input : String | semmle.label | input : String | @@ -23,14 +35,24 @@ nodes | ScriptEngineTest.java:27:31:27:35 | input | semmle.label | input | | ScriptEngineTest.java:30:46:30:57 | input : String | semmle.label | input : String | | ScriptEngineTest.java:34:31:34:35 | input | semmle.label | input | -| ScriptEngineTest.java:37:26:37:38 | args : String[] | semmle.label | args : String[] | -| ScriptEngineTest.java:38:56:38:62 | ...[...] : String | semmle.label | ...[...] : String | -| ScriptEngineTest.java:39:63:39:69 | ...[...] : String | semmle.label | ...[...] : String | -| ScriptEngineTest.java:40:70:40:76 | ...[...] : String | semmle.label | ...[...] : String | -| ScriptEngineTest.java:41:58:41:64 | ...[...] : String | semmle.label | ...[...] : String | +| ScriptEngineTest.java:37:41:37:52 | input : String | semmle.label | input : String | +| ScriptEngineTest.java:40:42:40:46 | input | semmle.label | input | +| ScriptEngineTest.java:44:41:44:52 | input : String | semmle.label | input : String | +| ScriptEngineTest.java:47:51:47:55 | input | semmle.label | input | +| ScriptEngineTest.java:51:26:51:38 | args : String[] | semmle.label | args : String[] | +| ScriptEngineTest.java:52:56:52:62 | ...[...] : String | semmle.label | ...[...] : String | +| ScriptEngineTest.java:53:63:53:69 | ...[...] : String | semmle.label | ...[...] : String | +| ScriptEngineTest.java:54:70:54:76 | ...[...] : String | semmle.label | ...[...] : String | +| ScriptEngineTest.java:55:58:55:64 | ...[...] : String | semmle.label | ...[...] : String | +| ScriptEngineTest.java:56:53:56:59 | ...[...] : String | semmle.label | ...[...] : String | +| ScriptEngineTest.java:57:53:57:59 | ...[...] : String | semmle.label | ...[...] : String | #select -| RhinoServlet.java:29:29:29:78 | evaluateString(...) | RhinoServlet.java:25:23:25:50 | getParameter(...) : String | RhinoServlet.java:29:55:29:58 | code | JavaScript Engine evaluate $@. | RhinoServlet.java:25:23:25:50 | getParameter(...) | user input | -| ScriptEngineTest.java:12:19:12:42 | eval(...) | ScriptEngineTest.java:37:26:37:38 | args : String[] | ScriptEngineTest.java:12:37:12:41 | input | JavaScript Engine evaluate $@. | ScriptEngineTest.java:37:26:37:38 | args | user input | -| ScriptEngineTest.java:19:19:19:36 | eval(...) | ScriptEngineTest.java:37:26:37:38 | args : String[] | ScriptEngineTest.java:19:31:19:35 | input | JavaScript Engine evaluate $@. | ScriptEngineTest.java:37:26:37:38 | args | user input | -| ScriptEngineTest.java:27:19:27:36 | eval(...) | ScriptEngineTest.java:37:26:37:38 | args : String[] | ScriptEngineTest.java:27:31:27:35 | input | JavaScript Engine evaluate $@. | ScriptEngineTest.java:37:26:37:38 | args | user input | -| ScriptEngineTest.java:34:19:34:36 | eval(...) | ScriptEngineTest.java:37:26:37:38 | args : String[] | ScriptEngineTest.java:34:31:34:35 | input | JavaScript Engine evaluate $@. | ScriptEngineTest.java:37:26:37:38 | args | user input | +| RhinoServlet.java:32:29:32:78 | evaluateString(...) | RhinoServlet.java:28:23:28:50 | getParameter(...) : String | RhinoServlet.java:32:55:32:58 | code | Java Script Engine evaluate $@. | RhinoServlet.java:28:23:28:50 | getParameter(...) | user input | +| RhinoServlet.java:86:25:86:97 | compileToClassFiles(...) | RhinoServlet.java:84:23:84:50 | getParameter(...) : String | RhinoServlet.java:86:54:86:57 | code | Java Script Engine evaluate $@. | RhinoServlet.java:84:23:84:50 | getParameter(...) | user input | +| RhinoServlet.java:92:23:92:89 | defineClass(...) | RhinoServlet.java:91:23:91:50 | getParameter(...) : String | RhinoServlet.java:92:74:92:88 | getBytes(...) | Java Script Engine evaluate $@. | RhinoServlet.java:91:23:91:50 | getParameter(...) | user input | +| ScriptEngineTest.java:12:19:12:42 | eval(...) | ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:12:37:12:41 | input | Java Script Engine evaluate $@. | ScriptEngineTest.java:51:26:51:38 | args | user input | +| ScriptEngineTest.java:19:19:19:36 | eval(...) | ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:19:31:19:35 | input | Java Script Engine evaluate $@. | ScriptEngineTest.java:51:26:51:38 | args | user input | +| ScriptEngineTest.java:27:19:27:36 | eval(...) | ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:27:31:27:35 | input | Java Script Engine evaluate $@. | ScriptEngineTest.java:51:26:51:38 | args | user input | +| ScriptEngineTest.java:34:19:34:36 | eval(...) | ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:34:31:34:35 | input | Java Script Engine evaluate $@. | ScriptEngineTest.java:51:26:51:38 | args | user input | +| ScriptEngineTest.java:40:27:40:47 | compile(...) | ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:40:42:40:46 | input | Java Script Engine evaluate $@. | ScriptEngineTest.java:51:26:51:38 | args | user input | +| ScriptEngineTest.java:47:20:47:56 | getProgram(...) | ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:47:51:47:55 | input | Java Script Engine evaluate $@. | ScriptEngineTest.java:51:26:51:38 | args | user input | diff --git a/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/CompilerEnvirons.java b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/CompilerEnvirons.java new file mode 100644 index 00000000000..3cb0619499e --- /dev/null +++ b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/CompilerEnvirons.java @@ -0,0 +1,12 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + package org.mozilla.javascript; + + public class CompilerEnvirons { + public CompilerEnvirons() { + } + } \ No newline at end of file diff --git a/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/Context.java b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/Context.java index 57d30a3ab25..1bda212cfa4 100644 --- a/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/Context.java +++ b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/Context.java @@ -480,6 +480,78 @@ public class Context return null; } + /** + * @deprecated + * @see #compileReader(Reader in, String sourceName, int lineno, Object securityDomain) + */ + @Deprecated + public final Script compileReader( + Scriptable scope, Reader in, String sourceName, int lineno, Object securityDomain) + throws IOException { + return null; + } + + /** + * Compiles the source in the given reader. + * + *

    Returns a script that may later be executed. Will consume all the source in the reader. + * + * @param in the input reader + * @param sourceName a string describing the source, such as a filename + * @param lineno the starting line number for reporting errors + * @param securityDomain an arbitrary object that specifies security information about the + * origin or owner of the script. For implementations that don't care about security, this + * value may be null. + * @return a script that may later be executed + * @exception IOException if an IOException was generated by the Reader + * @see org.mozilla.javascript.Script + */ + public final Script compileReader( + Reader in, String sourceName, int lineno, Object securityDomain) throws IOException { + return null; + } + + /** + * Compiles the source in the given string. + * + *

    Returns a script that may later be executed. + * + * @param source the source string + * @param sourceName a string describing the source, such as a filename + * @param lineno the starting line number for reporting errors. Use 0 if the line number is + * unknown. + * @param securityDomain an arbitrary object that specifies security information about the + * origin or owner of the script. For implementations that don't care about security, this + * value may be null. + * @return a script that may later be executed + * @see org.mozilla.javascript.Script + */ + public final Script compileString( + String source, String sourceName, int lineno, Object securityDomain) { + return null; + } + + /** + * Compile a JavaScript function. + * + *

    The function source must be a function definition as defined by ECMA (e.g., "function f(a) + * { return a; }"). + * + * @param scope the scope to compile relative to + * @param source the function definition source + * @param sourceName a string describing the source, such as a filename + * @param lineno the starting line number + * @param securityDomain an arbitrary object that specifies security information about the + * origin or owner of the script. For implementations that don't care about security, this + * value may be null. + * @return a Function that may later be called + * @see org.mozilla.javascript.Function + */ + public final Function compileFunction( + Scriptable scope, String source, String sourceName, int lineno, Object securityDomain) { + return null; + } + /** * Convert the value to a JavaScript boolean value. *

    diff --git a/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/DefiningClassLoader.java b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/DefiningClassLoader.java new file mode 100644 index 00000000000..3819798c351 --- /dev/null +++ b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/DefiningClassLoader.java @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + package org.mozilla.javascript; + + /** + * Load generated classes. + * + * @author Norris Boyd + */ + public class DefiningClassLoader extends ClassLoader + implements GeneratedClassLoader + { + public DefiningClassLoader() { + } + + public DefiningClassLoader(ClassLoader parentLoader) { + } + + @Override + public Class defineClass(String name, byte[] data) { + return null; + } + + @Override + public void linkClass(Class cl) { + } + + @Override + public Class loadClass(String name, boolean resolve) + throws ClassNotFoundException + { + return null; + } + } \ No newline at end of file diff --git a/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/Function.java b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/Function.java new file mode 100644 index 00000000000..a35a7c2dfba --- /dev/null +++ b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/Function.java @@ -0,0 +1,46 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// API class + +package org.mozilla.javascript; + +/** + * This is interface that all functions in JavaScript must implement. The interface provides for + * calling functions and constructors. + * + * @see org.mozilla.javascript.Scriptable + * @author Norris Boyd + */ +public interface Function extends Scriptable { + /** + * Call the function. + * + *

    Note that the array of arguments is not guaranteed to have length greater than 0. + * + * @param cx the current Context for this thread + * @param scope the scope to execute the function relative to. This is set to the value returned + * by getParentScope() except when the function is called from a closure. + * @param thisObj the JavaScript this object + * @param args the array of arguments + * @return the result of the call + */ + Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args); + + /** + * Call the function as a constructor. + * + *

    This method is invoked by the runtime in order to satisfy a use of the JavaScript + * new operator. This method is expected to create a new object and return it. + * + * @param cx the current Context for this thread + * @param scope an enclosing scope of the caller except when the function is called from a + * closure. + * @param args the array of arguments + * @return the allocated object + */ + Scriptable construct(Context cx, Scriptable scope, Object[] args); +} \ No newline at end of file diff --git a/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/GeneratedClassLoader.java b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/GeneratedClassLoader.java new file mode 100644 index 00000000000..c7450862917 --- /dev/null +++ b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/GeneratedClassLoader.java @@ -0,0 +1,34 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// API class + +package org.mozilla.javascript; + +/** + * Interface to define classes from generated byte code. + */ +public interface GeneratedClassLoader { + + /** + * Define a new Java class. + * Classes created via this method should have the same class loader. + * + * @param name fully qualified class name + * @param data class byte code + * @return new class object + */ + public Class defineClass(String name, byte[] data); + + /** + * Link the given class. + * + * @param cl Class instance returned from the previous call to + * {@link #defineClass(String, byte[])} + * @see java.lang.ClassLoader + */ + public void linkClass(Class cl); +} diff --git a/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/Script.java b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/Script.java new file mode 100644 index 00000000000..824dc0241c1 --- /dev/null +++ b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/Script.java @@ -0,0 +1,41 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// API class + +package org.mozilla.javascript; + +/** + * All compiled scripts implement this interface. + *

    + * This class encapsulates script execution relative to an + * object scope. + * @since 1.3 + * @author Norris Boyd + */ + +public interface Script { + + /** + * Execute the script. + *

    + * The script is executed in a particular runtime Context, which + * must be associated with the current thread. + * The script is executed relative to a scope--definitions and + * uses of global top-level variables and functions will access + * properties of the scope object. For compliant ECMA + * programs, the scope must be an object that has been initialized + * as a global object using Context.initStandardObjects. + *

    + * + * @param cx the Context associated with the current thread + * @param scope the scope to execute relative to + * @return the result of executing the script + * @see org.mozilla.javascript.Context#initStandardObjects() + */ + public Object exec(Context cx, Scriptable scope); + +} \ No newline at end of file diff --git a/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/optimizer/ClassCompiler.java b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/optimizer/ClassCompiler.java new file mode 100644 index 00000000000..cb2332d3f61 --- /dev/null +++ b/java/ql/test/experimental/stubs/rhino-1.7.13/org/mozilla/javascript/optimizer/ClassCompiler.java @@ -0,0 +1,112 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + package org.mozilla.javascript.optimizer; + + import org.mozilla.javascript.CompilerEnvirons; + + /** + * Generates class files from script sources. + * + * since 1.5 Release 5 + * @author Igor Bukanov + */ + + public class ClassCompiler + { + /** + * Construct ClassCompiler that uses the specified compiler environment + * when generating classes. + */ + public ClassCompiler(CompilerEnvirons compilerEnv) + { + } + + /** + * Set the class name to use for main method implementation. + * The class must have a method matching + * public static void main(Script sc, String[] args), it will be + * called when main(String[] args) is called in the generated + * class. The class name should be fully qulified name and include the + * package name like in org.foo.Bar. + */ + public void setMainMethodClass(String className) + { + } + + /** + * Get the name of the class for main method implementation. + * @see #setMainMethodClass(String) + */ + public String getMainMethodClass() + { + return null; + } + + /** + * Get the compiler environment the compiler uses. + */ + public CompilerEnvirons getCompilerEnv() + { + return null; + } + + /** + * Get the class that the generated target will extend. + */ + public Class getTargetExtends() + { + return null; + } + + /** + * Set the class that the generated target will extend. + * + * @param extendsClass the class it extends + */ + public void setTargetExtends(Class extendsClass) + { + } + + /** + * Get the interfaces that the generated target will implement. + */ + public Class[] getTargetImplements() + { + return null; + } + + /** + * Set the interfaces that the generated target will implement. + * + * @param implementsClasses an array of Class objects, one for each + * interface the target will extend + */ + public void setTargetImplements(Class[] implementsClasses) + { + } + + /** + * Compile JavaScript source into one or more Java class files. + * The first compiled class will have name mainClassName. + * If the results of {@link #getTargetExtends()} or + * {@link #getTargetImplements()} are not null, then the first compiled + * class will extend the specified super class and implement + * specified interfaces. + * + * @return array where elements with even indexes specifies class name + * and the following odd index gives class file body as byte[] + * array. The initial element of the array always holds + * mainClassName and array[1] holds its byte code. + */ + public Object[] compileToClassFiles(String source, + String sourceLocation, + int lineno, + String mainClassName) + { + return null; + } + } \ No newline at end of file diff --git a/java/ql/test/stubs/scriptengine/javax/script/Bindings.java b/java/ql/test/stubs/scriptengine/javax/script/Bindings.java new file mode 100644 index 00000000000..a8eeeb6fe5e --- /dev/null +++ b/java/ql/test/stubs/scriptengine/javax/script/Bindings.java @@ -0,0 +1,14 @@ +package javax.script; +import java.util.Map; + +public interface Bindings extends Map { + public Object put(String name, Object value); + + public void putAll(Map toMerge); + + public boolean containsKey(Object key); + + public Object get(Object key); + + public Object remove(Object key); +} diff --git a/java/ql/test/stubs/scriptengine/javax/script/Compilable.java b/java/ql/test/stubs/scriptengine/javax/script/Compilable.java new file mode 100644 index 00000000000..ce6700c5a66 --- /dev/null +++ b/java/ql/test/stubs/scriptengine/javax/script/Compilable.java @@ -0,0 +1,9 @@ +package javax.script; + +import java.io.Reader; + +public interface Compilable { + public CompiledScript compile(String script) throws ScriptException; + + public CompiledScript compile(Reader script) throws ScriptException; +} diff --git a/java/ql/test/stubs/scriptengine/javax/script/CompiledScript.java b/java/ql/test/stubs/scriptengine/javax/script/CompiledScript.java new file mode 100644 index 00000000000..2f03d58c9a7 --- /dev/null +++ b/java/ql/test/stubs/scriptengine/javax/script/CompiledScript.java @@ -0,0 +1,17 @@ +package javax.script; + +public abstract class CompiledScript { + + public abstract Object eval(ScriptContext context) throws ScriptException; + + public Object eval(Bindings bindings) throws ScriptException { + return null; + } + + public Object eval() throws ScriptException { + return null; + } + + public abstract ScriptEngine getEngine(); + +} \ No newline at end of file diff --git a/java/ql/test/stubs/scriptengine/javax/script/ScriptEngine.java b/java/ql/test/stubs/scriptengine/javax/script/ScriptEngine.java index 4dc4f8c3186..35b91119e4f 100644 --- a/java/ql/test/stubs/scriptengine/javax/script/ScriptEngine.java +++ b/java/ql/test/stubs/scriptengine/javax/script/ScriptEngine.java @@ -2,5 +2,7 @@ package javax.script; public interface ScriptEngine { Object eval(String var1) throws ScriptException; + + public ScriptEngineFactory getFactory(); } diff --git a/java/ql/test/stubs/scriptengine/javax/script/ScriptEngineFactory.java b/java/ql/test/stubs/scriptengine/javax/script/ScriptEngineFactory.java index 7441c8a4ade..f802d86f80b 100644 --- a/java/ql/test/stubs/scriptengine/javax/script/ScriptEngineFactory.java +++ b/java/ql/test/stubs/scriptengine/javax/script/ScriptEngineFactory.java @@ -1,6 +1,11 @@ package javax.script; public interface ScriptEngineFactory { + public String getEngineName(); + + public String getMethodCallSyntax(String obj, String m, String... args); + + public String getProgram(String... statements); + ScriptEngine getScriptEngine(); } - diff --git a/java/ql/test/stubs/scriptengine/jdk/nashorn/api/scripting/ClassFilter.java b/java/ql/test/stubs/scriptengine/jdk/nashorn/api/scripting/ClassFilter.java new file mode 100644 index 00000000000..fcc624fc106 --- /dev/null +++ b/java/ql/test/stubs/scriptengine/jdk/nashorn/api/scripting/ClassFilter.java @@ -0,0 +1,5 @@ +package jdk.nashorn.api.scripting; + +public interface ClassFilter { + public boolean exposeToScripts(String className); +} diff --git a/java/ql/test/stubs/scriptengine/jdk/nashorn/api/scripting/NashornScriptEngine.java b/java/ql/test/stubs/scriptengine/jdk/nashorn/api/scripting/NashornScriptEngine.java index 8dc3c1afa10..e89282dfa27 100644 --- a/java/ql/test/stubs/scriptengine/jdk/nashorn/api/scripting/NashornScriptEngine.java +++ b/java/ql/test/stubs/scriptengine/jdk/nashorn/api/scripting/NashornScriptEngine.java @@ -1,10 +1,31 @@ package jdk.nashorn.api.scripting; -import javax.script.*; +import java.io.Reader; -public final class NashornScriptEngine extends AbstractScriptEngine { +import javax.script.AbstractScriptEngine; +import javax.script.Compilable; +import javax.script.CompiledScript; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; +import javax.script.ScriptException; + +public final class NashornScriptEngine extends AbstractScriptEngine implements Compilable { public Object eval(String var1) throws ScriptException { return null; } -} + @Override + public ScriptEngineFactory getFactory() { + return null; + } + + @Override + public CompiledScript compile(final Reader reader) throws ScriptException { + return null; + } + + @Override + public CompiledScript compile(final String str) throws ScriptException { + return null; + } +} diff --git a/java/ql/test/stubs/scriptengine/jdk/nashorn/api/scripting/NashornScriptEngineFactory.java b/java/ql/test/stubs/scriptengine/jdk/nashorn/api/scripting/NashornScriptEngineFactory.java index 763e098ddbe..177bf463eb3 100644 --- a/java/ql/test/stubs/scriptengine/jdk/nashorn/api/scripting/NashornScriptEngineFactory.java +++ b/java/ql/test/stubs/scriptengine/jdk/nashorn/api/scripting/NashornScriptEngineFactory.java @@ -3,20 +3,48 @@ package jdk.nashorn.api.scripting; import javax.script.ScriptEngine; import javax.script.ScriptEngineFactory; - public final class NashornScriptEngineFactory implements ScriptEngineFactory { public NashornScriptEngineFactory() { } + @Override + public String getEngineName() { + return null; + } + @Override + public String getMethodCallSyntax(final String obj, final String method, final String... args) { + return null; + } + + @Override + public String getProgram(final String... statements) { + return null; + } + + @Override public ScriptEngine getScriptEngine() { return null; } + public ScriptEngine getScriptEngine(final ClassLoader appLoader) { + return null; + } - public ScriptEngine getScriptEngine(String... args) { + public ScriptEngine getScriptEngine(final ClassFilter classFilter) { + return null; + } + + public ScriptEngine getScriptEngine(final String... args) { + return null; + } + + public ScriptEngine getScriptEngine(final String[] args, final ClassLoader appLoader) { + return null; + } + + public ScriptEngine getScriptEngine(final String[] args, final ClassLoader appLoader, final ClassFilter classFilter) { return null; } } - From e4699f7fa9a142158c1207ccbc8ac31d667ed782 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Tue, 11 May 2021 20:09:37 +0000 Subject: [PATCH 63/74] Optimize the query --- .../Security/CWE/CWE-094/RhinoInjection.java | 9 +- .../Security/CWE/CWE-094/ScriptInjection.ql | 33 +++--- .../security/CWE-094/RhinoServlet.java | 5 +- .../security/CWE-094/ScriptEngineTest.java | 42 +++++--- .../security/CWE-094/ScriptInjection.expected | 102 +++++++++--------- 5 files changed, 99 insertions(+), 92 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.java b/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.java index 4a87d5bc354..15adfbe4524 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.java +++ b/java/ql/src/experimental/Security/CWE/CWE-094/RhinoInjection.java @@ -1,3 +1,7 @@ +import org.mozilla.javascript.ClassShutter; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.Scriptable; + public class RhinoInjection extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { @@ -20,10 +24,7 @@ public class RhinoInjection extends HttpServlet { Scriptable scope = ctx.initStandardObjects(); ctx.setClassShutter(new ClassShutter() { public boolean visibleToScripts(String className) { - if(className.startsWith("com.example.")) { - return true; - } - return false; + return className.startsWith("com.example."); } }); } diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql index d4f8cfa5370..0819f85c17e 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql @@ -1,7 +1,7 @@ /** * @name Injection in Java Script Engine - * @description Evaluation of a user-controlled malicious JavaScript or Java expression in - * Java Script Engine may lead to remote code execution. + * @description Evaluation of user-controlled data using the Java Script Engine may + * lead to remote code execution. * @kind path-problem * @problem.severity error * @precision high @@ -78,43 +78,37 @@ predicate scriptEngine(MethodAccess ma, Expr sink) { } /** - * Holds if a Rhino expression evaluation method has the code injection vulnerability. + * Holds if a Rhino expression evaluation method is vulnerable to code injection. */ predicate evaluateRhinoExpression(MethodAccess ma, Expr sink) { exists(RhinoEvaluateExpressionMethod m | m = ma.getMethod() | ( - sink = ma.getArgument(1) and // The second argument is the JavaScript or Java input - not ma.getMethod().getName() = "compileReader" - or - sink = ma.getArgument(0) and // The first argument is the input reader - ma.getMethod().getName() = "compileReader" + if ma.getMethod().getName() = "compileReader" + then sink = ma.getArgument(0) // The first argument is the input reader + else sink = ma.getArgument(1) // The second argument is the JavaScript or Java input ) and not exists(MethodAccess ca | - ( - ca.getMethod().hasName("initSafeStandardObjects") // safe mode - or - ca.getMethod().hasName("setClassShutter") // `ClassShutter` constraint is enforced - ) and + ca.getMethod().hasName(["initSafeStandardObjects", "setClassShutter"]) and // safe mode or `ClassShutter` constraint is enforced ma.getQualifier() = ca.getQualifier().(VarAccess).getVariable().getAnAccess() ) ) } /** - * Holds if a Rhino expression compilation method has the code injection vulnerability. + * Holds if a Rhino expression compilation method is vulnerable to code injection. */ predicate compileScript(MethodAccess ma, Expr sink) { exists(RhinoCompileClassMethod m | m = ma.getMethod() | sink = ma.getArgument(0)) } /** - * Holds if a Rhino class loading method has the code injection vulnerability. + * Holds if a Rhino class loading method is vulnerable to code injection. */ predicate defineClass(MethodAccess ma, Expr sink) { exists(RhinoDefineClassMethod m | m = ma.getMethod() | sink = ma.getArgument(1)) } -/** A sink of script injection. */ +/** A script injection sink. */ class ScriptInjectionSink extends DataFlow::ExprNode { ScriptInjectionSink() { scriptEngine(_, this.getExpr()) or @@ -123,6 +117,7 @@ class ScriptInjectionSink extends DataFlow::ExprNode { defineClass(_, this.getExpr()) } + /** An access to the method associated with this sink. */ MethodAccess getMethodAccess() { scriptEngine(result, this.getExpr()) or evaluateRhinoExpression(result, this.getExpr()) or @@ -134,11 +129,7 @@ class ScriptInjectionSink extends DataFlow::ExprNode { class ScriptInjectionConfiguration extends TaintTracking::Configuration { ScriptInjectionConfiguration() { this = "ScriptInjectionConfiguration" } - override predicate isSource(DataFlow::Node source) { - source instanceof RemoteFlowSource - or - source instanceof LocalUserInput - } + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } override predicate isSink(DataFlow::Node sink) { sink instanceof ScriptInjectionSink } } diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/RhinoServlet.java b/java/ql/test/experimental/query-tests/security/CWE-094/RhinoServlet.java index 2c863b6f62f..e76a9543f87 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-094/RhinoServlet.java +++ b/java/ql/test/experimental/query-tests/security/CWE-094/RhinoServlet.java @@ -63,10 +63,7 @@ public class RhinoServlet extends HttpServlet { Scriptable scope = ctx.initStandardObjects(); ctx.setClassShutter(new ClassShutter() { public boolean visibleToScripts(String className) { - if(className.startsWith("com.example.")) { - return true; - } - return false; + return className.startsWith("com.example."); } }); diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/ScriptEngineTest.java b/java/ql/test/experimental/query-tests/security/CWE-094/ScriptEngineTest.java index 42d9cad6e64..ed7099d7598 100755 --- a/java/ql/test/experimental/query-tests/security/CWE-094/ScriptEngineTest.java +++ b/java/ql/test/experimental/query-tests/security/CWE-094/ScriptEngineTest.java @@ -1,9 +1,21 @@ +import javax.script.AbstractScriptEngine; +import javax.script.Compilable; +import javax.script.CompiledScript; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptEngineFactory; +import javax.script.ScriptException; + import jdk.nashorn.api.scripting.NashornScriptEngine; import jdk.nashorn.api.scripting.NashornScriptEngineFactory; -import javax.script.*; +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; -public class ScriptEngineTest { +public class ScriptEngineTest extends HttpServlet { public void testWithScriptEngineReference(String input) throws ScriptException { ScriptEngineManager scriptEngineManager = new ScriptEngineManager(); @@ -47,16 +59,7 @@ public class ScriptEngineTest { String program = engine.getFactory().getProgram(input); Object result = engine.eval(program); } - - public static void main(String[] args) throws ScriptException { - new ScriptEngineTest().testWithScriptEngineReference(args[0]); - new ScriptEngineTest().testNashornWithScriptEngineReference(args[0]); - new ScriptEngineTest().testNashornWithNashornScriptEngineReference(args[0]); - new ScriptEngineTest().testCustomScriptEngineReference(args[0]); - new ScriptEngineTest().testScriptEngineCompilable(args[0]); - new ScriptEngineTest().testScriptEngineGetProgram(args[0]); - } - + private static class MyCustomScriptEngine extends AbstractScriptEngine { public Object eval(String var1) throws ScriptException { return null; } @@ -82,4 +85,19 @@ public class ScriptEngineTest { @Override public String getProgram(final String... statements) { return null; } } + + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + try { + String code = request.getParameter("code"); + + new ScriptEngineTest().testWithScriptEngineReference(code); + new ScriptEngineTest().testNashornWithScriptEngineReference(code); + new ScriptEngineTest().testNashornWithNashornScriptEngineReference(code); + new ScriptEngineTest().testCustomScriptEngineReference(code); + new ScriptEngineTest().testScriptEngineCompilable(code); + new ScriptEngineTest().testScriptEngineGetProgram(code); + } catch (ScriptException se) { + throw new IOException(se.getMessage()); + } + } } diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/ScriptInjection.expected b/java/ql/test/experimental/query-tests/security/CWE-094/ScriptInjection.expected index 0c191a4f579..5f1d250e9a2 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-094/ScriptInjection.expected +++ b/java/ql/test/experimental/query-tests/security/CWE-094/ScriptInjection.expected @@ -1,58 +1,58 @@ edges | RhinoServlet.java:28:23:28:50 | getParameter(...) : String | RhinoServlet.java:32:55:32:58 | code | -| RhinoServlet.java:84:23:84:50 | getParameter(...) : String | RhinoServlet.java:86:54:86:57 | code | -| RhinoServlet.java:91:23:91:50 | getParameter(...) : String | RhinoServlet.java:92:74:92:88 | getBytes(...) | -| ScriptEngineTest.java:8:44:8:55 | input : String | ScriptEngineTest.java:12:37:12:41 | input | -| ScriptEngineTest.java:15:51:15:62 | input : String | ScriptEngineTest.java:19:31:19:35 | input | -| ScriptEngineTest.java:23:58:23:69 | input : String | ScriptEngineTest.java:27:31:27:35 | input | -| ScriptEngineTest.java:30:46:30:57 | input : String | ScriptEngineTest.java:34:31:34:35 | input | -| ScriptEngineTest.java:37:41:37:52 | input : String | ScriptEngineTest.java:40:42:40:46 | input | -| ScriptEngineTest.java:44:41:44:52 | input : String | ScriptEngineTest.java:47:51:47:55 | input | -| ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:52:56:52:62 | ...[...] : String | -| ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:53:63:53:69 | ...[...] : String | -| ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:54:70:54:76 | ...[...] : String | -| ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:55:58:55:64 | ...[...] : String | -| ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:56:53:56:59 | ...[...] : String | -| ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:57:53:57:59 | ...[...] : String | -| ScriptEngineTest.java:52:56:52:62 | ...[...] : String | ScriptEngineTest.java:8:44:8:55 | input : String | -| ScriptEngineTest.java:53:63:53:69 | ...[...] : String | ScriptEngineTest.java:15:51:15:62 | input : String | -| ScriptEngineTest.java:54:70:54:76 | ...[...] : String | ScriptEngineTest.java:23:58:23:69 | input : String | -| ScriptEngineTest.java:55:58:55:64 | ...[...] : String | ScriptEngineTest.java:30:46:30:57 | input : String | -| ScriptEngineTest.java:56:53:56:59 | ...[...] : String | ScriptEngineTest.java:37:41:37:52 | input : String | -| ScriptEngineTest.java:57:53:57:59 | ...[...] : String | ScriptEngineTest.java:44:41:44:52 | input : String | +| RhinoServlet.java:81:23:81:50 | getParameter(...) : String | RhinoServlet.java:83:54:83:57 | code | +| RhinoServlet.java:88:23:88:50 | getParameter(...) : String | RhinoServlet.java:89:74:89:88 | getBytes(...) | +| ScriptEngineTest.java:20:44:20:55 | input : String | ScriptEngineTest.java:24:37:24:41 | input | +| ScriptEngineTest.java:27:51:27:62 | input : String | ScriptEngineTest.java:31:31:31:35 | input | +| ScriptEngineTest.java:35:58:35:69 | input : String | ScriptEngineTest.java:39:31:39:35 | input | +| ScriptEngineTest.java:42:46:42:57 | input : String | ScriptEngineTest.java:46:31:46:35 | input | +| ScriptEngineTest.java:49:41:49:52 | input : String | ScriptEngineTest.java:52:42:52:46 | input | +| ScriptEngineTest.java:56:41:56:52 | input : String | ScriptEngineTest.java:59:51:59:55 | input | +| ScriptEngineTest.java:91:18:91:45 | getParameter(...) : String | ScriptEngineTest.java:93:57:93:60 | code : String | +| ScriptEngineTest.java:91:18:91:45 | getParameter(...) : String | ScriptEngineTest.java:94:64:94:67 | code : String | +| ScriptEngineTest.java:91:18:91:45 | getParameter(...) : String | ScriptEngineTest.java:95:71:95:74 | code : String | +| ScriptEngineTest.java:91:18:91:45 | getParameter(...) : String | ScriptEngineTest.java:96:59:96:62 | code : String | +| ScriptEngineTest.java:91:18:91:45 | getParameter(...) : String | ScriptEngineTest.java:97:54:97:57 | code : String | +| ScriptEngineTest.java:91:18:91:45 | getParameter(...) : String | ScriptEngineTest.java:98:54:98:57 | code : String | +| ScriptEngineTest.java:93:57:93:60 | code : String | ScriptEngineTest.java:20:44:20:55 | input : String | +| ScriptEngineTest.java:94:64:94:67 | code : String | ScriptEngineTest.java:27:51:27:62 | input : String | +| ScriptEngineTest.java:95:71:95:74 | code : String | ScriptEngineTest.java:35:58:35:69 | input : String | +| ScriptEngineTest.java:96:59:96:62 | code : String | ScriptEngineTest.java:42:46:42:57 | input : String | +| ScriptEngineTest.java:97:54:97:57 | code : String | ScriptEngineTest.java:49:41:49:52 | input : String | +| ScriptEngineTest.java:98:54:98:57 | code : String | ScriptEngineTest.java:56:41:56:52 | input : String | nodes | RhinoServlet.java:28:23:28:50 | getParameter(...) : String | semmle.label | getParameter(...) : String | | RhinoServlet.java:32:55:32:58 | code | semmle.label | code | -| RhinoServlet.java:84:23:84:50 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| RhinoServlet.java:86:54:86:57 | code | semmle.label | code | -| RhinoServlet.java:91:23:91:50 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| RhinoServlet.java:92:74:92:88 | getBytes(...) | semmle.label | getBytes(...) | -| ScriptEngineTest.java:8:44:8:55 | input : String | semmle.label | input : String | -| ScriptEngineTest.java:12:37:12:41 | input | semmle.label | input | -| ScriptEngineTest.java:15:51:15:62 | input : String | semmle.label | input : String | -| ScriptEngineTest.java:19:31:19:35 | input | semmle.label | input | -| ScriptEngineTest.java:23:58:23:69 | input : String | semmle.label | input : String | -| ScriptEngineTest.java:27:31:27:35 | input | semmle.label | input | -| ScriptEngineTest.java:30:46:30:57 | input : String | semmle.label | input : String | -| ScriptEngineTest.java:34:31:34:35 | input | semmle.label | input | -| ScriptEngineTest.java:37:41:37:52 | input : String | semmle.label | input : String | -| ScriptEngineTest.java:40:42:40:46 | input | semmle.label | input | -| ScriptEngineTest.java:44:41:44:52 | input : String | semmle.label | input : String | -| ScriptEngineTest.java:47:51:47:55 | input | semmle.label | input | -| ScriptEngineTest.java:51:26:51:38 | args : String[] | semmle.label | args : String[] | -| ScriptEngineTest.java:52:56:52:62 | ...[...] : String | semmle.label | ...[...] : String | -| ScriptEngineTest.java:53:63:53:69 | ...[...] : String | semmle.label | ...[...] : String | -| ScriptEngineTest.java:54:70:54:76 | ...[...] : String | semmle.label | ...[...] : String | -| ScriptEngineTest.java:55:58:55:64 | ...[...] : String | semmle.label | ...[...] : String | -| ScriptEngineTest.java:56:53:56:59 | ...[...] : String | semmle.label | ...[...] : String | -| ScriptEngineTest.java:57:53:57:59 | ...[...] : String | semmle.label | ...[...] : String | +| RhinoServlet.java:81:23:81:50 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| RhinoServlet.java:83:54:83:57 | code | semmle.label | code | +| RhinoServlet.java:88:23:88:50 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| RhinoServlet.java:89:74:89:88 | getBytes(...) | semmle.label | getBytes(...) | +| ScriptEngineTest.java:20:44:20:55 | input : String | semmle.label | input : String | +| ScriptEngineTest.java:24:37:24:41 | input | semmle.label | input | +| ScriptEngineTest.java:27:51:27:62 | input : String | semmle.label | input : String | +| ScriptEngineTest.java:31:31:31:35 | input | semmle.label | input | +| ScriptEngineTest.java:35:58:35:69 | input : String | semmle.label | input : String | +| ScriptEngineTest.java:39:31:39:35 | input | semmle.label | input | +| ScriptEngineTest.java:42:46:42:57 | input : String | semmle.label | input : String | +| ScriptEngineTest.java:46:31:46:35 | input | semmle.label | input | +| ScriptEngineTest.java:49:41:49:52 | input : String | semmle.label | input : String | +| ScriptEngineTest.java:52:42:52:46 | input | semmle.label | input | +| ScriptEngineTest.java:56:41:56:52 | input : String | semmle.label | input : String | +| ScriptEngineTest.java:59:51:59:55 | input | semmle.label | input | +| ScriptEngineTest.java:91:18:91:45 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| ScriptEngineTest.java:93:57:93:60 | code : String | semmle.label | code : String | +| ScriptEngineTest.java:94:64:94:67 | code : String | semmle.label | code : String | +| ScriptEngineTest.java:95:71:95:74 | code : String | semmle.label | code : String | +| ScriptEngineTest.java:96:59:96:62 | code : String | semmle.label | code : String | +| ScriptEngineTest.java:97:54:97:57 | code : String | semmle.label | code : String | +| ScriptEngineTest.java:98:54:98:57 | code : String | semmle.label | code : String | #select | RhinoServlet.java:32:29:32:78 | evaluateString(...) | RhinoServlet.java:28:23:28:50 | getParameter(...) : String | RhinoServlet.java:32:55:32:58 | code | Java Script Engine evaluate $@. | RhinoServlet.java:28:23:28:50 | getParameter(...) | user input | -| RhinoServlet.java:86:25:86:97 | compileToClassFiles(...) | RhinoServlet.java:84:23:84:50 | getParameter(...) : String | RhinoServlet.java:86:54:86:57 | code | Java Script Engine evaluate $@. | RhinoServlet.java:84:23:84:50 | getParameter(...) | user input | -| RhinoServlet.java:92:23:92:89 | defineClass(...) | RhinoServlet.java:91:23:91:50 | getParameter(...) : String | RhinoServlet.java:92:74:92:88 | getBytes(...) | Java Script Engine evaluate $@. | RhinoServlet.java:91:23:91:50 | getParameter(...) | user input | -| ScriptEngineTest.java:12:19:12:42 | eval(...) | ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:12:37:12:41 | input | Java Script Engine evaluate $@. | ScriptEngineTest.java:51:26:51:38 | args | user input | -| ScriptEngineTest.java:19:19:19:36 | eval(...) | ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:19:31:19:35 | input | Java Script Engine evaluate $@. | ScriptEngineTest.java:51:26:51:38 | args | user input | -| ScriptEngineTest.java:27:19:27:36 | eval(...) | ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:27:31:27:35 | input | Java Script Engine evaluate $@. | ScriptEngineTest.java:51:26:51:38 | args | user input | -| ScriptEngineTest.java:34:19:34:36 | eval(...) | ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:34:31:34:35 | input | Java Script Engine evaluate $@. | ScriptEngineTest.java:51:26:51:38 | args | user input | -| ScriptEngineTest.java:40:27:40:47 | compile(...) | ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:40:42:40:46 | input | Java Script Engine evaluate $@. | ScriptEngineTest.java:51:26:51:38 | args | user input | -| ScriptEngineTest.java:47:20:47:56 | getProgram(...) | ScriptEngineTest.java:51:26:51:38 | args : String[] | ScriptEngineTest.java:47:51:47:55 | input | Java Script Engine evaluate $@. | ScriptEngineTest.java:51:26:51:38 | args | user input | +| RhinoServlet.java:83:25:83:97 | compileToClassFiles(...) | RhinoServlet.java:81:23:81:50 | getParameter(...) : String | RhinoServlet.java:83:54:83:57 | code | Java Script Engine evaluate $@. | RhinoServlet.java:81:23:81:50 | getParameter(...) | user input | +| RhinoServlet.java:89:23:89:89 | defineClass(...) | RhinoServlet.java:88:23:88:50 | getParameter(...) : String | RhinoServlet.java:89:74:89:88 | getBytes(...) | Java Script Engine evaluate $@. | RhinoServlet.java:88:23:88:50 | getParameter(...) | user input | +| ScriptEngineTest.java:24:19:24:42 | eval(...) | ScriptEngineTest.java:91:18:91:45 | getParameter(...) : String | ScriptEngineTest.java:24:37:24:41 | input | Java Script Engine evaluate $@. | ScriptEngineTest.java:91:18:91:45 | getParameter(...) | user input | +| ScriptEngineTest.java:31:19:31:36 | eval(...) | ScriptEngineTest.java:91:18:91:45 | getParameter(...) : String | ScriptEngineTest.java:31:31:31:35 | input | Java Script Engine evaluate $@. | ScriptEngineTest.java:91:18:91:45 | getParameter(...) | user input | +| ScriptEngineTest.java:39:19:39:36 | eval(...) | ScriptEngineTest.java:91:18:91:45 | getParameter(...) : String | ScriptEngineTest.java:39:31:39:35 | input | Java Script Engine evaluate $@. | ScriptEngineTest.java:91:18:91:45 | getParameter(...) | user input | +| ScriptEngineTest.java:46:19:46:36 | eval(...) | ScriptEngineTest.java:91:18:91:45 | getParameter(...) : String | ScriptEngineTest.java:46:31:46:35 | input | Java Script Engine evaluate $@. | ScriptEngineTest.java:91:18:91:45 | getParameter(...) | user input | +| ScriptEngineTest.java:52:27:52:47 | compile(...) | ScriptEngineTest.java:91:18:91:45 | getParameter(...) : String | ScriptEngineTest.java:52:42:52:46 | input | Java Script Engine evaluate $@. | ScriptEngineTest.java:91:18:91:45 | getParameter(...) | user input | +| ScriptEngineTest.java:59:20:59:56 | getProgram(...) | ScriptEngineTest.java:91:18:91:45 | getParameter(...) : String | ScriptEngineTest.java:59:51:59:55 | input | Java Script Engine evaluate $@. | ScriptEngineTest.java:91:18:91:45 | getParameter(...) | user input | From 0ac8453398d44181ba9c0334fc8dd3e82ff31298 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Fri, 14 May 2021 12:50:24 +0000 Subject: [PATCH 64/74] Allow all arguments of methods in ScriptEngineFactory --- .../Security/CWE/CWE-094/ScriptInjection.ql | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql index 0819f85c17e..9faca9b8630 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql @@ -26,6 +26,17 @@ class ScriptEngineMethod extends Method { this.getDeclaringType().getASupertype*().hasQualifiedName("javax.script", "ScriptEngineFactory") and this.hasName(["getProgram", "getMethodCallSyntax"]) } + + /** Holds if the index is for an injectable parameter. */ + bindingset[index] + predicate isInjectableArgIndex(int index) { + if + this.getDeclaringType() + .getASupertype*() + .hasQualifiedName("javax.script", "ScriptEngineFactory") + then any() + else index = 0 + } } /** The context class `org.mozilla.javascript.Context` of Rhino Java Script Engine. */ @@ -71,9 +82,10 @@ class RhinoDefineClassMethod extends Method { /** Holds if `ma` is a method access of `ScriptEngineMethod`. */ predicate scriptEngine(MethodAccess ma, Expr sink) { - exists(Method m | m = ma.getMethod() | - m instanceof ScriptEngineMethod and - sink = ma.getArgument(0) + exists(ScriptEngineMethod m, int index | + m = ma.getMethod() and + m.isInjectableArgIndex(index) and + sink = ma.getArgument(index) ) } From 2c1374bdcf416823fda65365d2296c0961537b31 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Fri, 14 May 2021 20:05:56 +0000 Subject: [PATCH 65/74] Use inline implementation for ScriptEngineFactory --- .../Security/CWE/CWE-094/ScriptInjection.ql | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql index 9faca9b8630..5274140158c 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql @@ -26,17 +26,6 @@ class ScriptEngineMethod extends Method { this.getDeclaringType().getASupertype*().hasQualifiedName("javax.script", "ScriptEngineFactory") and this.hasName(["getProgram", "getMethodCallSyntax"]) } - - /** Holds if the index is for an injectable parameter. */ - bindingset[index] - predicate isInjectableArgIndex(int index) { - if - this.getDeclaringType() - .getASupertype*() - .hasQualifiedName("javax.script", "ScriptEngineFactory") - then any() - else index = 0 - } } /** The context class `org.mozilla.javascript.Context` of Rhino Java Script Engine. */ @@ -82,10 +71,11 @@ class RhinoDefineClassMethod extends Method { /** Holds if `ma` is a method access of `ScriptEngineMethod`. */ predicate scriptEngine(MethodAccess ma, Expr sink) { - exists(ScriptEngineMethod m, int index | + exists(ScriptEngineMethod m | m = ma.getMethod() and - m.isInjectableArgIndex(index) and - sink = ma.getArgument(index) + if m.getDeclaringType().getASupertype*().hasQualifiedName("javax.script", "ScriptEngineFactory") + then sink = ma.getArgument(_) // all arguments allow script injection + else sink = ma.getArgument(0) ) } From 2fa249a8eba1314acca9e7a953f5c3ed658c5b39 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Fri, 14 May 2021 20:31:05 +0000 Subject: [PATCH 66/74] Update method name and qldoc --- .../Security/CWE/CWE-094/ScriptInjection.ql | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql index 5274140158c..71accb28c37 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql @@ -69,8 +69,11 @@ class RhinoDefineClassMethod extends Method { } } -/** Holds if `ma` is a method access of `ScriptEngineMethod`. */ -predicate scriptEngine(MethodAccess ma, Expr sink) { +/** + * Holds if `ma` is a call to a `ScriptEngineMethod` and `sink` is an argument that + * will be executed. + */ +predicate isScriptArgument(MethodAccess ma, Expr sink) { exists(ScriptEngineMethod m | m = ma.getMethod() and if m.getDeclaringType().getASupertype*().hasQualifiedName("javax.script", "ScriptEngineFactory") @@ -113,7 +116,7 @@ predicate defineClass(MethodAccess ma, Expr sink) { /** A script injection sink. */ class ScriptInjectionSink extends DataFlow::ExprNode { ScriptInjectionSink() { - scriptEngine(_, this.getExpr()) or + isScriptArgument(_, this.getExpr()) or evaluateRhinoExpression(_, this.getExpr()) or compileScript(_, this.getExpr()) or defineClass(_, this.getExpr()) @@ -121,7 +124,7 @@ class ScriptInjectionSink extends DataFlow::ExprNode { /** An access to the method associated with this sink. */ MethodAccess getMethodAccess() { - scriptEngine(result, this.getExpr()) or + isScriptArgument(result, this.getExpr()) or evaluateRhinoExpression(result, this.getExpr()) or compileScript(result, this.getExpr()) or defineClass(result, this.getExpr()) From 9d392263a51e77783b78022839aa52ab37c7ba64 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Mon, 17 May 2021 16:07:00 +0000 Subject: [PATCH 67/74] Refactor inconsistent method names --- .../Security/CWE/CWE-094/ScriptInjection.ql | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql index 71accb28c37..aa5a8b74f93 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql @@ -85,7 +85,7 @@ predicate isScriptArgument(MethodAccess ma, Expr sink) { /** * Holds if a Rhino expression evaluation method is vulnerable to code injection. */ -predicate evaluateRhinoExpression(MethodAccess ma, Expr sink) { +predicate evaluatesRhinoExpression(MethodAccess ma, Expr sink) { exists(RhinoEvaluateExpressionMethod m | m = ma.getMethod() | ( if ma.getMethod().getName() = "compileReader" @@ -102,14 +102,14 @@ predicate evaluateRhinoExpression(MethodAccess ma, Expr sink) { /** * Holds if a Rhino expression compilation method is vulnerable to code injection. */ -predicate compileScript(MethodAccess ma, Expr sink) { +predicate compilesScript(MethodAccess ma, Expr sink) { exists(RhinoCompileClassMethod m | m = ma.getMethod() | sink = ma.getArgument(0)) } /** * Holds if a Rhino class loading method is vulnerable to code injection. */ -predicate defineClass(MethodAccess ma, Expr sink) { +predicate definesRhinoClass(MethodAccess ma, Expr sink) { exists(RhinoDefineClassMethod m | m = ma.getMethod() | sink = ma.getArgument(1)) } @@ -117,17 +117,17 @@ predicate defineClass(MethodAccess ma, Expr sink) { class ScriptInjectionSink extends DataFlow::ExprNode { ScriptInjectionSink() { isScriptArgument(_, this.getExpr()) or - evaluateRhinoExpression(_, this.getExpr()) or - compileScript(_, this.getExpr()) or - defineClass(_, this.getExpr()) + evaluatesRhinoExpression(_, this.getExpr()) or + compilesScript(_, this.getExpr()) or + definesRhinoClass(_, this.getExpr()) } /** An access to the method associated with this sink. */ MethodAccess getMethodAccess() { isScriptArgument(result, this.getExpr()) or - evaluateRhinoExpression(result, this.getExpr()) or - compileScript(result, this.getExpr()) or - defineClass(result, this.getExpr()) + evaluatesRhinoExpression(result, this.getExpr()) or + compilesScript(result, this.getExpr()) or + definesRhinoClass(result, this.getExpr()) } } From d4323a4a540372b0d3a38ebe8a8e53d2af3de462 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Mon, 17 May 2021 20:15:09 +0000 Subject: [PATCH 68/74] Update qldoc --- .../experimental/Security/CWE/CWE-094/ScriptInjection.qhelp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.qhelp b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.qhelp index 586304ebb7c..2683cf9ad29 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.qhelp +++ b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.qhelp @@ -4,7 +4,7 @@ -

    The Java Scripting API has been available since the release of Java 6, which allows +

    The Java Scripting API has been available since the release of Java 6. It allows applications to interact with scripts written in languages such as JavaScript. It serves as an embedded scripting engine inside Java applications which allows Java-to-JavaScript interoperability and provides a seamless integration between the two languages. If an @@ -21,7 +21,7 @@ -

    The following code could execute random JavaScript code in ScriptEngine

    +

    The following code could execute user-supplied JavaScript code in ScriptEngine

    From 02aa9c6fc7bde93ab56a1dc78423499d023978fe Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Tue, 18 May 2021 12:09:52 +0000 Subject: [PATCH 69/74] Optimize the sink and update qldoc --- .../Security/CWE/CWE-094/ScriptInjection.ql | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql index aa5a8b74f93..fb8bf867501 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-094/ScriptInjection.ql @@ -115,22 +115,23 @@ predicate definesRhinoClass(MethodAccess ma, Expr sink) { /** A script injection sink. */ class ScriptInjectionSink extends DataFlow::ExprNode { + MethodAccess methodAccess; + ScriptInjectionSink() { - isScriptArgument(_, this.getExpr()) or - evaluatesRhinoExpression(_, this.getExpr()) or - compilesScript(_, this.getExpr()) or - definesRhinoClass(_, this.getExpr()) + isScriptArgument(methodAccess, this.getExpr()) or + evaluatesRhinoExpression(methodAccess, this.getExpr()) or + compilesScript(methodAccess, this.getExpr()) or + definesRhinoClass(methodAccess, this.getExpr()) } /** An access to the method associated with this sink. */ - MethodAccess getMethodAccess() { - isScriptArgument(result, this.getExpr()) or - evaluatesRhinoExpression(result, this.getExpr()) or - compilesScript(result, this.getExpr()) or - definesRhinoClass(result, this.getExpr()) - } + MethodAccess getMethodAccess() { result = methodAccess } } +/** + * A taint tracking configuration that tracks flow from `RemoteFlowSource` to an argument + * of a method call that executes injected script. + */ class ScriptInjectionConfiguration extends TaintTracking::Configuration { ScriptInjectionConfiguration() { this = "ScriptInjectionConfiguration" } From 6103aabdce00d9c2d7683b47c8a71af86647cb32 Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Tue, 18 May 2021 19:17:11 +0200 Subject: [PATCH 70/74] C++: Add change-note. --- cpp/change-notes/2021-05-18-static-buffer-overflow.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 cpp/change-notes/2021-05-18-static-buffer-overflow.md diff --git a/cpp/change-notes/2021-05-18-static-buffer-overflow.md b/cpp/change-notes/2021-05-18-static-buffer-overflow.md new file mode 100644 index 00000000000..f56040fe8aa --- /dev/null +++ b/cpp/change-notes/2021-05-18-static-buffer-overflow.md @@ -0,0 +1,2 @@ +lgtm +* The "Static buffer overflow" query (cpp/static-buffer-overflow) has been improved to produce fewer false positives. From e9d2dd0b5709fa0015bc76415f5e29f24e1c32b6 Mon Sep 17 00:00:00 2001 From: Erik Krogh Kristensen Date: Tue, 18 May 2021 12:57:49 +0200 Subject: [PATCH 71/74] support the chaining methods on Express apps --- .../ql/src/semmle/javascript/frameworks/Express.qll | 3 +++ .../CWE-079/ReflectedXss/ReflectedXss.expected | 10 ++++++++++ .../ReflectedXssWithCustomSanitizer.expected | 1 + .../query-tests/Security/CWE-079/ReflectedXss/tst3.js | 7 +++++++ 4 files changed, 21 insertions(+) create mode 100644 javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/tst3.js diff --git a/javascript/ql/src/semmle/javascript/frameworks/Express.qll b/javascript/ql/src/semmle/javascript/frameworks/Express.qll index 53266c10ef8..7ef4bf7525a 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/Express.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/Express.qll @@ -19,6 +19,9 @@ module Express { or // `app = express.createServer()` result = DataFlow::moduleMember("express", "createServer").getAnInvocation() + or + // `app = express().disable(x)`, and other chaining methods + result = appCreation().getAMemberCall(["engine", "set", "param", "enable", "disable", "on"]) } /** diff --git a/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXss.expected b/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXss.expected index e8d093325ad..b1d940b437c 100644 --- a/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXss.expected +++ b/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXss.expected @@ -182,6 +182,11 @@ nodes | tst2.js:36:12:36:12 | p | | tst2.js:37:12:37:18 | other.p | | tst2.js:37:12:37:18 | other.p | +| tst3.js:5:7:5:24 | p | +| tst3.js:5:9:5:9 | p | +| tst3.js:5:9:5:9 | p | +| tst3.js:6:12:6:12 | p | +| tst3.js:6:12:6:12 | p | edges | ReflectedXss.js:8:33:8:45 | req.params.id | ReflectedXss.js:8:14:8:45 | "Unknow ... rams.id | | ReflectedXss.js:8:33:8:45 | req.params.id | ReflectedXss.js:8:14:8:45 | "Unknow ... rams.id | @@ -333,6 +338,10 @@ edges | tst2.js:30:9:30:9 | p | tst2.js:30:7:30:24 | p | | tst2.js:33:11:33:11 | p | tst2.js:37:12:37:18 | other.p | | tst2.js:33:11:33:11 | p | tst2.js:37:12:37:18 | other.p | +| tst3.js:5:7:5:24 | p | tst3.js:6:12:6:12 | p | +| tst3.js:5:7:5:24 | p | tst3.js:6:12:6:12 | p | +| tst3.js:5:9:5:9 | p | tst3.js:5:7:5:24 | p | +| tst3.js:5:9:5:9 | p | tst3.js:5:7:5:24 | p | #select | ReflectedXss.js:8:14:8:45 | "Unknow ... rams.id | ReflectedXss.js:8:33:8:45 | req.params.id | ReflectedXss.js:8:14:8:45 | "Unknow ... rams.id | Cross-site scripting vulnerability due to $@. | ReflectedXss.js:8:33:8:45 | req.params.id | user-provided value | | ReflectedXss.js:17:12:17:39 | "Unknow ... rams.id | ReflectedXss.js:17:31:17:39 | params.id | ReflectedXss.js:17:12:17:39 | "Unknow ... rams.id | Cross-site scripting vulnerability due to $@. | ReflectedXss.js:17:31:17:39 | params.id | user-provided value | @@ -376,3 +385,4 @@ edges | tst2.js:21:14:21:14 | p | tst2.js:14:9:14:9 | p | tst2.js:21:14:21:14 | p | Cross-site scripting vulnerability due to $@. | tst2.js:14:9:14:9 | p | user-provided value | | tst2.js:36:12:36:12 | p | tst2.js:30:9:30:9 | p | tst2.js:36:12:36:12 | p | Cross-site scripting vulnerability due to $@. | tst2.js:30:9:30:9 | p | user-provided value | | tst2.js:37:12:37:18 | other.p | tst2.js:30:9:30:9 | p | tst2.js:37:12:37:18 | other.p | Cross-site scripting vulnerability due to $@. | tst2.js:30:9:30:9 | p | user-provided value | +| tst3.js:6:12:6:12 | p | tst3.js:5:9:5:9 | p | tst3.js:6:12:6:12 | p | Cross-site scripting vulnerability due to $@. | tst3.js:5:9:5:9 | p | user-provided value | diff --git a/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXssWithCustomSanitizer.expected b/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXssWithCustomSanitizer.expected index 8ddc55dde36..6a919f7b43a 100644 --- a/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXssWithCustomSanitizer.expected +++ b/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/ReflectedXssWithCustomSanitizer.expected @@ -39,3 +39,4 @@ | tst2.js:21:14:21:14 | p | Cross-site scripting vulnerability due to $@. | tst2.js:14:9:14:9 | p | user-provided value | | tst2.js:36:12:36:12 | p | Cross-site scripting vulnerability due to $@. | tst2.js:30:9:30:9 | p | user-provided value | | tst2.js:37:12:37:18 | other.p | Cross-site scripting vulnerability due to $@. | tst2.js:30:9:30:9 | p | user-provided value | +| tst3.js:6:12:6:12 | p | Cross-site scripting vulnerability due to $@. | tst3.js:5:9:5:9 | p | user-provided value | diff --git a/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/tst3.js b/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/tst3.js new file mode 100644 index 00000000000..1fa1758c41c --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-079/ReflectedXss/tst3.js @@ -0,0 +1,7 @@ +var express = require('express'); + +var app = express(); +app.enable('x-powered-by').disable('x-powered-by').get('/', function (req, res) { + let { p } = req.params; + res.send(p); // NOT OK +}); From 9a1f80aa93279249aea0709c0f227cff8d1dfc7e Mon Sep 17 00:00:00 2001 From: Erik Krogh Kristensen Date: Tue, 18 May 2021 22:21:30 +0200 Subject: [PATCH 72/74] accept updated test output for express test --- .../ql/test/library-tests/frameworks/Express/tests.expected | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/javascript/ql/test/library-tests/frameworks/Express/tests.expected b/javascript/ql/test/library-tests/frameworks/Express/tests.expected index c6b9ec97fc0..586a404b48a 100644 --- a/javascript/ql/test/library-tests/frameworks/Express/tests.expected +++ b/javascript/ql/test/library-tests/frameworks/Express/tests.expected @@ -1422,6 +1422,7 @@ test_appCreation | src/express.js:2:11:2:19 | express() | | src/inheritedFromNode.js:2:11:2:19 | express() | | src/params.js:2:11:2:19 | express() | +| src/params.js:4:1:12:2 | app.par ... }\\n}) | | src/responseExprs.js:2:11:2:19 | express() | | src/routesetups.js:7:11:7:32 | express ... erver() | | src/subrouter.js:2:11:2:19 | express() | @@ -1519,6 +1520,7 @@ test_RouteExpr | src/express.js:46:1:51:2 | app.pos ... me];\\n}) | src/express.js:2:11:2:19 | express() | | src/inheritedFromNode.js:4:1:8:2 | app.pos ... url;\\n}) | src/inheritedFromNode.js:2:11:2:19 | express() | | src/params.js:4:1:12:2 | app.par ... }\\n}) | src/params.js:2:11:2:19 | express() | +| src/params.js:4:1:12:2 | app.par ... }\\n}) | src/params.js:4:1:12:2 | app.par ... }\\n}) | | src/params.js:14:1:16:2 | app.get ... o");\\n}) | src/params.js:2:11:2:19 | express() | | src/responseExprs.js:4:1:6:2 | app.get ... res1\\n}) | src/responseExprs.js:2:11:2:19 | express() | | src/responseExprs.js:7:1:9:2 | app.get ... es2;\\n}) | src/responseExprs.js:2:11:2:19 | express() | @@ -2174,6 +2176,7 @@ test_isRouterCreation | src/express.js:2:11:2:19 | express() | | src/inheritedFromNode.js:2:11:2:19 | express() | | src/params.js:2:11:2:19 | express() | +| src/params.js:4:1:12:2 | app.par ... }\\n}) | | src/responseExprs.js:2:11:2:19 | express() | | src/route.js:2:14:2:29 | express.Router() | | src/routesetups.js:3:1:3:16 | express.Router() | @@ -2264,6 +2267,7 @@ test_RouterDefinition_RouterDefinition | src/express.js:2:11:2:19 | express() | | src/inheritedFromNode.js:2:11:2:19 | express() | | src/params.js:2:11:2:19 | express() | +| src/params.js:4:1:12:2 | app.par ... }\\n}) | | src/responseExprs.js:2:11:2:19 | express() | | src/route.js:2:14:2:29 | express.Router() | | src/routesetups.js:3:1:3:16 | express.Router() | From 741eed93b2ddde49d69413cdac4d4511018f40d8 Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Wed, 19 May 2021 09:03:05 +0200 Subject: [PATCH 73/74] C++: Replace minimum(any(...)) with a min aggregate. Also removed the min aggregate further down since it's no longer needed. --- cpp/ql/src/Critical/OverflowStatic.ql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/ql/src/Critical/OverflowStatic.ql b/cpp/ql/src/Critical/OverflowStatic.ql index a7c1cb41cda..011c38ff734 100644 --- a/cpp/ql/src/Critical/OverflowStatic.ql +++ b/cpp/ql/src/Critical/OverflowStatic.ql @@ -101,7 +101,7 @@ class CallWithBufferSize extends FunctionCall { // result in this case we pick the minimum value obtainable from dataflow and range analysis. result = upperBound(statedSizeExpr()) - .minimum(any(Expr statedSizeSrc | + .minimum(min(Expr statedSizeSrc | DataFlow::localExprFlow(statedSizeSrc, statedSizeExpr()) | statedSizeSrc.getValue().toInt() @@ -112,7 +112,7 @@ class CallWithBufferSize extends FunctionCall { predicate wrongBufferSize(Expr error, string msg) { exists(CallWithBufferSize call, int bufsize, Variable buf, int statedSize | staticBuffer(call.buffer(), buf, bufsize) and - statedSize = min(call.statedSizeValue()) and + statedSize = call.statedSizeValue() and statedSize > bufsize and error = call.statedSizeExpr() and msg = From 4d0051360684e04731d26bfb5556c73b7b1f54de Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Wed, 19 May 2021 10:47:04 +0200 Subject: [PATCH 74/74] C++: Use the isParameterDerefOrQualifierObject predicate to remove a disjunction. --- .../semmle/code/cpp/ir/dataflow/internal/DataFlowUtil.qll | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/cpp/ql/src/semmle/code/cpp/ir/dataflow/internal/DataFlowUtil.qll b/cpp/ql/src/semmle/code/cpp/ir/dataflow/internal/DataFlowUtil.qll index 8ed61da4c92..6f0d6ff92ff 100644 --- a/cpp/ql/src/semmle/code/cpp/ir/dataflow/internal/DataFlowUtil.qll +++ b/cpp/ql/src/semmle/code/cpp/ir/dataflow/internal/DataFlowUtil.qll @@ -748,16 +748,10 @@ private predicate modelFlow(Operand opFrom, Instruction iTo) { ) or exists(int index, ReadSideEffectInstruction read | - modelIn.isParameterDeref(index) and + modelIn.isParameterDerefOrQualifierObject(index) and read = getSideEffectFor(call, index) and opFrom = read.getSideEffectOperand() ) - or - exists(ReadSideEffectInstruction read | - modelIn.isQualifierObject() and - read = getSideEffectFor(call, -1) and - opFrom = read.getSideEffectOperand() - ) ) ) }