Skip to content

Security toolkit demonstrating how Python 3.14's template strings can be leveraged to prevent common injection vulnerabilities in web applications.

Notifications You must be signed in to change notification settings

nim444/python-tstring-injection-prevention

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 

Repository files navigation

Python Template Strings (PEP-750): A Solution for XSS and SQL Injection Prevention

Python 3.14+ PEP-750 Security

Introduction

PEP-750 introduces template strings (t-strings) in Python 3.14, a powerful generalization of f-strings that helps developers write more secure code. Instead of evaluating to str, t-strings evaluate to a new type, Template:

template: Template = t"Hello {name}"

under

Motivation

Python f-strings are easy to use and very popular. However, they provide no way to intercept and transform interpolated values before they are combined into a final string. This limitation makes them unsuitable for certain security-sensitive use cases.

As a result, incautious use of f-strings can lead to serious security vulnerabilities:

  1. SQL Injection Attacks: When developers use f-strings to build SQL queries
  2. Cross-Site Scripting (XSS): When unescaped user input is included in HTML output

Template strings address these problems by providing developers with access to the string and its interpolated values before they're combined, enabling proper sanitization and escaping.

Preventing SQL Injection with Template Strings

SQL injection occurs when untrusted data is inserted directly into a SQL query string without proper sanitization. Using f-strings to build SQL queries is particularly dangerous:

# DANGEROUS - DO NOT USE! Vulnerable to SQL injection
username = "user'; DROP TABLE users; --"
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query)  # Could delete your users table!

With template strings, we can create a safer SQL query builder:

import sqlite3
from string.templatelib import Template, Interpolation

def safe_sql(template: Template) -> tuple[str, list]:
    query_parts = []
    params = []
    
    for item in template:
        match item:
            case str() as s:
                query_parts.append(s)
            case Interpolation() as interp:
                query_parts.append("?")  # Use parameterized query
                params.append(interp.value)
                
    return "".join(query_parts), params

# Safe usage
username = "user'; DROP TABLE users; --"
template = t"SELECT * FROM users WHERE username = '{username}'"
query, params = safe_sql(template)
cursor.execute(query, params)  # Safe, uses parameters

# This produces:
# query = "SELECT * FROM users WHERE username = ?"
# params = ["user'; DROP TABLE users; --"]

The safe_sql function extracts the interpolated values and uses them as parameters in a prepared statement, which is the recommended way to prevent SQL injection.

Preventing XSS with Template Strings

Cross-Site Scripting (XSS) occurs when untrusted data is included in a web page without proper HTML escaping:

# DANGEROUS - DO NOT USE! Vulnerable to XSS
user_input = "<script>stealCookies()</script>"
html = f"<div>Welcome, {user_input}!</div>"  # Executes malicious JavaScript

With template strings, we can create an HTML template processor that automatically escapes content:

import html as html_escape
from string.templatelib import Template, Interpolation

def html(template: Template) -> str:
    parts = []
    
    for item in template:
        match item:
            case str() as s:
                parts.append(s)
            case Interpolation() as interp:
                if isinstance(interp.value, dict) and interp.expression.strip() == "attributes":
                    # Special handling for HTML attributes
                    attr_str = " ".join(f'{k}="{html_escape.escape(str(v))}"' 
                                      for k, v in interp.value.items())
                    parts.append(attr_str)
                else:
                    # Auto-escape all other interpolated values
                    parts.append(html_escape.escape(str(interp.value)))
                    
    return "".join(parts)

# Safe usage - content is automatically escaped
evil_input = "<script>alert('evil')</script>"
template = t"<div>Welcome, {evil_input}!</div>"
safe_html = html(template)
# Result: "<div>Welcome, &lt;script&gt;alert('evil')&lt;/script&gt;!</div>"

# Handle HTML attributes elegantly
attributes = {"class": "user-profile", "data-id": "12<>"}
template = t"<div {attributes}>User content</div>"
safe_html = html(template)
# Result: '<div class="user-profile" data-id="12&lt;&gt;">User content</div>'

This approach ensures that all user-provided content is automatically escaped, preventing XSS vulnerabilities while making the code clean and readable.

Additional Examples

Example: Implementing f-strings with t-strings

It is easy to "implement" f-strings using t-strings. That is, we can write a function f(template: Template) -> str that processes a Template in much the same way as an f-string literal, returning the same result:

name = "World"
value = 42
templated = t"Hello {name!r}, value: {value:.2f}"
formatted = f"Hello {name!r}, value: {value:.2f}"
assert f(templated) == formatted

The f() function supports both conversion specifiers like !r and format specifiers like :.2f. The full code is fairly simple:

from string.templatelib import Template, Interpolation
from typing import Literal

def convert(value: object, conversion: Literal["a", "r", "s"] | None) -> object:
    if conversion == "a":
        return ascii(value)
    elif conversion == "r":
        return repr(value)
    elif conversion == "s":
        return str(value)
    return value

def f(template: Template) -> str:
    parts = []
    for item in template:
        match item:
            case str() as s:
                parts.append(s)
            case Interpolation(value, _, conversion, format_spec):
                value = convert(value, conversion)
                value = format(value, format_spec)
                parts.append(value)
    return "".join(parts)

Example: Structured Logging

Structured logging allows developers to log data in machine-readable formats like JSON. With t-strings, developers can easily log structured data alongside human-readable messages using just a single log statement.

import json
import logging
from string.templatelib import Interpolation, Template
from typing import Mapping

class TemplateMessage:
    def __init__(self, template: Template) -> None:
        self.template = template

    @property
    def message(self) -> str:
        # Use the f() function from the previous example
        return f(self.template)

    @property
    def values(self) -> Mapping[str, object]:
        return {
            item.expression: item.value
            for item in self.template
            if isinstance(item, Interpolation)
        }

    def __str__(self) -> str:
        return f"{self.message} >>> {json.dumps(self.values)}"

# Usage example
_ = TemplateMessage  # optional, to improve readability
action, amount, item = "traded", 42, "shrubs"
logging.info(_(t"User {action}: {amount:.2f} {item}"))

# Outputs:
# User traded: 42.00 shrubs >>> {"action": "traded", "amount": 42, "item": "shrubs"}

Conclusion

Template strings (PEP-750) provide a powerful mechanism for making Python applications more secure by allowing developers to intercept and transform interpolated values before they are combined into a final string. This is particularly valuable for preventing common security vulnerabilities like SQL injection and XSS attacks.

By using template strings instead of f-strings in security-sensitive contexts, developers can write code that is both clean and secure, helping to prevent some of the most common and dangerous security vulnerabilities in web applications.

Resources

About

Security toolkit demonstrating how Python 3.14's template strings can be leveraged to prevent common injection vulnerabilities in web applications.

Topics

Resources

Stars

Watchers

Forks