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