Python: Prevent SQL Injection with Best Practices

Prevent SQL injection

SQL Injection in Python: Overview

Databases are an important part of any application which has the ability to store data. Python applications interact with the database using SQLite, MySQLdb and ORMs (Object-Relational Mappers ) such as SQLAlchemy and Django ORM. 

Understanding how SQL injections are carried out in a Python application helps design and implement a secure application. In this blog we will learn best practices in Python to prevent SQL injection.  

By definition SQL injection means a kind of cyberattack where malicious code is injected into an SQL query to manipulate its intended behavior.  This causes unauthorized access, data theft, or even system compromise.

Why is SQL Injection Dangerous ?

SQL injection causes a significant threat to web applications due to potential damage it can create in terms of money, data, and trust of the organization.

  • Data theft: Attackers  steal sensitive information like customer data, financial records, or intellectual property.
  • Unauthorized access: SQL injection may provide  unauthorized access to restricted areas of a database or system.
  • System compromise: SQL injection may be used to gain complete control of a system in some cases.

Common Mistakes that Lead to SQL Injection in Python

Direct String Concatenation in SQL Queries

One of the most common mistakes which new developers make is  they directly concatenate user input into SQL queries which allows attackers to inject malicious code. For example in the below we are selecting all the records of a particular user based on name.

Python code

import sqlite3
conn = sqlite3.connect('example.db')
cursor = conn.cursor()


cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    username TEXT NOT NULL,
    password TEXT NOT NULL
)''')

sample_data = [
    ('admin', 'password123'),
    ('user1', 'mypassword'),
    ('guest', 'guestpassword')]

# User input (unsafe)
user_input = "admin' OR '1'='1" 
query = "SELECT * FROM users WHERE username = '" + user_input + "'"


# Execute the query (vulnerable to SQL injection)
cursor.execute(query)
results = cursor.fetchall()


print(results)


conn.close()

output

username      password
--------------------------
admin         password123
user1         mypassword
guest        guestpassword

When we execute this query it will always return a valid result, allowing unauthorized access.

Insecure User Input Handling​

Failing to validate or sanitize user inputs increases the risk of SQL injection. For example, allowing users to input any string without restrictions can lead to malicious queries being executed. 

To solve this kind of problem input constraints must be applied like checking data types  like Integer, string, date etc. If email is asked as input we can use regular expressions to validate it.

Using Unsanitized Inputs in SQL Queries

Using unsanitized inputs directly into SQL queries can lead to vulnerabilities. For instance:

Python code

user_input = "1; DROP TABLE products; "

#Unsanitized query
query = "SELECT * FROM products WHERE id = " + user_input
try:
    cursor.execute(query)
    results = cursor.fetchall()
    print(results)
except Exception as e:
    print("An error occurred:", e)

We can see that the “user_input” variable contains malicious code. It will get executed and delete the products table in the database and no one will know.

Best Practices in Python to Prevent SQL Injection

Use of Parameterized Queries

The main purpose of parameterized queries is to separate SQL code from data. This technique prevents attackers from inserting malicious code into the SQL query. In a parameterized query we need to get the parameter value from the user and execute the query. 

In the Python code below, if we look into the “get_user_by_username” function, it takes a single parameter called “user_name.” The function has a parameterized SQL query called “query” to safely retrieve user data by username by using a placeholder (?) for user input. By passing user_input as a separate argument, it prevents SQL injection by ensuring the input is treated as data rather than executable code.

Python Code

import sqlite3

#Function to create a sample database and table (for demonstration)
def create_database():
    conn = sqlite3.connect('example.db')
    cursor = conn.cursor()
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS users (
            id INTEGER PRIMARY KEY,
            username TEXT NOT NULL,
            password TEXT NOT NULL
        )
    ''')
    # Insert sample data
    cursor.execute("INSERT INTO users (username, password) VALUES (?, ?)", ("admin", "admin_pass"))
    cursor.execute("INSERT INTO users (username, password) VALUES (?, ?)", ("user", "user_pass"))
    conn.commit()
    conn.close()

# Function to query users safely using parameterized queries
def get_user_by_username(user_input):
    conn = sqlite3.connect('example.db')
    cursor = conn.cursor()
   
    # Parameterized query to prevent SQL injection
    query = "SELECT * FROM users WHERE username = ?"
    cursor.execute(query, (user_input,))
   
    # Fetch and return results
    results = cursor.fetchall()
    conn.close()
    return results
   
create_database()
user_input = input("Enter username to search: ")

# Retrieve user by username
users = get_user_by_username(user_input)

if users:
    for user in users:
        print(f"ID: {user[0]}, Username: {user[1]}, Password: {user[2]}")
else:
    print("No user found.")

Using Prepared Statements to Prevent SQL Injection

