From a1e9af2ab4fc1aea3dff7b20bda7e14166b10891 Mon Sep 17 00:00:00 2001 From: Kiro Agent <244629292+kiro-agent@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:48:42 +0000 Subject: [PATCH] feat: add shlex.quote and pipes.quote as command injection sanitizers shlex.quote (Python 3) and pipes.quote (Python 2) properly escape shell metacharacters, preventing command injection when user input is incorporated into shell commands. These were already sanitizers for py/shell-command-constructed-from-input but not for py/command-line-injection. --- .../CommandInjectionCustomizations.qll | 13 +++++++++ .../shlex_quote_test.py | 29 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 python/ql/test/query-tests/Security/CWE-078-CommandInjection/shlex_quote_test.py diff --git a/python/ql/lib/semmle/python/security/dataflow/CommandInjectionCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/CommandInjectionCustomizations.qll index facb422e7285..f0eae8b8929a 100644 --- a/python/ql/lib/semmle/python/security/dataflow/CommandInjectionCustomizations.qll +++ b/python/ql/lib/semmle/python/security/dataflow/CommandInjectionCustomizations.qll @@ -10,6 +10,7 @@ private import semmle.python.Concepts private import semmle.python.dataflow.new.RemoteFlowSources private import semmle.python.dataflow.new.BarrierGuards private import semmle.python.frameworks.data.ModelsAsData +private import semmle.python.ApiGraphs /** * Provides default sources, sinks and sanitizers for detecting @@ -102,4 +103,16 @@ module CommandInjection { class SanitizerFromModel extends Sanitizer { SanitizerFromModel() { ModelOutput::barrierNode(this, "command-injection") } } + + /** + * A call to `shlex.quote` or `pipes.quote`, considered as a sanitizer. + * These functions properly escape shell metacharacters, preventing command injection. + */ + class ShellQuoteSanitizer extends Sanitizer { + ShellQuoteSanitizer() { + this = API::moduleImport("shlex").getMember("quote").getACall() + or + this = API::moduleImport("pipes").getMember("quote").getACall() + } + } } diff --git a/python/ql/test/query-tests/Security/CWE-078-CommandInjection/shlex_quote_test.py b/python/ql/test/query-tests/Security/CWE-078-CommandInjection/shlex_quote_test.py new file mode 100644 index 000000000000..08f41d146751 --- /dev/null +++ b/python/ql/test/query-tests/Security/CWE-078-CommandInjection/shlex_quote_test.py @@ -0,0 +1,29 @@ +import os +import shlex +from flask import Flask, request # $ Source + +app = Flask(__name__) + + +@app.route("/run") +def run_command_safe(): + """shlex.quote properly escapes shell metacharacters - safe from injection.""" + filename = request.args.get("filename", "") + safe_filename = shlex.quote(filename) + os.system("cat " + safe_filename) # Safe - shlex.quote sanitizes + + +@app.route("/run_unsafe") +def run_command_unsafe(): + """Direct concatenation without quoting is vulnerable.""" + filename = request.args.get("filename", "") + os.system("cat " + filename) # $ Alert + + +@app.route("/run_pipes") +def run_command_pipes_quote(): + """pipes.quote is the Python 2 equivalent of shlex.quote.""" + import pipes + filename = request.args.get("filename", "") + safe_filename = pipes.quote(filename) + os.system("cat " + safe_filename) # Safe - pipes.quote sanitizes