Merge remote-tracking branch 'upstream/main' into 'rc/3.14'

This commit is contained in:
Arthur Baars
2024-06-28 19:50:35 +02:00
772 changed files with 16846 additions and 17035 deletions

View File

@@ -12,6 +12,29 @@
import python
import Variables.Definition
import semmle.python.ApiGraphs
private predicate is_pytest_fixture(Import imp, Variable name) {
exists(Alias a, API::Node pytest_fixture, API::Node decorator |
pytest_fixture = API::moduleImport("pytest").getMember("fixture") and
// The additional `.getReturn()` is to account for the difference between
// ```
// @pytest.fixture
// def foo():
// ...
// ```
// and
// ```
// @pytest.fixture(some, args, here)
// def foo():
// ...
// ```
decorator in [pytest_fixture, pytest_fixture.getReturn()] and
a = imp.getAName() and
a.getAsname().(Name).getVariable() = name and
a.getValue() = decorator.getReturn().getAValueReachableFromSource().asExpr()
)
}
predicate global_name_used(Module m, string name) {
exists(Name u, GlobalVariable v |
@@ -117,6 +140,7 @@ predicate unused_import(Import imp, Variable name) {
not all_not_understood(imp.getEnclosingModule()) and
not imported_module_used_in_doctest(imp) and
not imported_alias_used_in_typehint(imp, name) and
not is_pytest_fixture(imp, name) and
// Only consider import statements that actually point-to something (possibly an unknown module).
// If this is not the case, it's likely that the import statement never gets executed.
imp.getAName().getValue().pointsTo(_)

View File

@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Additional sanitizers have been added to the `py/full-ssrf` and `py/partial-ssrf` queries for methods that verify a string contains only a certain set of characters, such as `.isalnum()` as well as regular expression tests.

View File

@@ -1,17 +0,0 @@
<!DOCTYPE qhelp SYSTEM "qhelp.dtd">
<qhelp>
<overview>
<p>
Processing an unvalidated user input can allow an attacker to inject arbitrary command in your local and remote servers when creating a ssh connection.
</p>
</overview>
<recommendation>
<p>
This vulnerability can be prevented by not allowing untrusted user input to be passed as ProxyCommand or exec_command.
</p>
</recommendation>
<example>
<p>In the example below, the ProxyCommand and exec_command are controlled by the user and hence leads to a vulnerability.</p>
<sample src="paramikoBad.py" />
</example>
</qhelp>

View File

@@ -1,59 +0,0 @@
/**
* @name RCE with user provided command with paramiko ssh client
* @description user provided command can lead to execute code on a external server that can be belong to other users or admins
* @kind path-problem
* @problem.severity error
* @security-severity 9.3
* @precision high
* @id py/paramiko-command-injection
* @tags security
* experimental
* external/cwe/cwe-074
*/
import python
import semmle.python.dataflow.new.DataFlow
import semmle.python.dataflow.new.TaintTracking
import semmle.python.dataflow.new.RemoteFlowSources
import semmle.python.ApiGraphs
private API::Node paramikoClient() {
result = API::moduleImport("paramiko").getMember("SSHClient").getReturn()
}
private module ParamikoConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
/**
* exec_command of `paramiko.SSHClient` class execute command on ssh target server
* the `paramiko.ProxyCommand` is equivalent of `ssh -o ProxyCommand="CMD"`
* and it run CMD on current system that running the ssh command
* the Sink related to proxy command is the `connect` method of `paramiko.SSHClient` class
*/
predicate isSink(DataFlow::Node sink) {
sink = paramikoClient().getMember("exec_command").getACall().getParameter(0, "command").asSink()
or
sink = paramikoClient().getMember("connect").getACall().getParameter(11, "sock").asSink()
}
/**
* this additional taint step help taint tracking to find the vulnerable `connect` method of `paramiko.SSHClient` class
*/
predicate isAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
exists(API::CallNode call |
call = API::moduleImport("paramiko").getMember("ProxyCommand").getACall() and
nodeFrom = call.getParameter(0, "command_line").asSink() and
nodeTo = call
)
}
}
/** Global taint-tracking for detecting "paramiko command injection" vulnerabilities. */
module ParamikoFlow = TaintTracking::Global<ParamikoConfig>;
import ParamikoFlow::PathGraph
from ParamikoFlow::PathNode source, ParamikoFlow::PathNode sink
where ParamikoFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "This code execution depends on a $@.", source.getNode(),
"a user-provided value"

View File

@@ -0,0 +1,22 @@
<!DOCTYPE qhelp SYSTEM "qhelp.dtd">
<qhelp>
<overview>
<p>
Allowing users to execute arbitrary commands using an SSH connection on a remote server can lead to security issues unless you implement proper authorization.
</p>
<p>
Assume that you connect to a remote system via SSH connection from your main or local server that accepts user-controlled data and has interaction with users that you don't trust, passing these data to SSH API as a part of a command that will be executed on a secondary remote server can lead to security issues. You should consider proper authorization rules very carefully.
</p>
</overview>
<recommendation>
<p>
This vulnerability can be prevented by implementing proper authorization rules for untrusted user input that can be passed to your secondary servers.
</p>
</recommendation>
<example>
<p>In the example below, the exec_command is controlled by the user and hence leads to a vulnerability.</p>
<sample src="paramikoBad.py" />
<p>In the example below, the exec_command is controlled by the an Authorized user and hence it is safe.</p>
<sample src="paramikoGood.py" />
</example>
</qhelp>

View File

@@ -0,0 +1,21 @@
/**
* @name Command execution on a secondary remote server
* @description user provided command can lead to execute code on a external server that can be belong to other users or admins
* @kind path-problem
* @problem.severity error
* @security-severity 9.3
* @precision high
* @id py/paramiko-command-injection
* @tags security
* experimental
* external/cwe/cwe-074
*/
import python
import experimental.semmle.python.security.RemoteCommandExecution
import RemoteCommandExecutionFlow::PathGraph
from RemoteCommandExecutionFlow::PathNode source, RemoteCommandExecutionFlow::PathNode sink
where RemoteCommandExecutionFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "This code execution depends on a $@.", source.getNode(),
"a user-provided value"

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env python
from flask import request, Flask
import paramiko
from paramiko import SSHClient
app = Flask(__name__)
paramiko_ssh_client = SSHClient()
paramiko_ssh_client.load_system_host_keys()
paramiko_ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
paramiko_ssh_client.connect(hostname="127.0.0.1", port="22", username="ssh_user_name", pkey="k", timeout=11, banner_timeout=200)
@app.route('/external_exec_command_1')
def withoutAuthorization():
user_cmd = request.args.get('command')
stdin, stdout, stderr = paramiko_ssh_client.exec_command(user_cmd)
return stdout
if __name__ == '__main__':
app.debug = False
app.run()

View File

@@ -12,23 +12,14 @@ paramiko_ssh_client.connect(hostname="127.0.0.1", port="22", username="ssh_user_
@app.route('/external_exec_command_1')
def bad1():
def withAuthorization():
user_cmd = request.args.get('command')
auth_jwt = request.args.get('Auth')
# validating jwt token first
# .... then continue to run the command
stdin, stdout, stderr = paramiko_ssh_client.exec_command(user_cmd)
return stdout
@app.route('/external_exec_command_2')
def bad2():
user_cmd = request.args.get('command')
stdin, stdout, stderr = paramiko_ssh_client.exec_command(command=user_cmd)
return stdout
@app.route('/proxycommand')
def bad2():
user_cmd = request.args.get('command')
stdin, stdout, stderr = paramiko_ssh_client.connect('hostname', username='user',password='yourpassword',sock=paramiko.ProxyCommand(user_cmd))
return stdout
if __name__ == '__main__':
app.debug = False

View File

@@ -15,6 +15,39 @@ private import semmle.python.dataflow.new.TaintTracking
private import experimental.semmle.python.Frameworks
private import semmle.python.Concepts
/**
* A data-flow node that executes an operating system command,
* on a remote server likely by SSH connections.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `RemoteCommandExecution::Range` instead.
*/
class RemoteCommandExecution extends DataFlow::Node instanceof RemoteCommandExecution::Range {
/** Holds if a shell interprets `arg`. */
predicate isShellInterpreted(DataFlow::Node arg) { super.isShellInterpreted(arg) }
/** Gets the argument that specifies the command to be executed. */
DataFlow::Node getCommand() { result = super.getCommand() }
}
/** Provides classes for modeling new remote server command execution APIs. */
module RemoteCommandExecution {
/**
* A data-flow node that executes an operating system command,
* on a remote server likely by SSH connections.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `RemoteCommandExecution` instead.
*/
abstract class Range extends DataFlow::Node {
/** Gets the argument that specifies the command to be executed. */
abstract DataFlow::Node getCommand();
/** Holds if a shell interprets `arg`. */
predicate isShellInterpreted(DataFlow::Node arg) { none() }
}
}
/** Provides classes for modeling copying file related APIs. */
module CopyFile {
/**

View File

@@ -2,10 +2,16 @@
* Helper file that imports all framework modeling.
*/
private import experimental.semmle.python.frameworks.AsyncSsh
private import experimental.semmle.python.frameworks.Stdlib
private import experimental.semmle.python.frameworks.Flask
private import experimental.semmle.python.frameworks.Django
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.Twisted
private import experimental.semmle.python.frameworks.JWT
private import experimental.semmle.python.frameworks.Csv
private import experimental.semmle.python.libraries.PyJWT
@@ -14,5 +20,6 @@ private import experimental.semmle.python.libraries.Authlib
private import experimental.semmle.python.libraries.PythonJose
private import experimental.semmle.python.frameworks.CopyFile
private import experimental.semmle.python.frameworks.Sendgrid
private import experimental.semmle.python.frameworks.Ssh2
private import experimental.semmle.python.libraries.FlaskMail
private import experimental.semmle.python.libraries.SmtpLib

View File

@@ -0,0 +1,30 @@
/**
* Provides classes modeling security-relevant aspects of the `asyncssh` PyPI package.
* See https://pypi.org/project/asyncssh/.
*/
private import python
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.Concepts
private import semmle.python.ApiGraphs
import experimental.semmle.python.Concepts
/**
* Provides models for the `asyncssh` PyPI package.
* See https://pypi.org/project/asyncssh/.
*/
private module Asyncssh {
/**
* Gets `asyncssh` package.
*/
private API::Node asyncssh() { result = API::moduleImport("asyncssh") }
/**
* A `run` method responsible for executing commands on remote secondary servers.
*/
class AsyncsshRun extends RemoteCommandExecution::Range, API::CallNode {
AsyncsshRun() { this = asyncssh().getMember("connect").getReturn().getMember("run").getACall() }
override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() }
}
}

View File

@@ -0,0 +1,55 @@
/**
* Provides classes modeling security-relevant aspects of the `netmiko` PyPI package.
* See https://pypi.org/project/netmiko/.
*/
private import python
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.Concepts
private import semmle.python.ApiGraphs
import experimental.semmle.python.Concepts
/**
* Provides models for the `netmiko` PyPI package.
* See https://pypi.org/project/netmiko/.
*/
private module Netmiko {
/**
* Gets `netmiko` package.
*/
private API::Node netmiko() { result = API::moduleImport("netmiko") }
/**
* Gets `netmiko.ConnectHandler` return value.
*/
private API::Node netmikoConnectHandler() {
result = netmiko().getMember("ConnectHandler").getReturn()
}
/**
* The `send_*` methods responsible for executing commands on remote secondary servers.
*/
class NetmikoSendCommand extends RemoteCommandExecution::Range, API::CallNode {
boolean isMultiline;
NetmikoSendCommand() {
this =
netmikoConnectHandler()
.getMember(["send_command", "send_command_expect", "send_command_timing"])
.getACall() and
isMultiline = false
or
this =
netmikoConnectHandler().getMember(["send_multiline", "send_multiline_timing"]).getACall() and
isMultiline = true
}
override DataFlow::Node getCommand() {
result = this.getParameter(0, "command_string").asSink() and
isMultiline = false
or
result = this.getParameter(0, "commands").asSink() and
isMultiline = true
}
}
}

View File

@@ -0,0 +1,35 @@
/**
* Provides classes modeling security-relevant aspects of the `paramiko` PyPI package.
* See https://pypi.org/project/paramiko/.
*/
private import python
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.Concepts
private import semmle.python.ApiGraphs
import experimental.semmle.python.Concepts
/**
* Provides models for the `paramiko` PyPI package.
* See https://pypi.org/project/paramiko/.
*/
private module Paramiko {
/**
* Gets `paramiko` package.
*/
private API::Node paramiko() { result = API::moduleImport("paramiko") }
/**
* Gets `paramiko.SSHClient` return value.
*/
private API::Node paramikoClient() { result = paramiko().getMember("SSHClient").getReturn() }
/**
* The `exec_command` of `paramiko.SSHClient` class execute command on ssh target server
*/
class ParamikoExecCommand extends RemoteCommandExecution::Range, API::CallNode {
ParamikoExecCommand() { this = paramikoClient().getMember("exec_command").getACall() }
override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() }
}
}

View File

@@ -0,0 +1,33 @@
/**
* 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.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 RemoteCommandExecution::Range, API::CallNode {
PexpectCommandExec() {
this =
API::moduleImport("pexpect")
.getMember("pxssh")
.getMember("pxssh")
.getReturn()
.getMember(["send", "sendline"])
.getACall()
}
override DataFlow::Node getCommand() { result = this.getParameter(0, "s").asSink() }
}
}

View File

@@ -0,0 +1,56 @@
/**
* Provides classes modeling security-relevant aspects of the `scrapli` PyPI package.
* See https://pypi.org/project/scrapli/.
*/
private import python
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.Concepts
private import semmle.python.ApiGraphs
import experimental.semmle.python.Concepts
/**
* Provides models for the `scrapli` PyPI package.
* See https://pypi.org/project/scrapli/.
*/
private module Scrapli {
/**
* Gets `scrapli` package.
*/
private API::Node scrapli() { result = API::moduleImport("scrapli") }
/**
* Gets `scrapli.driver` package.
*/
private API::Node scrapliDriver() { result = scrapli().getMember("driver") }
/**
* Gets `scrapli.driver.core` package.
*/
private API::Node scrapliCore() { result = scrapliDriver().getMember("core") }
/**
* A `send_command` method responsible for executing commands on remote secondary servers.
*/
class ScrapliSendCommand extends RemoteCommandExecution::Range, API::CallNode {
ScrapliSendCommand() {
this =
scrapliCore()
.getMember([
"AsyncNXOSDriver", "AsyncJunosDriver", "AsyncEOSDriver", "AsyncIOSXEDriver",
"AsyncIOSXRDriver", "NXOSDriver", "JunosDriver", "EOSDriver", "IOSXEDriver",
"IOSXRDriver"
])
.getReturn()
.getMember("send_command")
.getACall()
or
this = scrapli().getMember("Scrapli").getReturn().getMember("send_command").getACall()
or
this =
scrapliDriver().getMember("GenericDriver").getReturn().getMember("send_command").getACall()
}
override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() }
}
}

