متد wpdb::prepare() یکی از مهم‌ترین ابزارهای امنیتی در وردپرس است که هر توسعه‌دهنده‌ای در پروژه‌های واقعی به آن نیاز پیدا می‌کند. این متد مسئول تبدیل پارامترهای متغیر به مقادیر escape شده و امن در کوئری‌های SQL است و بدون آن هر کوئری دستی به یک ریسک جدی تبدیل می‌شود.

متد wpdb::prepare یکی از پرکاربردترین متدهای وردپرس برای اجرای کوئری‌های سفارشی به‌صورت امن است. این متد امکان استفاده از placeholders را فراهم می‌کند و پایه جلوگیری از SQL Injection در پروژه‌های حرفه‌ای محسوب می‌شود. در این راهنما ساختار کامل، انواع placeholder، نمونه‌های واقعی، اشتباهات رایج و نکات امنیتی این متد بررسی می‌شود. همچنین تفاوت آن با کوئری‌های مستقیم و روش‌های بهینه استفاده از آن در پروژه‌های بزرگ توضیح داده می‌شود. در پایان پرسش‌های پرتکرار و منظر مهندسی سطح بالای این متد مرور خواهد شد.

در تعداد زیادی از پروژه‌هایی که برای بازبینی امنیتی بررسی کرده‌ام، نقطه مشترک اکثر آسیب‌پذیری‌ها یک چیز بوده: کوئری دستی بدون prepare. ساده‌ترین راه ورود به دیتابیس یک سایت وردپرسی همین است و خبر خوب اینکه جلوگیری از آن فقط به یک عادت کوچک در نوشتن کد نیاز دارد.

چرا wpdb::prepare اهمیت دارد

وردپرس روی یک لایه انتزاعی به نام $wpdb ساخته شده که تمام ارتباط با دیتابیس از طریق آن انجام می‌شود. متد prepare() در همین لایه، وظیفه escape کردن ورودی‌ها و جایگزینی آن‌ها در query string را دارد. بدون این مرحله، هر ورودی کاربر — چه از URL، چه از فرم، چه از API — می‌تواند به‌عنوان بخشی از دستور SQL اجرا شود.

SQL Injection (تزریق اس کیو ال) یکی از شایع‌ترین حملات وب است که در آن مهاجم با تزریق کدی مثل ' OR 1=1 -- سعی می‌کند منطق کوئری را تغییر دهد. متد prepare این کد را به‌عنوان یک رشته معمولی escape می‌کند و از اجرای آن جلوگیری می‌کند.

برای درک کامل این نوع حملات، مطلب WordPress SQL Injection Prevention را حتماً مطالعه کنید، چون این موضوع پایه‌ای است و باید در تمام پروژه‌ها رعایت شود.

ساختار و امضای متد prepare

امضای این متد به شکل زیر است:

$wpdb->prepare( string $query, mixed ...$args ): string|void

پارامتر اول، query string است که شامل placeholderهاست. بقیه پارامترها به ترتیب در جای placeholderها قرار می‌گیرند. خروجی یک رشته آماده اجرا با متدهای get_results()، get_var()، get_row() و get_col() است.

نکته مهم این است که خروجی prepare را نباید دوباره escape کنید. اگر مقدار را با esc_sql() escape کرده و بعد به prepare بدهید، عملاً double escaping اتفاق می‌افتد و مقدار شما در دیتابیس با کاراکترهای اضافی ذخیره می‌شود.

انواع placeholder و معنای دقیق آن‌ها

در حال حاضر این متد از چند نوع placeholder پشتیبانی می‌کند که در ادامه هرکدام را با مثال بررسی می‌کنیم:

placeholder %s برای رشته

برای مقادیر رشته‌ای، از %s استفاده کنید. این placeholder مقدار را داخل کوتیشن قرار می‌دهد و کاراکترهای خاص را escape می‌کند:

$name = 'Ali';
$query = $wpdb->prepare(
    "SELECT * FROM {$wpdb->users} WHERE display_name = %s",
    $name
);

placeholder %d برای عدد صحیح

برای اعداد صحیح از %d استفاده کنید. این placeholder مقدار را به int تبدیل می‌کند و کوتیشن لازم ندارد:

$user_id = 42;
$query = $wpdb->prepare(
    "SELECT * FROM {$wpdb->usermeta} WHERE user_id = %d",
    $user_id
);

placeholder %f برای اعداد اعشاری

