Python LiteralString: Prevent SQL and Command Injection

Python's typing.LiteralString, introduced in PEP 675 and Python 3.11, provides a compile-time mechanism to eliminate injection vulnerabilities like SQL injection and command injection. By distinguishing between hardcoded, developer-controlled string literals and arbitrary user-controlled strings, static type checkers can flag unsafe string interpolations before code ever runs in production. This article explains how LiteralString works, how type checkers enforce its constraints, and how to apply it to secure database queries and system command execution.

The Injection Problem with Standard Type Hints

Static type checkers like Mypy and Pyright have long accepted str as a universal type for textual data. However, str does not distinguish between trusted text written directly in source code and untrusted input received over the network or from a user interface:

def execute_query(query: str) -> None:
    ...

# Safe: Developer-authored literal
execute_query("SELECT * FROM users WHERE active = 1")

# Unsafe: SQL Injection vulnerability, but valid under `str`
user_input = "1; DROP TABLE users; --"
execute_query(f"SELECT * FROM users WHERE id = {user_input}")

Because both expressions evaluate to str, the type checker sees no difference, allowing dangerous string concatenation and f-strings to pass static analysis without warning.

How typing.LiteralString Works

typing.LiteralString solves this problem by defining a type that represents any string composed exclusively of source-code literals. Unlike typing.Literal["exact_string"], which requires an exact, pre-declared value, LiteralString matches any string literal, as well as any expression built exclusively from other LiteralString instances.

A type checker will infer LiteralString for:

Crucially, if a dynamic variable of type str is concatenated or formatted into the string, the resulting type is widened to standard str, losing its LiteralString status.

Securing SQL Queries

To prevent SQL injection, database client libraries can type their query parameter as LiteralString while requiring dynamic arguments to be passed separately as query parameters.

from typing import Any, LiteralString

def run_query(query: LiteralString, params: tuple[Any, ...] = ()) -> None:
    # Database execution logic using parameterized queries
    ...

user_id = "123"

# PASSES TYPE CHECKING:
# The query template is a LiteralString; the variable is parameterized.
run_query("SELECT * FROM users WHERE id = %s", (user_id,))

# FAILS TYPE CHECKING:
# Argument 1 to "run_query" has incompatible type "str"; expected "LiteralString"
run_query(f"SELECT * FROM users WHERE id = {user_id}")

When developers run static analysis, the type checker detects that the f-string contains a standard str (user_id), invalidating LiteralString compatibility and immediately halting the build.

Securing Command Execution

The same principle applies to system shells and external command wrappers. Command injection typically occurs when dynamic parameters are concatenated directly into a shell command line.

import subprocess
from typing import LiteralString

def execute_command(command_template: LiteralString, *args: str) -> None:
    # Enforces separation between executable path/flags and runtime values
    subprocess.run([command_template, *args], check=True)

filename = "report.txt; rm -rf /"

# PASSES TYPE CHECKING:
# "ls" is a LiteralString; filename is passed as an isolated argument.
execute_command("ls", filename)

# FAILS TYPE CHECKING:
# Dynamic formatting produces `str`, which violates the LiteralString signature.
execute_command(f"ls {filename}")

By enforcing LiteralString on the executable or command base, APIs compel developers to structure invocations using parameter lists rather than arbitrary shell strings.

Composing Safe Dynamic Queries

Applications often need to build queries dynamically based on logic, such as conditionally appending ORDER BY or LIMIT clauses. LiteralString allows composition as long as all constituent fragments are also literals:

def build_query(sort_descending: bool) -> None:
    query: LiteralString = "SELECT * FROM products"
    
    if sort_descending:
        # Valid: Appending a literal retains LiteralString type
        query += " ORDER BY price DESC"
    else:
        query += " ORDER BY price ASC"
        
    run_query(query)

Because both appended segments are hardcoded literals, the composite query remains a valid LiteralString.

Key Takeaways

  1. Shift-Left Security: LiteralString detects injection risks during development or CI/CD static type checks, long before code reaches a staging or production environment.
  2. Zero Runtime Overhead: Like all Python type annotations, LiteralString has no runtime performance cost and is completely ignored by the Python interpreter during execution.
  3. API Design Standard: Library authors and internal framework designers can use LiteralString to make unsafe string interpolations syntactically invalid across an entire codebase.