Python - Python Security: Preventing SQL Injection and Command Injection

 

1. Introduction

Python is widely used to develop web applications, database-driven systems, automation scripts, and software tools. These applications often accept input from users and use that input to interact with databases or execute operating system commands. If the input is handled incorrectly, attackers may manipulate the application to perform actions that the developer never intended.

Two important security vulnerabilities are SQL injection and command injection. SQL injection occurs when untrusted input changes the structure of a database query. Command injection occurs when untrusted input changes the structure of a command executed by the operating system. Understanding these vulnerabilities helps Python developers build safer and more reliable applications.

2. What Is SQL Injection?

SQL injection is a security vulnerability in which an attacker supplies specially crafted input that changes the meaning of an SQL statement. It commonly occurs when a program combines user input and SQL code into a single string.

For example, consider a Python application that checks whether a user has entered the correct username.

An unsafe implementation might look like this:

import sqlite3connection = sqlite3.connect("users.db")cursor = connection.cursor()username = input("Enter your username: ")query = (    "SELECT * FROM users WHERE username = '"    + username    + "'")cursor.execute(query)print(cursor.fetchall())connection.close()
 

In this example, the program directly inserts the user's input into the SQL query. If the input contains special SQL syntax, it may change the query's intended meaning.

The problem is not the use of SQLite or Python itself. The problem is that the program treats user-controlled input as part of the SQL code rather than as data.

How SQL injection works

Suppose a database contains a table named users with columns such as id, username, and email.

The intended query is:

SELECT * FROM users WHERE username = 'alex';

The application expects to retrieve the record belonging to the username alex. However, if user input is inserted directly into the query, specially crafted input may alter its conditions or structure.

Depending on the application and database permissions, successful SQL injection could allow an attacker to bypass login checks, access unauthorized records, modify data, or delete information.

The impact depends on the query, the database configuration, the permissions of the database account, and other security controls.

3. How to Prevent SQL Injection in Python

The most important protection is to use parameterized queries, also known as prepared statements. These allow the database driver to distinguish SQL instructions from user-provided values.

Secure implementation using SQLite

import sqlite3connection = sqlite3.connect("users.db")cursor = connection.cursor()username = input("Enter your username: ")query = "SELECT * FROM users WHERE username = ?"cursor.execute(query, (username,))results = cursor.fetchall()for user in results:    print(user)connection.close()
 

In this example, the question mark is a parameter placeholder. The username is passed separately as a value to cursor.execute().

The database treats the supplied username as data rather than interpreting it as SQL syntax. Even if the input contains quotation marks or SQL operators, it cannot change the structure of this query through that parameter.

The placeholder syntax differs between database drivers. SQLite uses ?, while other drivers may use %s or named parameters. Developers should follow the documentation for their specific database library.

Additional SQL injection prevention practices

  • Use parameterized queries: Never construct SQL statements by directly concatenating untrusted values.

  • Validate input: Check that values meet the expected format, length, and type. Validation complements parameterization but does not replace it.

  • Apply least-privilege permissions: Give the application's database account only the permissions it needs.

  • Avoid exposing database errors: Do not show detailed database errors or internal query information to ordinary users.

  • Use safe database APIs: When using an ORM, such as SQLAlchemy or Django's ORM, prefer its parameterized query and filtering interfaces. Avoid unsafe raw SQL construction.

These practices reduce the likelihood and potential impact of SQL injection attacks.

4. What Is Command Injection?

Command injection is a vulnerability that occurs when an application passes untrusted input into an operating system command in a way that allows the input to alter the command being executed.

Python provides modules such as subprocess for running external programs. These modules are useful for tasks such as processing files, running system utilities, and automating administrative work. However, they must be used carefully when user input is involved.

Consider the following unsafe example:

import subprocessfilename = input("Enter a filename: ")command = "cat " + filenamesubprocess.run(command, shell=True)
 

This code combines the filename with a command string and asks the system shell to interpret the entire string.

If the input contains shell metacharacters, the shell may interpret them as instructions instead of treating the input as a filename. Depending on the operating system and shell, this can result in unintended commands being executed.

The use of shell=True with untrusted input is particularly risky because it enables shell interpretation of the constructed command.

Why command injection is dangerous

A successful command injection attack may allow an attacker to:

  • Read files that the application is allowed to access.

  • Modify or delete files.

  • Execute additional programs.

  • Access environment variables or sensitive configuration data.

  • Perform other actions permitted by the application's operating system account.

The severity depends on the privileges of the Python process and the environment in which the application runs.

5. How to Prevent Command Injection in Python

A recommended approach is to use subprocess.run() with a list of arguments and avoid invoking a shell when it is unnecessary.

Secure implementation

import subprocessfilename = input("Enter a filename: ")result = subprocess.run(    ["cat", "--", filename],    shell=False,    capture_output=True,    text=True,    check=False)if result.returncode == 0:    print(result.stdout)else:    print("Unable to read the file.")
 

This example is intended for Unix-like systems that provide the cat command and support the -- option. The -- indicates that subsequent arguments should be treated as operands rather than options by commands that support this convention.

