Skip to content

Commit ab21990

Browse files
committed
add jsonpickle and pexpect libs in case of unsafe decoding and secondary command execution, add proper test cases
1 parent 3e6b4a1 commit ab21990

File tree

13 files changed

+169
-2
lines changed

13 files changed

+169
-2
lines changed

python/ql/lib/semmle/python/Frameworks.qll

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ private import semmle.python.frameworks.Idna
3434
private import semmle.python.frameworks.Invoke
3535
private import semmle.python.frameworks.Jmespath
3636
private import semmle.python.frameworks.Joblib
37+
private import semmle.python.frameworks.JsonPickle
3738
private import semmle.python.frameworks.Ldap
3839
private import semmle.python.frameworks.Ldap3
3940
private import semmle.python.frameworks.Libtaxii
@@ -48,6 +49,7 @@ private import semmle.python.frameworks.Oracledb
4849
private import semmle.python.frameworks.Pandas
4950
private import semmle.python.frameworks.Paramiko
5051
private import semmle.python.frameworks.Peewee
52+
private import semmle.python.frameworks.Pexpect
5153
private import semmle.python.frameworks.Phoenixdb
5254
private import semmle.python.frameworks.Psycopg
5355
private import semmle.python.frameworks.Psycopg2
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `jsonpickle` PyPI package.
3+
* See https://pypi.org/project/jsonpickle/.
4+
*/
5+
6+
private import python
7+
private import semmle.python.dataflow.new.DataFlow
8+
private import semmle.python.dataflow.new.RemoteFlowSources
9+
private import semmle.python.Concepts
10+
private import semmle.python.ApiGraphs
11+
12+
/**
13+
* Provides models for the `jsonpickle` PyPI package.
14+
* See https://pypi.org/project/jsonpickle/.
15+
*/
16+
private module Jsonpickle {
17+
/**
18+
* A Call to `jsonpickle.decode`.
19+
* See https://jsonpickle.readthedocs.io/en/latest/api.html#jsonpickle.decode
20+
*/
21+
private class JsonpickleDecode extends Decoding::Range, API::CallNode {
22+
JsonpickleDecode() { this = API::moduleImport("jsonpickle").getMember("decode").getACall() }
23+
24+
override predicate mayExecuteInput() { any() }
25+
26+
override DataFlow::Node getAnInput() { result = this.getParameter(0, "string").asSink() }
27+
28+
override DataFlow::Node getOutput() { result = this }
29+
30+
override string getFormat() { result = "pickle" }
31+
}
32+
}

