Skip to content

Commit ec0529d

Browse files
authored
Merge pull request #14145 from p-/p--asyncio-cmdi-exec
Python: Support for command injection sinks found in the `asyncio` module
2 parents d4c3dff + bfb4be2 commit ec0529d

File tree

3 files changed

+133
-0
lines changed

3 files changed

+133
-0
lines changed

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

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4396,6 +4396,117 @@ private module StdlibPrivate {
43964396
preservesValue = true
43974397
}
43984398
}
4399+
4400+
// ---------------------------------------------------------------------------
4401+
// asyncio
4402+
// ---------------------------------------------------------------------------
4403+
/** Provides models for the `asyncio` module. */
4404+
module AsyncIO {
4405+
/**
4406+
* A call to the `asyncio.create_subprocess_exec` function (also accessible via the `subprocess` module of `asyncio`)
4407+
*
4408+
* See https://docs.python.org/3/library/asyncio-subprocess.html#creating-subprocesses
4409+
*/
4410+
private class CreateSubprocessExec extends SystemCommandExecution::Range,
4411+
FileSystemAccess::Range, API::CallNode
4412+
{
4413+
CreateSubprocessExec() {
4414+
this = API::moduleImport("asyncio").getMember("create_subprocess_exec").getACall()
4415+
or
4416+
this =
4417+
API::moduleImport("asyncio")
4418+
.getMember("subprocess")
4419+
.getMember("create_subprocess_exec")
4420+
.getACall()
4421+
}
4422+
4423+
override DataFlow::Node getCommand() { result = this.getParameter(0, "program").asSink() }
4424+
4425+
override DataFlow::Node getAPathArgument() { result = this.getCommand() }
4426+
4427+
override predicate isShellInterpreted(DataFlow::Node arg) {
4428+
none() // this is a safe API.
4429+
}
4430+
}
4431+
4432+
/**
4433+
* A call to the `asyncio.create_subprocess_shell` function (also accessible via the `subprocess` module of `asyncio`)
4434+
*
4435+
* See https://docs.python.org/3/library/asyncio-subprocess.html#asyncio.create_subprocess_shell
4436+
*/
4437+
private class CreateSubprocessShell extends SystemCommandExecution::Range,
4438+
FileSystemAccess::Range, API::CallNode
4439+
{
4440+
CreateSubprocessShell() {
4441+
this = API::moduleImport("asyncio").getMember("create_subprocess_shell").getACall()
4442+
or
4443+
this =
4444+
API::moduleImport("asyncio")
4445+
.getMember("subprocess")
4446+
.getMember("create_subprocess_shell")
4447+
.getACall()
4448+
}
4449+
4450+
override DataFlow::Node getCommand() { result = this.getParameter(0, "cmd").asSink() }
4451+
4452+
override DataFlow::Node getAPathArgument() { result = this.getCommand() }
4453+
4454+
override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getCommand() }
4455+
}
4456+
4457+
/**
4458+
* Get an asyncio event loop (an object with basetype `AbstractEventLoop`).
4459+
*
4460+
* See https://docs.python.org/3/library/asyncio-eventloop.html
4461+
*/
4462+
private API::Node getAsyncioEventLoop() {
4463+
result = API::moduleImport("asyncio").getMember("get_running_loop").getReturn()
4464+
or
4465+
result = API::moduleImport("asyncio").getMember("get_event_loop").getReturn() // deprecated in Python 3.10.0 and later
4466+
or
4467+
result = API::moduleImport("asyncio").getMember("new_event_loop").getReturn()
4468+
}
4469+
4470+
/**
4471+
* A call to `subprocess_exec` on an event loop instance.
4472+
*
4473+
* See https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.subprocess_exec
4474+
*/
4475+
private class EventLoopSubprocessExec extends API::CallNode, SystemCommandExecution::Range,
4476+
FileSystemAccess::Range
4477+
{
4478+
EventLoopSubprocessExec() {
4479+
this = getAsyncioEventLoop().getMember("subprocess_exec").getACall()
4480+
}
4481+
4482+
override DataFlow::Node getCommand() { result = this.getArg(1) }
4483+
4484+
override DataFlow::Node getAPathArgument() { result = this.getCommand() }
4485+
4486+
override predicate isShellInterpreted(DataFlow::Node arg) {
4487+
none() // this is a safe API.
4488+
}
4489+
}
4490+
4491+
/**
4492+
* A call to `subprocess_shell` on an event loop instance.
4493+
*
4494+
* See https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.subprocess_shell
4495+
*/
4496+
private class EventLoopSubprocessShell extends API::CallNode, SystemCommandExecution::Range,
4497+
FileSystemAccess::Range
4498+
{
4499+
EventLoopSubprocessShell() {
4500+
this = getAsyncioEventLoop().getMember("subprocess_shell").getACall()
4501+
}
4502+
4503+
override DataFlow::Node getCommand() { result = this.getParameter(1, "cmd").asSink() }
4504+
4505+
override DataFlow::Node getAPathArgument() { result = this.getCommand() }
4506+
4507+
override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getCommand() }
4508+
}
4509+
}
43994510
}
44004511

44014512
// ---------------------------------------------------------------------------
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Extended the `py/command-line-injection` query with sinks from Python's `asyncio` module.

python/ql/test/library-tests/frameworks/stdlib/SystemCommandExecution.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,21 @@ def os_members():
158158
wrong_use = shlex.quote("ls {}".format(tainted))
159159
# still dangerous, for example
160160
cmd = "sh -c " + wrong_use
161+
162+
########################################
163+
# Program/shell command execution via asyncio
164+
165+
import asyncio
166+
from asyncio import subprocess
167+
168+
asyncio.run(asyncio.create_subprocess_exec("executable", "arg0")) # $getCommand="executable" getAPathArgument="executable"
169+
asyncio.run(subprocess.create_subprocess_exec("executable", "arg0")) # $getCommand="executable" getAPathArgument="executable"
170+
171+
loop = asyncio.new_event_loop()
172+
loop.run_until_complete(loop.subprocess_exec(asyncio.SubprocessProtocol, "executable", "arg0")) # $getCommand="executable" getAPathArgument="executable"
173+
174+
asyncio.run(asyncio.create_subprocess_shell("shell_command")) # $getCommand="shell_command" getAPathArgument="shell_command"
175+
asyncio.run(subprocess.create_subprocess_shell("shell_command")) # $getCommand="shell_command" getAPathArgument="shell_command"
176+
177+
loop = asyncio.get_running_loop()
178+
loop.run_until_complete(loop.subprocess_shell(asyncio.SubprocessProtocol, "shell_command")) # $getCommand="shell_command" getAPathArgument="shell_command"

0 commit comments

Comments
 (0)