The Prepared statements are pre-compiled SQL queries that allow for secure data insertion. The user input is treated only as data. The SQLquery is compiled first and only after that user data is inserted. This way we are able to prevent SQL injection in Python. NOTE: SQLite3 library does not have a  prepare statement. Instead of that we have to use parameterized queries directly with placeholders in the SQL string.  For example
cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
MySQL supports prepared statements in Python with libraries like MySQL Connector. However we typically don’t need to call a PREPARE function. We can directly use the parameterized queries which handle preparation under the hood. For example
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))
PostgreSQL The psycopg2 library doesn’t directly provide a prepare() method like some other libraries, but it allows us to use server-side prepared statements for performance benefits.
# Create a prepared statement 
cursor.execute("PREPARE get_user AS SELECT * FROM users WHERE username = $1;") 

# Execute the prepared statement with the user input 
cursor.execute("EXECUTE get_user (%s);", (username,))

Use ORM (Object-Relational Mapping)

All the python developers are now moving to ORMs. The main reason is it gives high-level abstraction for database interactions which helps prevent SQL injection by automatically handling query parameters safely.

Popular ORM Libraries in Python

  • SQLAlchemy: A powerful ORM that provides both high-level and low-level APIs for database interaction. It offers built-in protection against SQL injection.
  • Django ORM: Part of the Django web framework, it abstracts database queries, making it difficult to inadvertently create vulnerable SQL queries.

Practical Examples: how to prevent sql injection python

1. Using Parameterized Queries with sqlite3. In this example, the parameterized query ensures that username is treated as data, not executable code.
import sqlite3

conn = sqlite3.connect('example.db')
cursor = conn.cursor()

username = 'user'
cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
2. Preventing SQL Injection with MySQLdb Using %s as a placeholder protects against injection by separating SQL code from user input.
import MySQLdb

db = MySQLdb.connect("localhost", "user", "password", "dbname")
cursor = db.cursor()

username = 'user'
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))
3. SQL Injection Prevention in SQLAlchemy
SQLAlchemy’s text construct allows for safe parameter binding, preventing SQL injection.
from sqlalchemy import create_engine, text

engine = create_engine('sqlite:///example.db')
with engine.connect() as conn:
    result = conn.execute(text("SELECT * FROM users WHERE username = :username"), {"username": user_input})

Input Validation and Sanitization

Importance of Validating User Input
Validating user input helps us to ensure that only data matching expected patterns is considered. Which will reduce the risk of SQL injection.

Examples of Input Validation Techniques

  • Regular Expressions: We can use regex to enforce patterns for inputs like email addresses or usernames.
  • Type Checks: We can validate inputs expected data types. For example : Integer, String, Date etc

When to Use Whitelisting and Blacklisting

  • Whitelisting: We can make a list of acceptable input. Based on acceptable input, we will make a pattern for the source of input.
  • Blacklisting: Making a list of bad inputs to filter out input data.

Advanced Techniques to Protect Against SQL Injection

1. Stored Procedures
Implementing stored procedures in your application helps encapsulate SQL code on the database side. They limit the exposure of the database direct SQL commands which adds an extra layer of security.

2. Role-Based Access Control (RBAC)
To keep the database secure we can implement role based access control based on functionalities a user is performing. Limiting the power to execute certain code. This way we are able to reduce the impact of SQL injection attacks.

3. Database Security Best Practices

  • Keep updating the database software regularly to protect against known vulnerabilities.
  • Use strong passwords which have a number, character and special character. The length of the password should be between 10 and 15.
  • Implement firewalls to limit database exposure to the internet.

Testing for SQL Injection Vulnerabilities

Manual Testing Methods
Manually attacking the system by entering a user input placeholder with a query that potentially exposes a vulnerability.
Automated Tools for SQL Injection Detection

  • SQLMap : It is an open-source tool which is designed to automate the process of detecting and exploiting SQL injection vulnerabilities. It supports multiple database types and provides powerful features to automate exploitation.
  • Web Vulnerability Scanners : Tools like OWASP ZAP and Burp Suite can be used to scan web applications for vulnerabilities such as SQL injection. These scanners automate the attacks which were performed manually by us earlier. They simulate attacks and provide reports on potential weaknesses.

Real-World Examples of SQL Injection Attacks

SQL Slammer worm: A notorious example of a SQL injection-based worm that caused significant disruption to the internet in 2003.

Heartbleed bug: A vulnerability in the OpenSSL cryptographic library that could be exploited to extract sensitive information from web servers.

In every SQL injection attack certain steps are involved. If we see a broader overview

  1. First the attacker identifies vulnerable code. He identifies application that directly concatenate user input into SQL queries.
  2. Then attacker creates a malicious SQl statement that manipulates the original query’s logic.
  3. In the last stage, the injected code is executed allowing the attacker to achieve their malicious goals.

The damage of SQL injection can be severe. It causes Data loss, service disruption and reputation damage of a organization.

Conclusion

In Python prevent sql injection by using parameterized queries, prepared statements, Object-Relational Mapping, Input Validation and Sanitization,  Role-Based Access Control and by following Database Security Best Practices. 

We should also test for SQL Injection vulnerabilities by pretending as attackers. Usage of tools like SQLMap and Web vulnerability Scanners can also be used for testing the security of website. You can also go through our other blog which helps you learn difference between continue and pass function.  

1 thought on “Python: Prevent SQL Injection with Best Practices”

  1. Pingback: Python: Error Handling Best Practices

Comments are closed.