diff --git a/python/ql/lib/semmle/python/Frameworks.qll b/python/ql/lib/semmle/python/Frameworks.qll index f8298ffc7b7..569f7981e9d 100644 --- a/python/ql/lib/semmle/python/Frameworks.qll +++ b/python/ql/lib/semmle/python/Frameworks.qll @@ -34,6 +34,7 @@ private import semmle.python.frameworks.Idna private import semmle.python.frameworks.Invoke private import semmle.python.frameworks.Jmespath private import semmle.python.frameworks.Joblib +private import semmle.python.frameworks.JsonPickle private import semmle.python.frameworks.Ldap private import semmle.python.frameworks.Ldap3 private import semmle.python.frameworks.Libtaxii @@ -48,6 +49,7 @@ private import semmle.python.frameworks.Oracledb private import semmle.python.frameworks.Pandas private import semmle.python.frameworks.Paramiko private import semmle.python.frameworks.Peewee +private import semmle.python.frameworks.Pexpect private import semmle.python.frameworks.Phoenixdb private import semmle.python.frameworks.Psycopg private import semmle.python.frameworks.Psycopg2 diff --git a/python/ql/lib/semmle/python/frameworks/JsonPickle.qll b/python/ql/lib/semmle/python/frameworks/JsonPickle.qll new file mode 100644 index 00000000000..1c1d0c1f54f --- /dev/null +++ b/python/ql/lib/semmle/python/frameworks/JsonPickle.qll @@ -0,0 +1,32 @@ +/** + * Provides classes modeling security-relevant aspects of the `jsonpickle` PyPI package. + * See https://pypi.org/project/jsonpickle/. + */ + +private import python +private import semmle.python.dataflow.new.DataFlow +private import semmle.python.dataflow.new.RemoteFlowSources +private import semmle.python.Concepts +private import semmle.python.ApiGraphs + +/** + * Provides models for the `jsonpickle` PyPI package. + * See https://pypi.org/project/jsonpickle/. + */ +private module Jsonpickle { + /** + * A Call to `jsonpickle.decode`. + * See https://jsonpickle.readthedocs.io/en/latest/api.html#jsonpickle.decode + */ + private class JsonpickleDecode extends Decoding::Range, API::CallNode { + JsonpickleDecode() { this = API::moduleImport("jsonpickle").getMember("decode").getACall() } + + override predicate mayExecuteInput() { any() } + + override DataFlow::Node getAnInput() { result = this.getParameter(0, "string").asSink() } + + override DataFlow::Node getOutput() { result = this } + + override string getFormat() { result = "pickle" } + } +} diff --git a/python/ql/lib/semmle/python/frameworks/Paramiko.qll b/python/ql/lib/semmle/python/frameworks/Paramiko.qll index e41d4b5ad55..2599271cc0c 100644 --- a/python/ql/lib/semmle/python/frameworks/Paramiko.qll +++ b/python/ql/lib/semmle/python/frameworks/Paramiko.qll @@ -14,7 +14,7 @@ private import semmle.python.ApiGraphs * See https://pypi.org/project/paramiko/. */ private module Paramiko { - /* + /** * The first argument of `paramiko.ProxyCommand`. * * the `paramiko.ProxyCommand` is equivalent of `ssh -o ProxyCommand="CMD"` @@ -22,7 +22,6 @@ private module Paramiko { * * See https://paramiko.pydata.org/docs/reference/api/paramiko.eval.html */ - class ParamikoProxyCommand extends SystemCommandExecution::Range, API::CallNode { ParamikoProxyCommand() { this = API::moduleImport("paramiko").getMember("ProxyCommand").getACall() diff --git a/python/ql/lib/semmle/python/frameworks/Pexpect.qll b/python/ql/lib/semmle/python/frameworks/Pexpect.qll new file mode 100644 index 00000000000..768bc2c3de1 --- /dev/null +++ b/python/ql/lib/semmle/python/frameworks/Pexpect.qll @@ -0,0 +1,46 @@ +/** + * Provides classes modeling security-relevant aspects of the `pexpect` PyPI package. + * See https://pypi.org/project/pexpect/. + */ + +private import python +private import semmle.python.dataflow.new.DataFlow +private import semmle.python.dataflow.new.RemoteFlowSources +private import semmle.python.Concepts +private import semmle.python.ApiGraphs + +/** + * Provides models for the `pexpect` PyPI package. + * See https://pypi.org/project/pexpect/. + */ +private module Pexpect { + /** + * The calls to `pexpect.*` functions that execute commands + * See https://pexpect.readthedocs.io/en/stable/api/pexpect.html#pexpect.spawn + * See https://pexpect.readthedocs.io/en/stable/api/pexpect.html#pexpect.run + */ + class PexpectCommandExec extends SystemCommandExecution::Range, API::CallNode { + PexpectCommandExec() { + this = API::moduleImport("pexpect").getMember(["run", "runu", "spawn", "spawnu"]).getACall() + } + + override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() } + + override predicate isShellInterpreted(DataFlow::Node arg) { none() } + } + + /** + * A call to `pexpect.popen_spawn.PopenSpawn` + * See https://pexpect.readthedocs.io/en/stable/api/popen_spawn.html#pexpect.popen_spawn.PopenSpawn + */ + class PexpectPopenSpawn extends SystemCommandExecution::Range, API::CallNode { + PexpectPopenSpawn() { + this = + API::moduleImport("pexpect").getMember("popen_spawn").getMember("PopenSpawn").getACall() + } + + override DataFlow::Node getCommand() { result = this.getParameter(0, "cmd").asSink() } + + override predicate isShellInterpreted(DataFlow::Node arg) { none() } + } +} diff --git a/python/ql/src/experimental/semmle/python/Frameworks.qll b/python/ql/src/experimental/semmle/python/Frameworks.qll index 45ef3cf96b4..1d8fe10e911 100644 --- a/python/ql/src/experimental/semmle/python/Frameworks.qll +++ b/python/ql/src/experimental/semmle/python/Frameworks.qll @@ -10,6 +10,7 @@ private import experimental.semmle.python.frameworks.Werkzeug private import experimental.semmle.python.frameworks.LDAP private import experimental.semmle.python.frameworks.Netmiko private import experimental.semmle.python.frameworks.Paramiko +private import experimental.semmle.python.frameworks.Pexpect private import experimental.semmle.python.frameworks.Scrapli private import experimental.semmle.python.frameworks.JWT private import experimental.semmle.python.frameworks.Csv diff --git a/python/ql/src/experimental/semmle/python/frameworks/Pexpect.qll b/python/ql/src/experimental/semmle/python/frameworks/Pexpect.qll new file mode 100644 index 00000000000..86459d47c3d --- /dev/null +++ b/python/ql/src/experimental/semmle/python/frameworks/Pexpect.qll @@ -0,0 +1,34 @@ +/** + * Provides classes modeling security-relevant aspects of the `pexpect` PyPI package. + * See https://pypi.org/project/pexpect/. + */ + +private import python +private import semmle.python.dataflow.new.DataFlow +private import semmle.python.dataflow.new.RemoteFlowSources +private import semmle.python.ApiGraphs +import experimental.semmle.python.Concepts + +/** + * Provides models for the `pexpect` PyPI package. + * See https://pypi.org/project/pexpect/. + */ +private module Pexpect { + /** + * The calls to `pexpect.pxssh.pxssh` functions that execute commands + * See https://pexpect.readthedocs.io/en/stable/api/pxssh.html + */ + class PexpectCommandExec extends SecondaryCommandInjection { + PexpectCommandExec() { + this = + API::moduleImport("pexpect") + .getMember("pxssh") + .getMember("pxssh") + .getReturn() + .getMember(["send", "sendline"]) + .getACall() + .getParameter(0, "s") + .asSink() + } + } +} diff --git a/python/ql/test/experimental/query-tests/Security/CWE-074-SecondaryServerCmdInjection/Pexpect.py b/python/ql/test/experimental/query-tests/Security/CWE-074-SecondaryServerCmdInjection/Pexpect.py new file mode 100644 index 00000000000..c3ee7420e19 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-074-SecondaryServerCmdInjection/Pexpect.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python + +from fastapi import FastAPI +from pexpect import pxssh + +ssh = pxssh.pxssh() +hostname = "localhost" +username = "username" +password = "password" +ssh.login(hostname, username, password) + +app = FastAPI() + +@app.get("/bad1") +async def bad1(cmd: str): + ssh.send(cmd) # $ result=BAD getSecondaryCommand=cmd + ssh.prompt() + ssh.sendline(cmd) # $ result=BAD getSecondaryCommand=cmd + ssh.prompt() + ssh.logout() + return {"success": stdout} \ No newline at end of file diff --git a/python/ql/test/library-tests/frameworks/jsonpickle/ConceptsTest.expected b/python/ql/test/library-tests/frameworks/jsonpickle/ConceptsTest.expected new file mode 100644 index 00000000000..8ec8033d086 --- /dev/null +++ b/python/ql/test/library-tests/frameworks/jsonpickle/ConceptsTest.expected @@ -0,0 +1,2 @@ +testFailures +failures diff --git a/python/ql/test/library-tests/frameworks/jsonpickle/ConceptsTest.ql b/python/ql/test/library-tests/frameworks/jsonpickle/ConceptsTest.ql new file mode 100644 index 00000000000..b557a0bccb6 --- /dev/null +++ b/python/ql/test/library-tests/frameworks/jsonpickle/ConceptsTest.ql @@ -0,0 +1,2 @@ +import python +import experimental.meta.ConceptsTest diff --git a/python/ql/test/library-tests/frameworks/jsonpickle/Decode.py b/python/ql/test/library-tests/frameworks/jsonpickle/Decode.py new file mode 100644 index 00000000000..478e823a559 --- /dev/null +++ b/python/ql/test/library-tests/frameworks/jsonpickle/Decode.py @@ -0,0 +1,15 @@ +import os + +import jsonpickle + + +class Thing(object): + def __reduce__(self): + return os.system, ("curl 127.0.0.1:1234",) + + +obj = Thing() + +pickledObj = jsonpickle.encode(obj) +objUnPickled = jsonpickle.decode(pickledObj, safe=True) # $ decodeInput=pickledObj decodeOutput=jsonpickle.decode(..) decodeFormat=pickle decodeMayExecuteInput +print(objUnPickled.name) diff --git a/python/ql/test/library-tests/frameworks/pexpect/ConceptsTest.expected b/python/ql/test/library-tests/frameworks/pexpect/ConceptsTest.expected new file mode 100644 index 00000000000..8ec8033d086 --- /dev/null +++ b/python/ql/test/library-tests/frameworks/pexpect/ConceptsTest.expected @@ -0,0 +1,2 @@ +testFailures +failures diff --git a/python/ql/test/library-tests/frameworks/pexpect/ConceptsTest.ql b/python/ql/test/library-tests/frameworks/pexpect/ConceptsTest.ql new file mode 100644 index 00000000000..b557a0bccb6 --- /dev/null +++ b/python/ql/test/library-tests/frameworks/pexpect/ConceptsTest.ql @@ -0,0 +1,2 @@ +import python +import experimental.meta.ConceptsTest diff --git a/python/ql/test/library-tests/frameworks/pexpect/ExecCmd.py b/python/ql/test/library-tests/frameworks/pexpect/ExecCmd.py new file mode 100644 index 00000000000..5f8c0886909 --- /dev/null +++ b/python/ql/test/library-tests/frameworks/pexpect/ExecCmd.py @@ -0,0 +1,9 @@ +import pexpect +from pexpect import popen_spawn + +cmd = "ls -la" +result = pexpect.run(cmd) # $ getCommand=cmd +result = pexpect.runu(cmd) # $ getCommand=cmd +result = pexpect.spawn(cmd) # $ getCommand=cmd +result = pexpect.spawnu(cmd) # $ getCommand=cmd +result = popen_spawn.PopenSpawn(cmd) # $ getCommand=cmd \ No newline at end of file