python/ql/lib/semmle/python/frameworks/Paramiko.qll

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,14 @@ private import semmle.python.ApiGraphs
1414
* See https://pypi.org/project/paramiko/.
1515
*/
1616
private module Paramiko {
17-
/*
17+
/**
1818
* The first argument of `paramiko.ProxyCommand`.
1919
*
2020
* the `paramiko.ProxyCommand` is equivalent of `ssh -o ProxyCommand="CMD"`
2121
* and it run CMD on current system that running the ssh command
2222
*
2323
* See https://paramiko.pydata.org/docs/reference/api/paramiko.eval.html
2424
*/
25-
2625
class ParamikoProxyCommand extends SystemCommandExecution::Range, API::CallNode {
2726
ParamikoProxyCommand() {
2827
this = API::moduleImport("paramiko").getMember("ProxyCommand").getACall()
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `pexpect` PyPI package.
3+
* See https://pypi.org/project/pexpect/.
4+
*/
5+
6+
private import python
7+
private import semmle.python.dataflow.new.DataFlow
8+
private import semmle.python.dataflow.new.RemoteFlowSources
9+
private import semmle.python.Concepts
10+
private import semmle.python.ApiGraphs
11+
12+
/**
13+
* Provides models for the `pexpect` PyPI package.
14+
* See https://pypi.org/project/pexpect/.
15+
*/
16+
private module Pexpect {
17+
/**
18+
* The calls to `pexpect.*` functions that execute commands
19+
* See https://pexpect.readthedocs.io/en/stable/api/pexpect.html#pexpect.spawn
20+
* See https://pexpect.readthedocs.io/en/stable/api/pexpect.html#pexpect.run
21+
*/
22+
class PexpectCommandExec extends SystemCommandExecution::Range, API::CallNode {
23+
PexpectCommandExec() {
24+
this = API::moduleImport("pexpect").getMember(["run", "runu", "spawn", "spawnu"]).getACall()
25+
}
26+
27+
override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() }
28+
29+
override predicate isShellInterpreted(DataFlow::Node arg) { none() }
30+
}
31+
32+
/**
33+
* A call to `pexpect.popen_spawn.PopenSpawn`
34+
* See https://pexpect.readthedocs.io/en/stable/api/popen_spawn.html#pexpect.popen_spawn.PopenSpawn
35+
*/
36+
class PexpectPopenSpawn extends SystemCommandExecution::Range, API::CallNode {
37+
PexpectPopenSpawn() {
38+
this =
39+
API::moduleImport("pexpect").getMember("popen_spawn").getMember("PopenSpawn").getACall()
40+
}
41+
42+
override DataFlow::Node getCommand() { result = this.getParameter(0, "cmd").asSink() }
43+
44+
override predicate isShellInterpreted(DataFlow::Node arg) { none() }
45+
}
46+
}

python/ql/src/experimental/semmle/python/Frameworks.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ private import experimental.semmle.python.frameworks.Werkzeug
1010
private import experimental.semmle.python.frameworks.LDAP
1111
private import experimental.semmle.python.frameworks.Netmiko
1212
private import experimental.semmle.python.frameworks.Paramiko
13+
private import experimental.semmle.python.frameworks.Pexpect
1314
private import experimental.semmle.python.frameworks.Scrapli
1415
private import experimental.semmle.python.frameworks.JWT
1516
private import experimental.semmle.python.frameworks.Csv
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `pexpect` PyPI package.
3+
* See https://pypi.org/project/pexpect/.
4+
*/
5+
6+
private import python
7+
private import semmle.python.dataflow.new.DataFlow
8+
private import semmle.python.dataflow.new.RemoteFlowSources
9+
private import semmle.python.ApiGraphs
10+
import experimental.semmle.python.Concepts
11+
12+
/**
13+
* Provides models for the `pexpect` PyPI package.
14+
* See https://pypi.org/project/pexpect/.
15+
*/
16+
private module Pexpect {
17+
/**
18+
* The calls to `pexpect.pxssh.pxssh` functions that execute commands
19+
* See https://pexpect.readthedocs.io/en/stable/api/pxssh.html
20+
*/
21+
class PexpectCommandExec extends SecondaryCommandInjection {
22+
PexpectCommandExec() {
23+
this =
24+
API::moduleImport("pexpect")
25+
.getMember("pxssh")
26+
.getMember("pxssh")
27+
.getReturn()
28+
.getMember(["send", "sendline"])
29+
.getACall()
30+
.getParameter(0, "s")
31+
.asSink()
32+
}
33+
}
34+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env python
2+
3+
from fastapi import FastAPI
4+
from pexpect import pxssh
5+
6+
ssh = pxssh.pxssh()
7+
hostname = "localhost"
8+
username = "username"
9+
password = "password"
10+
ssh.login(hostname, username, password)
11+
12+
app = FastAPI()
13+
14+
@app.get("/bad1")
15+
async def bad1(cmd: str):
16+
ssh.send(cmd) # $ result=BAD getSecondaryCommand=cmd
17+
ssh.prompt()
18+
ssh.sendline(cmd) # $ result=BAD getSecondaryCommand=cmd
19+
ssh.prompt()
20+
ssh.logout()
21+
return {"success": stdout}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
testFailures
2+
failures
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import python
2+
import experimental.meta.ConceptsTest
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import os
2+
3+
import jsonpickle
4+
5+
6+
class Thing(object):
7+
def __reduce__(self):
8+
return os.system, ("curl 127.0.0.1:1234",)
9+
10+
11+
obj = Thing()
12+
13+
pickledObj = jsonpickle.encode(obj)
14+
objUnPickled = jsonpickle.decode(pickledObj, safe=True) # $ decodeInput=pickledObj decodeOutput=jsonpickle.decode(..) decodeFormat=pickle decodeMayExecuteInput
15+
print(objUnPickled.name)

0 commit comments

Comments
 (0)