Skip to content

Commit 8c4dccc

Browse files
committed
Python: initial support for CMDi via asyncio
1 parent 49f5d38 commit 8c4dccc

File tree

4 files changed

+253
-1
lines changed

4 files changed

+253
-1
lines changed

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

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4396,6 +4396,145 @@ private module StdlibPrivate {
43964396
preservesValue = true
43974397
}
43984398
}
4399+
4400+
// ---------------------------------------------------------------------------
4401+
// asyncio
4402+
// ---------------------------------------------------------------------------
4403+
/** Gets a reference to the `asyncio` module. */
4404+
API::Node asyncio() { result = API::moduleImport("asyncio") }
4405+
4406+
/** Provides models for the `asyncio` module. */
4407+
module AsyncIO {
4408+
/**
4409+
* A call to the `asyncio.create_subprocess_exec` function (also accessible via the `subprocess` module of `asyncio`)
4410+
*
4411+
* See https://docs.python.org/3/library/asyncio-subprocess.html#creating-subprocesses
4412+
*/
4413+
private class CreateSubprocessExec extends SystemCommandExecution::Range,
4414+
FileSystemAccess::Range, DataFlow::CallCfgNode
4415+
{
4416+
CreateSubprocessExec() {
4417+
exists(string name |
4418+
name = "create_subprocess_exec" and
4419+
(
4420+
this = asyncio().getMember(name).getACall()
4421+
or
4422+
this = asyncio().getMember("subprocess").getMember(name).getACall()
4423+
)
4424+
)
4425+
}
4426+
4427+
override DataFlow::Node getCommand() {
4428+
result = this.getArg(0)
4429+
or
4430+
result = this.getArgByName("program")
4431+
}
4432+
4433+
override DataFlow::Node getAPathArgument() { result = this.getCommand() }
4434+
4435+
override predicate isShellInterpreted(DataFlow::Node arg) {
4436+
none() // this is a safe API.
4437+
}
4438+
}
4439+
}
4440+
4441+
/**
4442+
* A call to the `asyncio.create_subprocess_shell` function (also in the `subprocess` module of `asyncio`)
4443+
*
4444+
* See https://docs.python.org/3/library/asyncio-subprocess.html#creating-subprocesses
4445+
*/
4446+
private class CreateSubprocessShell extends SystemCommandExecution::Range,
4447+
FileSystemAccess::Range, DataFlow::CallCfgNode
4448+
{
4449+
CreateSubprocessShell() {
4450+
exists(string name |
4451+
name = "create_subprocess_shell" and
4452+
(
4453+
this = asyncio().getMember(name).getACall()
4454+
or
4455+
this = asyncio().getMember("subprocess").getMember(name).getACall()
4456+
)
4457+
)
4458+
}
4459+
4460+
override DataFlow::Node getCommand() {
4461+
result = this.getArg(0)
4462+
or
4463+
result = this.getArgByName("cmd")
4464+
}
4465+
4466+
override DataFlow::Node getAPathArgument() { result = this.getCommand() }
4467+
4468+
override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getCommand() }
4469+
}
4470+
4471+
/**
4472+
* A source for an event loop (an object with basetype `AbstractEventLoop`).
4473+
*
4474+
* See https://docs.python.org/3/library/asyncio-eventloop.html
4475+
*/
4476+
private class EventLoopSource extends DataFlow::LocalSourceNode, DataFlow::CallCfgNode {
4477+
EventLoopSource() {
4478+
this = asyncio().getMember("get_running_loop").getACall()
4479+
or
4480+
this = asyncio().getMember("get_event_loop").getACall() // deprecated in Python 3.10.0 and later
4481+
or
4482+
this = asyncio().getMember("new_event_loop").getACall()
4483+
}
4484+
}
4485+
4486+
/** Gets a reference to an event loop instance. */
4487+
private DataFlow::TypeTrackingNode eventLoopInstance(DataFlow::TypeTracker t) {
4488+
t.start() and
4489+
result instanceof EventLoopSource
4490+
or
4491+
exists(DataFlow::TypeTracker t2 | result = eventLoopInstance(t2).track(t2, t))
4492+
}
4493+
4494+
/** Gets a reference to an event loop instance. */
4495+
DataFlow::Node eventLoopInstance() {
4496+
eventLoopInstance(DataFlow::TypeTracker::end()).flowsTo(result)
4497+
}
4498+
4499+
/**
4500+
* A call to `subprocess_exec` on an event loop instance.
4501+
*
4502+
* See https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.subprocess_exec
4503+
*/
4504+
private class EventLoopSubprocessExec extends DataFlow::MethodCallNode,
4505+
SystemCommandExecution::Range, FileSystemAccess::Range
4506+
{
4507+
EventLoopSubprocessExec() { this.calls(eventLoopInstance(), "subprocess_exec") }
4508+
4509+
override DataFlow::Node getCommand() { result = this.getArg(1) }
4510+
4511+
override DataFlow::Node getAPathArgument() { result = this.getCommand() }
4512+
4513+
override predicate isShellInterpreted(DataFlow::Node arg) {
4514+
none() // this is a safe API.
4515+
}
4516+
}
4517+
4518+
/**
4519+
* A call to `subprocess_shell` on an event loop instance.
4520+
*
4521+
* See https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.subprocess_shell
4522+
*/
4523+
private class EventLoopSubprocessShell extends DataFlow::MethodCallNode,
4524+
SystemCommandExecution::Range, FileSystemAccess::Range
4525+
{
4526+
EventLoopSubprocessShell() { this.calls(eventLoopInstance(), "subprocess_shell") }
4527+
4528+
override DataFlow::Node getCommand() {
4529+
result = this.getArg(1)
4530+
or
4531+
result = this.getArgByName("cmd")
4532+
}
4533+
4534+
override DataFlow::Node getAPathArgument() { result = this.getCommand() }
4535+
4536+
override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getCommand() }
4537+
}
43994538
}
44004539