برای اعداد float از %f استفاده می‌شود:

$price = 19.99;
$query = $wpdb->prepare(
    "SELECT * FROM {$wpdb->prefix}orders WHERE amount > %f",
    $price
);

نمونه‌های عملی در پروژه واقعی

در ادامه چند الگوی عملی که در پروژه‌های واقعی زیاد به کار می‌رود را مرور می‌کنیم:

کوئری با شرط IN و آرایه

یکی از پرتکرارترین سوالات این است که چطور یک آرایه از IDها را در IN قرار دهیم. پاسخ: باید برای هر عنصر یک placeholder بسازید:

$ids = array( 1, 2, 3, 4 );
$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
$query = $wpdb->prepare(
    "SELECT * FROM {$wpdb->users} WHERE ID IN ($placeholders)",
    $ids
);

این الگو بسیار پرکاربرد است و اگر درست نوشته نشود، به یکی از رایج‌ترین حفره‌های امنیتی تبدیل می‌شود.

کوئری با LIKE و wildcard

برای جستجو با LIKE، خود wildcard را در placeholder قرار دهید، نه در query string:

$term = 'ali';
$like = '%' . $wpdb->esc_like( $term ) . '%';
$query = $wpdb->prepare(
    "SELECT ID FROM {$wpdb->users} WHERE user_login LIKE %s",
    $like
);

استفاده از $wpdb->esc_like() بسیار مهم است چون کاراکترهای خاص در LIKE مثل % و _ را escape می‌کند. برای آشنایی با دیگر توابع پاک‌سازی، مطلب راهنمای Sanitization در وردپرس منبع جامعی است.

درج داده با insert و prepare

هرچند وردپرس متد $wpdb->insert() را برای درج ارائه می‌دهد، گاهی نیاز به کوئری دستی دارید:

$wpdb->query(
    $wpdb->prepare(
        "INSERT INTO {$wpdb->prefix}logs (user_id, action) VALUES (%d, %s)",
        $user_id,
        $action
    )
);

برای مقایسه با روش‌های استاندارد insert و update، مطلب متد wpdb::insert و همچنین متد wpdb::update را ببینید.

پاک کردن داده با delete

$deleted = $wpdb->query(
    $wpdb->prepare(
        "DELETE FROM {$wpdb->prefix}logs WHERE created_at < %s",
        $date
    )
);

برای درک بهتر الگوهای حذف امن، مطلب متد wpdb::delete را مطالعه کنید.

کوئری با نتیجه آرایه‌ای

بعد از prepare، معمولاً از متدی مثل get_results() برای دریافت نتیجه استفاده می‌کنید. مطلب متد wpdb::get_results جزئیات این مرحله را پوشش می‌دهد.

اشتباهات رایج در استفاده از prepare

بر اساس تجربه بررسی دهها افزونه، این اشتباهات تکراری هستند:

نبود prepare در کوئری

اولین و شایع‌ترین اشتباه: نوشتن کوئری با متغیر مستقیم بدون استفاده از prepare. حتی اگر متغیر از یک منبع مطمئن بیاید، باز هم باید prepare استفاده شود چون ممکن است در آینده منبع تغییر کند.

نبود placeholder کافی

گاهی توسعه‌دهنده placeholder می‌گذارد اما تعداد پارامترها را اشتباه می‌شمارد. این کار به warning و در موارد جدی به بازگشت کوئری ناقص منجر می‌شود.

double escaping

استفاده همزمان از esc_sql() و prepare باعث می‌شود کاراکترهای escape شده در دیتابیس ذخیره شوند و بعداً هنگام خواندن، داده به‌شکل اشتباه نمایش داده شود. این باگ به‌سختی ردیابی می‌شود.

نبود بررسی خطا

همیشه نتیجه prepare را در برابر null و خطا بررسی کنید. گاهی query string نامعتبر است و prepare خروجی null یا false می‌دهد که اگر به get_results() بدهید، خطای PHP رخ می‌دهد.

نبود تست روی ورودی‌های مخرب

همیشه با ورودی‌هایی مثل '، --، ; و کاراکترهای یونیکد تست کنید تا مطمئن شوید prepare درست عمل می‌کند. بدون این تست، فرض اینکه کد امن است، خطرناک است.

امنیت و عملکرد در prepare