View File

@@ -0,0 +1,39 @@
/**
* Provides classes modeling security-relevant aspects of the `ssh2-python` PyPI package.
* See https://pypi.org/project/ssh2-python/.
*/
private import python
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.Concepts
private import semmle.python.ApiGraphs
import experimental.semmle.python.Concepts
/**
* Provides models for the `ssh2-python` PyPI package.
* See https://pypi.org/project/ssh2-python/.
*/
private module Ssh2 {
/**
* Gets `ssh2` package.
*/
private API::Node ssh2() { result = API::moduleImport("ssh2") }
/**
* Gets `ssh2.session.Session` return value.
*/
private API::Node ssh2Session() {
result = ssh2().getMember("session").getMember("Session").getReturn()
}
/**
* An `execute` method responsible for executing commands on remote secondary servers.
*/
class Ssh2Execute extends RemoteCommandExecution::Range, API::CallNode {
Ssh2Execute() {
this = ssh2Session().getMember("open_session").getReturn().getMember("execute").getACall()
}
override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() }
}
}

View File

@@ -0,0 +1,35 @@
/**
* Provides classes modeling security-relevant aspects of the `twisted` PyPI package.
* See https://twistedmatrix.com/.
*/
private import python
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.dataflow.new.TaintTracking
private import semmle.python.Concepts
private import semmle.python.ApiGraphs
private import semmle.python.frameworks.internal.InstanceTaintStepsHelper
import experimental.semmle.python.Concepts
/**
* Provides models for the `twisted` PyPI package.
* See https://docs.twistedmatrix.com/en/stable/api/twisted.conch.endpoints.SSHCommandClientEndpoint.html
*/
private module Twisted {
/**
* The `newConnection` and `existingConnection` functions of `twisted.conch.endpoints.SSHCommandClientEndpoint` class execute command on ssh target server
*/
class ParamikoExecCommand extends RemoteCommandExecution::Range, API::CallNode {
ParamikoExecCommand() {
this =
API::moduleImport("twisted")
.getMember("conch")
.getMember("endpoints")
.getMember("SSHCommandClientEndpoint")
.getMember(["newConnection", "existingConnection"])
.getACall()
}
override DataFlow::Node getCommand() { result = this.getParameter(1, "command").asSink() }
}
}

View File

@@ -0,0 +1,16 @@
import python
import semmle.python.dataflow.new.TaintTracking
import semmle.python.dataflow.new.RemoteFlowSources
import semmle.python.ApiGraphs
import semmle.python.dataflow.new.internal.DataFlowPublic
import codeql.util.Unit
import experimental.semmle.python.Concepts
module RemoteCommandExecutionConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
predicate isSink(DataFlow::Node sink) { sink = any(RemoteCommandExecution rce).getCommand() }
}
/** Global taint-tracking for detecting "secondary server command injection" vulnerabilities. */
module RemoteCommandExecutionFlow = TaintTracking::Global<RemoteCommandExecutionConfig>;