44014540
// ---------------------------------------------------------------------------

python/ql/test/query-tests/Security/CWE-078-CommandInjection/CommandInjection.expected

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ edges
88
| command_injection.py:5:26:5:32 | GSSA Variable request | command_injection.py:54:15:54:21 | ControlFlowNode for request |
99
| command_injection.py:5:26:5:32 | GSSA Variable request | command_injection.py:71:12:71:18 | ControlFlowNode for request |
1010
| command_injection.py:5:26:5:32 | GSSA Variable request | command_injection.py:78:12:78:18 | ControlFlowNode for request |
11+
| command_injection.py:5:26:5:32 | GSSA Variable request | command_injection.py:87:13:87:19 | ControlFlowNode for request |
12+
| command_injection.py:5:26:5:32 | GSSA Variable request | command_injection.py:92:13:92:19 | ControlFlowNode for request |
13+
| command_injection.py:5:26:5:32 | GSSA Variable request | command_injection.py:102:13:102:19 | ControlFlowNode for request |
14+
| command_injection.py:5:26:5:32 | GSSA Variable request | command_injection.py:112:13:112:19 | ControlFlowNode for request |
15+
| command_injection.py:5:26:5:32 | GSSA Variable request | command_injection.py:117:13:117:19 | ControlFlowNode for request |
16+
| command_injection.py:5:26:5:32 | GSSA Variable request | command_injection.py:122:13:122:19 | ControlFlowNode for request |
1117
| command_injection.py:11:5:11:9 | SSA variable files | command_injection.py:13:15:13:27 | ControlFlowNode for BinaryExpr |
1218
| command_injection.py:11:13:11:19 | ControlFlowNode for request | command_injection.py:11:13:11:24 | ControlFlowNode for Attribute |
1319
| command_injection.py:11:13:11:24 | ControlFlowNode for Attribute | command_injection.py:11:13:11:41 | ControlFlowNode for Attribute() |
@@ -45,6 +51,30 @@ edges
4551
| command_injection.py:78:12:78:18 | ControlFlowNode for request | command_injection.py:78:12:78:23 | ControlFlowNode for Attribute |
4652
| command_injection.py:78:12:78:23 | ControlFlowNode for Attribute | command_injection.py:78:12:78:39 | ControlFlowNode for Attribute() |
4753
| command_injection.py:78:12:78:39 | ControlFlowNode for Attribute() | command_injection.py:78:5:78:8 | SSA variable path |
54+
| command_injection.py:87:5:87:9 | SSA variable files | command_injection.py:88:48:88:52 | ControlFlowNode for files |
55+
| command_injection.py:87:13:87:19 | ControlFlowNode for request | command_injection.py:87:13:87:24 | ControlFlowNode for Attribute |
56+
| command_injection.py:87:13:87:24 | ControlFlowNode for Attribute | command_injection.py:87:13:87:41 | ControlFlowNode for Attribute() |
57+
| command_injection.py:87:13:87:41 | ControlFlowNode for Attribute() | command_injection.py:87:5:87:9 | SSA variable files |
58+
| command_injection.py:92:5:92:9 | SSA variable files | command_injection.py:93:51:93:55 | ControlFlowNode for files |
59+
| command_injection.py:92:13:92:19 | ControlFlowNode for request | command_injection.py:92:13:92:24 | ControlFlowNode for Attribute |
60+
| command_injection.py:92:13:92:24 | ControlFlowNode for Attribute | command_injection.py:92:13:92:41 | ControlFlowNode for Attribute() |
61+
| command_injection.py:92:13:92:41 | ControlFlowNode for Attribute() | command_injection.py:92:5:92:9 | SSA variable files |
62+
| command_injection.py:102:5:102:9 | SSA variable files | command_injection.py:106:82:106:86 | ControlFlowNode for files |
63+
| command_injection.py:102:13:102:19 | ControlFlowNode for request | command_injection.py:102:13:102:24 | ControlFlowNode for Attribute |
64+
| command_injection.py:102:13:102:24 | ControlFlowNode for Attribute | command_injection.py:102:13:102:41 | ControlFlowNode for Attribute() |
65+
| command_injection.py:102:13:102:41 | ControlFlowNode for Attribute() | command_injection.py:102:5:102:9 | SSA variable files |
66+
| command_injection.py:112:5:112:9 | SSA variable files | command_injection.py:113:49:113:53 | ControlFlowNode for files |
67+
| command_injection.py:112:13:112:19 | ControlFlowNode for request | command_injection.py:112:13:112:24 | ControlFlowNode for Attribute |
68+
| command_injection.py:112:13:112:24 | ControlFlowNode for Attribute | command_injection.py:112:13:112:41 | ControlFlowNode for Attribute() |
69+
| command_injection.py:112:13:112:41 | ControlFlowNode for Attribute() | command_injection.py:112:5:112:9 | SSA variable files |
70+
| command_injection.py:117:5:117:9 | SSA variable files | command_injection.py:118:52:118:56 | ControlFlowNode for files |
71+
| command_injection.py:117:13:117:19 | ControlFlowNode for request | command_injection.py:117:13:117:24 | ControlFlowNode for Attribute |
72+
| command_injection.py:117:13:117:24 | ControlFlowNode for Attribute | command_injection.py:117:13:117:41 | ControlFlowNode for Attribute() |
73+
| command_injection.py:117:13:117:41 | ControlFlowNode for Attribute() | command_injection.py:117:5:117:9 | SSA variable files |
74+
| command_injection.py:122:5:122:9 | SSA variable files | command_injection.py:125:83:125:87 | ControlFlowNode for files |
75+
| command_injection.py:122:13:122:19 | ControlFlowNode for request | command_injection.py:122:13:122:24 | ControlFlowNode for Attribute |
76+
| command_injection.py:122:13:122:24 | ControlFlowNode for Attribute | command_injection.py:122:13:122:41 | ControlFlowNode for Attribute() |
77+
| command_injection.py:122:13:122:41 | ControlFlowNode for Attribute() | command_injection.py:122:5:122:9 | SSA variable files |
4878
nodes
4979
| command_injection.py:5:26:5:32 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember |
5080
| command_injection.py:5:26:5:32 | GSSA Variable request | semmle.label | GSSA Variable request |
@@ -93,6 +123,36 @@ nodes
93123
| command_injection.py:78:12:78:23 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
94124
| command_injection.py:78:12:78:39 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
95125
| command_injection.py:80:19:80:30 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
126+
| command_injection.py:87:5:87:9 | SSA variable files | semmle.label | SSA variable files |
127+
| command_injection.py:87:13:87:19 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
128+
| command_injection.py:87:13:87:24 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
129+
| command_injection.py:87:13:87:41 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
130+
| command_injection.py:88:48:88:52 | ControlFlowNode for files | semmle.label | ControlFlowNode for files |
131+
| command_injection.py:92:5:92:9 | SSA variable files | semmle.label | SSA variable files |
132+
| command_injection.py:92:13:92:19 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
133+
| command_injection.py:92:13:92:24 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
134+
| command_injection.py:92:13:92:41 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
135+
| command_injection.py:93:51:93:55 | ControlFlowNode for files | semmle.label | ControlFlowNode for files |
136+
| command_injection.py:102:5:102:9 | SSA variable files | semmle.label | SSA variable files |
137+
| command_injection.py:102:13:102:19 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
138+
| command_injection.py:102:13:102:24 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
139+
| command_injection.py:102:13:102:41 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
140+
| command_injection.py:106:82:106:86 | ControlFlowNode for files | semmle.label | ControlFlowNode for files |
141+
| command_injection.py:112:5:112:9 | SSA variable files | semmle.label | SSA variable files |
142+
| command_injection.py:112:13:112:19 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
143+
| command_injection.py:112:13:112:24 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
144+
| command_injection.py:112:13:112:41 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
145+
| command_injection.py:113:49:113:53 | ControlFlowNode for files | semmle.label | ControlFlowNode for files |
146+
| command_injection.py:117:5:117:9 | SSA variable files | semmle.label | SSA variable files |
147+
| command_injection.py:117:13:117:19 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
148+
| command_injection.py:117:13:117:24 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
149+
| command_injection.py:117:13:117:41 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
150+
| command_injection.py:118:52:118:56 | ControlFlowNode for files | semmle.label | ControlFlowNode for files |
151+
| command_injection.py:122:5:122:9 | SSA variable files | semmle.label | SSA variable files |
152+
| command_injection.py:122:13:122:19 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
153+
| command_injection.py:122:13:122:24 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
154+
| command_injection.py:122:13:122:41 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
155+
| command_injection.py:125:83:125:87 | ControlFlowNode for files | semmle.label | ControlFlowNode for files |
96156
subpaths
97157
#select
98158
| command_injection.py:13:15:13:27 | ControlFlowNode for BinaryExpr | command_injection.py:5:26:5:32 | ControlFlowNode for ImportMember | command_injection.py:13:15:13:27 | ControlFlowNode for BinaryExpr | This command line depends on a $@. | command_injection.py:5:26:5:32 | ControlFlowNode for ImportMember | user-provided value |
@@ -108,3 +168,9 @@ subpaths
108168
| command_injection.py:59:20:59:26 | ControlFlowNode for command | command_injection.py:5:26:5:32 | ControlFlowNode for ImportMember | command_injection.py:59:20:59:26 | ControlFlowNode for command | This command line depends on a $@. | command_injection.py:5:26:5:32 | ControlFlowNode for ImportMember | user-provided value |
109169
| command_injection.py:73:19:73:30 | ControlFlowNode for BinaryExpr | command_injection.py:5:26:5:32 | ControlFlowNode for ImportMember | command_injection.py:73:19:73:30 | ControlFlowNode for BinaryExpr | This command line depends on a $@. | command_injection.py:5:26:5:32 | ControlFlowNode for ImportMember | user-provided value |
110170
| command_injection.py:80:19:80:30 | ControlFlowNode for BinaryExpr | command_injection.py:5:26:5:32 | ControlFlowNode for ImportMember | command_injection.py:80:19:80:30 | ControlFlowNode for BinaryExpr | This command line depends on a $@. | command_injection.py:5:26:5:32 | ControlFlowNode for ImportMember | user-provided value |
171+
| command_injection.py:88:48:88:52 | ControlFlowNode for files | command_injection.py:5:26:5:32 | ControlFlowNode for ImportMember | command_injection.py:88:48:88:52 | ControlFlowNode for files | This command line depends on a $@. | command_injection.py:5:26:5:32 | ControlFlowNode for ImportMember | user-provided value |
172+
| command_injection.py:93:51:93:55 | ControlFlowNode for files | command_injection.py:5:26:5:32 | ControlFlowNode for ImportMember | command_injection.py:93:51:93:55 | ControlFlowNode for files | This command line depends on a $@. | command_injection.py:5:26:5:32 | ControlFlowNode for ImportMember | user-provided value |
173+
| command_injection.py:106:82:106:86 | ControlFlowNode for files | command_injection.py:5:26:5:32 | ControlFlowNode for ImportMember | command_injection.py:106:82:106:86 | ControlFlowNode for files | This command line depends on a $@. | command_injection.py:5:26:5:32 | ControlFlowNode for ImportMember | user-provided value |
174+
| command_injection.py:113:49:113:53 | ControlFlowNode for files | command_injection.py:5:26:5:32 | ControlFlowNode for ImportMember | command_injection.py:113:49:113:53 | ControlFlowNode for files | This command line depends on a $@. | command_injection.py:5:26:5:32 | ControlFlowNode for ImportMember | user-provided value |
175+
| command_injection.py:118:52:118:56 | ControlFlowNode for files | command_injection.py:5:26:5:32 | ControlFlowNode for ImportMember | command_injection.py:118:52:118:56 | ControlFlowNode for files | This command line depends on a $@. | command_injection.py:5:26:5:32 | ControlFlowNode for ImportMember | user-provided value |
176+
| command_injection.py:125:83:125:87 | ControlFlowNode for files | command_injection.py:5:26:5:32 | ControlFlowNode for ImportMember | command_injection.py:125:83:125:87 | ControlFlowNode for files | This command line depends on a $@. | command_injection.py:5:26:5:32 | ControlFlowNode for ImportMember | user-provided value |
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
missingAnnotationOnSink
21
failures
2+
missingAnnotationOnSink
33
testFailures