در کنار امنیت، این متد روی عملکرد هم اثر می‌گذارد. هر فراخوانی prepare یک مرتبه پردازش رشته روی سمت PHP انجام می‌دهد. برای کوئری‌هایی که در حلقه‌های بزرگ اجرا می‌شوند، این هزینه به سرعت جمع می‌شود. راهکار درست این است که به جای prepare مکرر، یک query واحد با placeholders زیاد بسازید و یک‌بار prepare کنید.

نکته دیگر اینکه prepare روی کوئری‌های بسیار بزرگ می‌تواند سرباز نرخ قابل توجهی داشته باشد. در این حالت استفاده از $wpdb->query() با پارامترهای کاملاً عددی و کنترل‌شده منطقی‌تر است. اما به شرطی که هیچ ورودی کاربر در آن دخیل نباشد.

برای آشنایی با الگوهای پیشرفته کوئری و بهینه‌سازی، مطلب کدنویسی کوئری‌های سفارشی در وردپرس را از دست ندهید.

پرسش‌های پرتکرار درباره wpdb::prepare

آیا prepare برای همه کوئری‌ها لازم است؟

برای هر کوئری که مقدار متغیر در آن وجود دارد، بله. فقط کوئری‌های کاملاً استاتیک که هیچ ورودی خارجی ندارند، از این قاعده مستثنی هستند.

تفاوت prepare با esc_sql چیست؟

esc_sql() فقط کاراکترهای خاص را escape می‌کند اما کوتیشن‌گذاری مناسب انجام نمی‌دهد. prepare هم escape می‌کند و هم بر اساس placeholder کوتیشن درست می‌گذارد.

آیا prepare از آرایه پشتیبانی می‌کند؟

خیر، prepare آرایه را به‌عنوان یک پارامتر نمی‌فهمد. باید برای هر عنصر آرایه یک placeholder بسازید و آرایه را با splat operator پاس دهید.

آیا prepare روی جدول‌های سفارشی هم کار می‌کند؟

بله، prepare مستقل از جدول است و روی هر کوئری SQL کار می‌کند. فقط باید نام جدول سفارشی خودتان را با دقت و به‌صورت hard-coded قرار دهید (نه از ورودی کاربر).

آیا استفاده از prepare روی همه کوئری‌ها سرعت را کم می‌کند؟

در حالت عادی اختلاف ناچیز است. فقط در حلقه‌های بسیار پرتکرار می‌تواند اثرگذار باشد که در آن صورت، بازطراحی کوئری راه‌حل است، نه حذف prepare.

منظر یک مهندس ارشد

در سطح مهندسی، wpdb::prepare() یک prepared statement کامل نیست. این متد صرفاً یک لایه escape روی رشته انجام می‌دهد و کوئری نهایی به‌صورت رشته واحد به سرور MySQL ارسال می‌شود. این یعنی وردپرس از مزایای واقعی prepared statement سرور مثل plan caching بهره‌مند نمی‌شود. این موضوع در دیتابیس‌های بسیار پرترافیک می‌تواند یک تفاوت عملکردی معنادار با PDO واقعی ایجاد کند.

نکته دوم اینکه، prepare روی همه نوع داده‌ها به‌شکل یکسان عمل نمی‌کند. برای %s مقدار را در utf8mb4 و با charset فعلی escape می‌کند. اگر charset اتصال اشتباه تنظیم شده باشد (که در برخی سرورهای قدیمی اتفاق می‌افتد)، حتی prepare هم نمی‌تواند جلوی برخی حملات خاص را بگیرد. بنابراین بررسی $wpdb->charset و $wpdb->collate در محیط تولید ضروری است.

در نهایت، اگر می‌خواهید یک لایه Database Abstraction سفارشی برای پروژه‌های Enterprise بنویسید، می‌توانید از ترکیب prepare و query builderهای اختصاصی استفاده کنید. برای درک الگوهای حرفه‌ای کوئری، مطلب بهینه‌سازی کوئری‌های وردپرس را توصیه می‌کنم. همچنین الگوهای سبک‌سازی اسنیپت‌های کوئری در قطعه کد بهینه‌سازی کوئری قابل بررسی است.

در پایان، اگر تجربه‌ای از مواجهه با یک SQL Injection واقعی یا حتی یک مشکل double escaping در پروژه‌ای دارید، برای من جالب است بدانید کدام مرحله از رفع مشکل بیشترین زمان را گرفته است. تجربه خود را در دیدگاه‌ها بنویسید تا برای سایر توسعه‌دهندگان هم مفید باشد.