The important security features are:

  • The command and its arguments are supplied as separate list elements.

  • shell=False prevents the shell from interpreting the argument string.

  • The filename is passed as an argument rather than concatenated into a shell command.

This significantly reduces the risk of shell-based command injection. However, it does not automatically make every filename safe. The program should still validate the filename and restrict which files may be accessed.

For example, an application that permits access only to a particular directory should resolve the requested path and verify that it remains within the permitted directory. Simply checking whether a filename contains certain characters is not a sufficient security measure.

Additional command injection prevention practices

  • Avoid shell=True unless shell functionality is genuinely required.

  • Pass commands as argument lists rather than constructed strings.

  • Use an allowlist when users may choose from a fixed set of operations.

  • Validate paths, filenames, and other input according to the application's requirements.

  • Run the application with limited operating system permissions.

  • Avoid exposing sensitive command output or environment details to users.

  • Prefer Python's built-in file and operating system APIs over external commands when they can perform the task safely.

6. Difference Between SQL Injection and Command Injection

Feature

SQL Injection

Command Injection

Target

Database queries

Operating system commands

Main cause

Untrusted input alters SQL syntax

Untrusted input alters command interpretation

Common risky practice

Building SQL through string concatenation

Building shell commands through string concatenation

Main prevention

Parameterized database queries

Argument lists with shell=False

Possible impact

Unauthorized database access or modification

Unauthorized command execution or file access

Although both vulnerabilities involve unsafe handling of input, they affect different components of an application. Therefore, each requires appropriate protections.

7. Input Validation and the Principle of Least Privilege

Input validation is an important layer of application security. A Python application should verify that the data received from a user matches what the application expects.

For example, if a program expects an age, it should reject non-numeric values and values outside the permitted range. If it expects a username, it may enforce a length limit and an appropriate character policy.

However, input validation alone cannot reliably prevent SQL injection or command injection. An application might accept a valid-looking value that still becomes dangerous when inserted into an SQL query or interpreted by a shell. The correct method for handling the data is therefore essential.

The principle of least privilege provides another layer of protection. A database account should not have administrative permissions if it only needs to read a few tables. Likewise, a Python process should not run with administrator or root privileges unless they are necessary.

If a vulnerability occurs despite preventive measures, restricted permissions can reduce the damage an attacker may cause.

8. Secure Error Handling and Logging

Error handling is another important aspect of Python security. Applications sometimes expose internal details when database queries fail or external commands return errors. Such information may reveal database structures, filesystem paths, configuration values, or implementation details.

A safer approach is to show users a general error message while recording relevant diagnostic information in protected application logs.

For example:

import loggingimport sqlite3logging.basicConfig(level=logging.ERROR)try:    connection = sqlite3.connect("users.db")    cursor = connection.cursor()    username = input("Enter your username: ")    cursor.execute(        "SELECT * FROM users WHERE username = ?",        (username,)    )    print(cursor.fetchall())except sqlite3.Error:    logging.exception("A database operation failed.")    print("The request could not be completed.")finally:    if "connection" in locals():        connection.close()
 

This example demonstrates parameterized SQL and basic error handling. In a production application, developers should also consider connection management, access controls for logs, and whether error details could expose sensitive information.

Logs should never contain passwords, authentication tokens, or other secrets unnecessarily.

9. Testing Python Applications for Injection Vulnerabilities

Security testing helps developers identify unsafe input handling before an application is deployed. Tests should cover both normal inputs and unusual or malicious-looking inputs.

For SQL injection prevention, developers can verify that inputs containing quotation marks and SQL operators are treated as ordinary values and do not bypass authorization checks or change query behavior.

For command injection prevention, tests can verify that filenames containing shell metacharacters are passed as literal arguments and do not cause additional commands to execute. Tests should also confirm that file access remains restricted to authorized locations.

Automated testing tools, code reviews, static analysis, and dependency scanning can help detect security weaknesses. However, passing a limited set of tests does not prove that an application is completely secure. Testing should be combined with secure coding practices and appropriate deployment controls.

10. Best Practices for Secure Python Development

Developers should follow these principles when building secure Python applications:

  1. Treat all external input as untrusted, including input from users, files, APIs, and environment variables.

  2. Use parameterized queries for database operations.

  3. Avoid constructing shell commands from user-controlled strings.

  4. Prefer argument lists and shell=False when invoking external programs.

  5. Validate input according to clear business rules.

  6. Apply least-privilege permissions to database accounts and operating system processes.

  7. Handle errors without exposing sensitive internal information.

  8. Test security-sensitive code with unexpected and malicious-looking inputs.

  9. Keep Python packages and system dependencies updated.

  10. Review security assumptions whenever application functionality changes.

Conclusion

Preventing SQL injection and command injection is an essential part of secure Python development. SQL injection arises when untrusted input changes the structure of a database query, whereas command injection occurs when input changes the interpretation of an operating system command.

Python developers can substantially reduce these risks by using parameterized SQL queries, avoiding unnecessary shell execution, passing external commands as argument lists, validating input, restricting permissions, and testing security-sensitive functionality. These techniques help protect application data, operating system resources, and users from unauthorized actions.