رشته نهایی یا قالب قابل بررسی؟

وقتی با f-string یک پیام می‌سازیم، مقدارها وارد رشته نهایی می‌شوند و مرز میان بخش ثابت و داده تا حد زیادی از دست می‌رود. t-string که در Python 3.14 معرفی شد، یک Template می‌سازد که بخش‌های ثابت و مقدارهای درون‌یابی‌شده را جدا نگه می‌دارد. این جداسازی برای ابزارهای قالب، ثبت رخداد و پردازش ورودی مفید است؛ اما به معنی امن‌شدن خودکار همه خروجی‌ها نیست.

کاربرد آموزشی روشن آن، نمایش یک نام کاربر داخل متن HTML است. اگر نام شامل علامت کوچک‌تر باشد، باید به متن تبدیل شود، نه تگ فعال. در نمونه، پردازشگر بخش ثابت را حفظ و هر مقدار را با html.escape تبدیل می‌کند. این مثال فقط برای جای‌گذاری در متن HTML طراحی شده است. محل attribute، URL، JavaScript یا CSS قواعد جدا دارد و نباید همان پردازشگر را بی‌بررسی به آن‌ها تعمیم داد.

پردازش مقدارها در مرز خروجی

Template را باید یک ساختار داده بدانیم، نه یک موتور قالب کامل. درون‌یابی همچنان عبارت پایتون را ارزیابی می‌کند؛ پس استفاده از عبارت دارای اثر جانبی در قالب تصمیم مناسبی نیست. همچنین Template ورودی غیرقابل اعتماد را به کد قابل اجرا تبدیل نمی‌کند و نباید برای ساختن آن سراغ eval بروید. اگر از آن برای SQL استفاده می‌کنید، اتصال به پارامترهای واقعی driver لازم است؛ escape متنی جای bind را نمی‌گیرد.

نمونه عمداً conversion و format spec را رد می‌کند تا رفتار پیچیده را پنهان نکند. پشتیبانی از !r یا قالب عددی به تصمیم روشن درباره ترتیب تبدیل و escape نیاز دارد. اگر قرار است یک پردازشگر در چند تیم استفاده شود، قرارداد نوع ورودی، context خروجی و حالت خطا را مستند کنید. نام امن در تابع، جای این قرارداد را نمی‌گیرد و بهتر است نام آن محدودیت را نشان دهد.

برای آزمون، نام معمولی، نشانه‌های HTML، نقل‌قول و رشته فارسی را امتحان کنید. خروجی را هم به شکل متن و هم در context مجاز صفحه بررسی کنید. در کنار این‌ها، مورد دارای format spec باید خطای پیش‌بینی‌شده بدهد. هدف پروژه آزمایشی فقط چاپ خروجی زیبا نیست؛ باید بتوان نشان داد پردازشگر در موارد پشتیبانی‌نشده بی‌صدا رفتار متفاوتی ندارد.

نمونه کد و روش بررسی

پیش‌نیاز و خروجی مورد انتظار در توضیح مثال آمده است. ابتدا نمونه را در محیط آزمایشی اجرا کنید و سپس آن را با داده و محدودیت‌های پروژه خود تطبیق دهید.

1. فقط Python 3.14 یا جدیدتر؛ پردازشگر آموزشی متن HTML

from html import escape
from string.templatelib import Template

def render_html_text(template: Template) -> str:
    output = []
    for index, interpolation in enumerate(template.interpolations):
        output.append(template.strings[index])
        if interpolation.conversion or interpolation.format_spec:
            raise ValueError("formatting is not supported")
        output.append(escape(str(interpolation.value)))
    output.append(template.strings[-1])
    return "".join(output)

name = "<student>"
print(render_html_text(t"<p>Hello {name}</p>"))

خروجی باید <p>Hello &lt;student&gt;</p> باشد. فقط مقدار پویا escape می‌شود و بخش ثابت باید کد مورد اعتماد توسعه‌دهنده باشد. این مثال sanitizer عمومی HTML نیست و روی Python 3.12 قابل اجرا نیست.

2. دیدن بخش ثابت، مقدار و مشخصات درون‌یابی

این نمونه مستقل پیش از نوشتن processor، ساختار داده را قابل مشاهده می‌کند. expression متن عبارت داخل آکولاد است، ولی value نتیجه ارزیابی‌شده همان عبارت است. نگه‌داشتن هر دو به معنی اجازه اجرای دوباره expression نیست.