python/ql/test/query-tests/Security/CWE-078-CommandInjection/command_injection.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,50 @@ def restricted_characters():
7878
path = request.args.get('path', '')
7979
if re.match(r'^[a-zA-Z0-9_-]+$', path):
8080
os.system("ls " + path) # $SPURIOUS: result=BAD
81+
82+
import asyncio
83+
from asyncio import subprocess
84+
85+
@app.route("/asyncio-exec1")
86+
def asyncio_exec_command_injection1():
87+
files = request.args.get('files', '')
88+
asyncio.run(asyncio.create_subprocess_exec(files)) # $result=BAD
89+
90+
@app.route("/asyncio-exec2")
91+
def asyncio_exec_command_injection2():
92+
files = request.args.get('files', '')
93+
asyncio.run(subprocess.create_subprocess_exec(files)) # $result=BAD
94+
95+
@app.route("/asyncio-exec-args")
96+
def asyncio_exec_arg_injection():
97+
files = request.args.get('files', '')
98+
asyncio.run(asyncio.create_subprocess_exec("ls", files)) # $result=OK - only an argument injection, not a command injection
99+
100+
@app.route("/asyncio-eventloop-command1")
101+
def asyncio_eventloop_exec_command_injection1():
102+
files = request.args.get('files', '')
103+
args = ["-a", "-l"]
104+
loop = asyncio.new_event_loop()
105+
try:
106+
loop.run_until_complete(loop.subprocess_exec(asyncio.SubprocessProtocol, files, *args)) # $result=BAD
107+
finally:
108+
loop.close()
109+
110+
@app.route("/asyncio-shell1")
111+
def asyncio_shell_command_injection1():
112+
files = request.args.get('files', '')
113+
asyncio.run(asyncio.create_subprocess_shell(files)) # $result=BAD
114+
115+
@app.route("/asyncio-shell2")
116+
def asyncio_shell_command_injection1():
117+
files = request.args.get('files', '')
118+
asyncio.run(subprocess.create_subprocess_shell(files)) # $result=BAD
119+
120+
@app.route("/asyncio-eventloop-shell1")
121+
def asyncio_eventloop_shell_command_injection1():
122+
files = request.args.get('files', '')
123+
loop = asyncio.new_event_loop()
124+
try:
125+
loop.run_until_complete(loop.subprocess_shell(asyncio.SubprocessProtocol, files)) # $result=BAD
126+
finally:
127+
loop.close()

0 commit comments

Comments
 (0)