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:
- Literal string values:
"SELECT * FROM users" - Concatenations of literals:
"SELECT " + "* FROM users" - F-strings containing only other literal strings:
f"{'SELECT'} {'*'}"
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
- Shift-Left Security:
LiteralStringdetects injection risks during development or CI/CD static type checks, long before code reaches a staging or production environment. - Zero Runtime Overhead: Like all Python type
annotations,
LiteralStringhas no runtime performance cost and is completely ignored by the Python interpreter during execution. - API Design Standard: Library authors and internal
framework designers can use
LiteralStringto make unsafe string interpolations syntactically invalid across an entire codebase.