# Python 3.14+
name = "لیان"
template = t"Hello {name}!"
assert template.strings == ("Hello ", "!")
assert template.values == ("لیان",)
item = template.interpolations[0]
print("expression:", item.expression)
print("value:", item.value)
print("conversion:", item.conversion)
print("format:", item.format_spec)
assert item.expression == "name"

خروجی expression برابر name و مقدار برابر لیان است. این اطلاعات برای ابزار تحلیل مفید است، ولی expression را به eval ندهید. عبارت‌های دارای اثر جانبی هنگام ساخت Template اجرا می‌شوند؛ پردازشگر نباید پنهانی آن‌ها را تکرار کند.

3. تبدیل Template به SQL پارامتری؛ نه escape متنی

این processor فقط برای مقدارها در جایگاه پارامتر SQLite است. بخش ثابت SQL باید از کد مورد اعتماد توسعه‌دهنده بیاید؛ نام جدول، جهت مرتب‌سازی و نام ستون از جنس پارامتر مقدار نیستند و به allowlist جدا نیاز دارند.

# Python 3.14+; fixed developer-controlled SQL structure only.
import sqlite3

def bind_sql(template):
    pieces, values = [], []
    for index, item in enumerate(template.interpolations):
        pieces.append(template.strings[index])
        if item.conversion or item.format_spec:
            raise ValueError("SQL formatting is not supported")
        pieces.append("?")
        values.append(item.value)
    pieces.append(template.strings[-1])
    return "".join(pieces), values

db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE students(id INTEGER,name TEXT)")
db.execute("INSERT INTO students VALUES(?,?)", (1, "Alice"))
user_name = "Robert'); DROP TABLE students;--"
sql, params = bind_sql(t"SELECT id FROM students WHERE name = {user_name}")
assert db.execute(sql, params).fetchall() == []
assert db.execute("SELECT count(*) FROM students").fetchone()[0] == 1
print("Query parameters preserved; table still exists")
db.close()

انتظار Query parameters preserved; table still exists داریم. مقدار حمله در params می‌ماند و به دستور SQL تبدیل نمی‌شود. دور {user_name} در قالب گیومه نگذارید؛ placeholder را driver جای‌گذاری می‌کند. این کمک‌تابع برای همه dialectها یا قالب‌های SQL دلخواه طراحی نشده است.

4. آزمون متن HTML و رد قالب‌بندی پشتیبانی‌نشده

کد پردازشگر ابتدای مقاله را در safe_html.py و این فایل را در test_safe_html.py بگذارید. هر دو فایل باید با مفسر 3.14 یا جدیدتر parse شوند؛ نصب pytest روی مفسر قدیمی، نحو تازه را قابل اجرا نمی‌کند.

# Save the first example's processor as safe_html.py.
# test_safe_html.py; run with Python 3.14+ and pytest.
import pytest
from safe_html import render_html_text

@pytest.mark.parametrize("value, expected", [
    ("<student>", "<p>&lt;student&gt;</p>"),
    ("A&B", "<p>A&amp;B</p>"),
    ("لیان", "<p>لیان</p>"),
])
def test_text_node_context(value, expected):
    assert render_html_text(t"<p>{value}</p>") == expected

def test_rejects_format_spec():
    value = 42
    with pytest.raises(ValueError):
        render_html_text(t"<p>{value:04d}</p>")

def test_rejects_conversion():
    value = "hello"
    with pytest.raises(ValueError):
        render_html_text(t"<p>{value!r}</p>")

با python -m pytest -q test_safe_html.py انتظار 5 passed داریم. این قرارداد فقط text node است؛ از سبزبودن تست نتیجه نگیرید که درج مقدار در URL، attribute، CSS یا JavaScript هم امن است. هر context پردازش و آزمون مخصوص خود را می‌خواهد.

قابلیت تازه با مسئولیت مشخص

اگر پروژه هنوز روی Python قدیمی‌تر است، واردکردن نحو t-string در فایل باعث خطای parse می‌شود و یک شرط runtime ساده مشکل را حل نمی‌کند. نسخه مورد نیاز را در ابزار build و محیط اجرا مشخص کنید. ابتدا یک مسیر کوچک و غیرحساس را ارزیابی کنید. ارزش قابلیت تازه زمانی روشن می‌شود که جداسازی قالب و داده، قرارداد خروجی را دقیق‌تر و بازبینی آن را ساده‌تر کند.

چک‌لیست اجرای عملی

  • runtime حداقل Python 3.14 باشد.
  • context متن HTML از attribute و اسکریپت جدا باشد.
  • format spec و conversion پشتیبانی‌نشده رد شوند.
  • Template با eval از ورودی کاربر ساخته نشود.

منابع

تدوین و راهنمای عملی: دانشنامه لیان.