پاکسازی دادهها در کدنویسی وردپرس
راهنمای عملی پاکسازی دادهها در وردپرس؛ از sanitize ورودی و escape خروجی تا سناریوهای واقعی و اشتباهات رایج بر پایه تجربه میدانی.
چیزی که هیچوقت فراموش نمیکنم، آن پیام صبحِ زودِ یک شنبه بود: «سایت پر از لینکهای شرطبندی شده، مشتریهایمان دارند فرار میکنند.» سایت یک آموزشگاه زبان بود، نه فروشگاه، نه سایت خبری. هک شده بود، ولی نه از درِ بسته؛ از یک فرم تماس ساده که فیلد «موضوع» را بدون پاکسازی ذخیره میکرد و در پنل مدیریت، بدون escape نمایش میداد. یک خط `< script >` که در دیتابیس جا خوش کرده بود، در هر بار بازدید پنل، یک اسکریپت خارجی را اجرا میکرد که در پسزمینه، لینکهای شرطبندی تزریق میکرد. آن روز صبح، وقتی کد را بازبینی میکردم، فقط یک جمله در ذهنم تکرار میشد: پاکسازی داده، ارزانترین بیمهنامهای است که یک توسعهدهنده وردپرس میتواند برای سایت خودش و مشتریاش بخرد. این مقاله، همان چیزهایی است که از آن روز، در هر پروژهای — از افزونههای کوچک تا پلتفرمهای سازمانی — بهعنوان قاعده پاکسازی داده اجرا میکنم. اگر با مفاهیم پایه آشنا نیستید، پیش از ادامه افزونه وردپرس چیست، نوشتن PHP امن برای وردپرس و اعتبارسنجی دادهها در وردپرس را بخوانید.
سه هفتهای که سرنوشت پروژههایم را تغییر داد
قبل از آن شنبه، پاکسازی داده برای من یک «قاعده استاندارد» بود که در لیست بازبینی کد نوشته شده بود و سرسری اجرا میشد. بعد از آن حادثه، سه هفته کار روی پاکسازی سایت، مسیرم را برای همیشه عوض کرد. سه درسی که در آن سه هفته گرفتم، امروز پایه کارم در هر پروژه است:
یک: تفاوت بین «پاکسازی شده در ورودی» و «پاکسازی شده در ذخیره» جدی است. در آن پروژه، تیم فکر میکرد چون $_POST را قبل از ذخیره با sanitize_text_field فیلتر کرده، خطر رفع شده. اما یک مسیر جانبی وجود داشت — داده از یک endpoint قدیمی وارد میشد و مسیر مستقیم به update_post_meta داشت. این نوع «مسیر جانبی»، جایی است که ۸۰٪ آسیبپذیریها را ایجاد میکند.
دو: پاکسازی یک تابع نیست؛ یک جریان است. داده در طول مسیر از فرم تا ذخیره تا نمایش، سه یا چهار نقطه دارد که باید فیلتر شود. هر جا از این نقاط را نادیده بگیرید، یک شانس برای مهاجم باز گذاشتهاید. در آن سایت، مسیر «فرم → ذخیره → پنل مدیریت → ایمیل روزانه» چهار نقطه داشت و فقط یکیشان پاکسازی میشد.
سه: پاکسازی، ارزانترین نوع پیشگیری است. هزینه پاکسازی سایت آسیبدیده، هزینه بازگرداندن اعتماد مشتری، و هزینه زمانی که تیم صرف «توضیح دادن به مدیرعامل» کرد، چند ده برابر زمانِ نوشتن چند خط تابع پاکسازی بود. از آن روز، در هر پروژهای که تحویل میگیرم، اول به سراغ مسیرهای داده میروم، نه به ظاهر سایت.
پاکسازی داده، ارزانترین بیمهنامهای است که میتوانید برای سایت مشتری بخرید — ولی فقط وقتی که از روز اول بخرید، نه بعد از فاجعه.
پاکسازی داده دقیقاً چیست؟
پاکسازی (Sanitization) فرآیند تبدیل داده «خطرناک» به داده «ایمن» است. تفاوت مهمی است با اعتبارسنجی که در مقاله جداگانه اعتبارسنجی دادهها در وردپرس کامل باز کردهام؛ اما برای درک تفاوت، این مثال ساده کافی است:
- اعتبارسنجی (Validation): «آیا این ایمیل واقعاً یک ایمیل است؟» — پاسخ بله یا خیر.
- پاکسازی (Sanitization): «این ایمیل را طوری تبدیل کن که حتی اگر مخرب بود، ضرری نزند.» — خروجی یک مقدار اصلاحشده.
مثال عملی: اگر کاربر وارد کند ali@example.com<script>alert()</script>، اعتبارسنجی میگوید «این ایمیل معتبر نیست» و ذخیره را رد میکند. اما اگر شما به هر دلیلی نمیخواهید داده را رد کنید (مثلاً چون کاربر خوب است و فقط اشتباه تایپی کرده)، پاکسازی همان رشته را به ali@example.com تبدیل میکند و بدون دردسر ذخیره میشود.
در پروژههای واقعی، این دو لایه مکمل هم هستند: اعتبارسنجی، داده نامعتبر را رد میکند؛ پاکسازی، داده خطرناک را بیآزار میکند. یکی بدون دیگری، نیمی از مسیر را پوشش میدهد.
چهار لحظهای که باید پاکسازی کنید
در تجربهام، اکثر آسیبپذیریها از این میآید که توسعهدهنده فکر میکند «اگر پاکسازی را در یک نقطه انجام بدهم، کافی است». اما داده، در طول مسیر از فرم تا نمایش، چهار نقطه دارد که باید فیلتر شود:
- لحظه دریافت از کاربر: وقتی داده از
$_GET،$_POST،$_COOKIEیا$_FILESخوانده میشود. پاکسازی اینجا، از آلوده شدن دیتابیس جلوگیری میکند. - لحظه ذخیره در دیتابیس: وقتی داده با
update_post_meta،update_optionیاwp_insert_postذخیره میشود. اینجا لایه دفاعی دوم است؛ اگر ورودی پاکسازی نشده بود، اینجا فرصت جبران است. - لحظه خواندن از دیتابیس: اگر دادههای قدیمی آلوده در دیتابیس دارید، پاکسازی در لحظه خواندن، جلوی انتشار آلودگی را میگیرد.
- لحظه نمایش به کاربر: escape خروجی، آخرین لایه دفاعی است. اگر سه لایه قبلی از کار بیفتند، این لایه، سایت را نجات میدهد.
نکتهای که در پروژههای بزرگ به آن رسیدهام: هیچوقت روی یک لایه تنها حساب نکنید. ساختار جریان داده، در پروژههای سازمانی، پر از مسیرهای جانبی است — یک endpoint قدیمی، یک تابع کمکی که یادتان رفته، یک اسنیپت در چایلد تم. پاکسازی چهار لایه، همان چیزی است که «دفاع در عمق» نام دارد و در امنیت وردپرس، حیاتی است. مسیر کامل استراتژی دفاع در عمق را در امنیت وردپرس برای مبتدیان آوردهام.
دادهای که چهار بار پاکسازی شده، هرگز کار مهاجم را به پایان نمیرساند — حتی اگر یک لایه از چهار لایه خراب شود.
پاکسازی ورودی بر اساس نوع داده
وردپرس برای هر نوع داده، تابع پاکسازی اختصاصی دارد. استفاده از sanitize_text_field برای همه چیز، یک اشتباه رایج است که هم داده را خراب میکند و هم امنیت را کم میکند. جدول زیر، نگاشت من است در پروژههای واقعی:
| نوع داده | تابع پاکسازی | نکته |
|---|---|---|
| متن کوتاه ساده | sanitize_text_field | حذف تگ، کاراکترهای کنترلی و فضای اضافی |
| متن چندخطی | sanitize_textarea_field | مانند متن ساده، ولی خط جدید حفظ میشود |
| HTML مجاز | wp_kses_post | فقط تگهای امنِ استاندارد پست |
| ایمیل | sanitize_email | حذف کاراکترهای غیرمجاز |
| URL (برای ذخیره) | esc_url_raw | حذف پروتکلهای خطرناک |
| کلید / slug | sanitize_key | حروف کوچک، اعداد، خط تیره، زیرخط |
| نام فایل | sanitize_file_name | حذف کاراکترهای خطرناک در نام فایل |
| عدد صحیح | absint | تبدیل به عدد صحیح مثبت |
| عنوان / نام کلاس | sanitize_html_class | برای HTML class امن |
| ایمیل چندتایی | sanitize_email + آرایه | هر ایمیل جداگانه پاکسازی شود |
یک نکته تخصصی از تجربه: در پروژههای چندزبانه، همیشه داده را با wp_unslash قبل از پاکسازی، از escape خودکار وردپرس آزاد کنید. اگر این را نادیده بگیرید، کاراکترهای بکاسلش در متن فارسی، نابود میشوند. الگوی صحیح:
$title = sanitize_text_field( wp_unslash( $_POST['title'] ) );
مسیر کامل این تکنیک در نوشتن PHP امن برای وردپرس آمده است.
Escape خروجی بر اساس context
پاکسازی ورودی، کافی نیست. دادهای که از دیتابیس خوانده میشود، در لحظه نمایش هم باید escape شود. چرا؟ چون ممکن است:
- داده از مسیر جانبی وارد دیتابیس شده باشد (مثل مهاجرت داده یا اسکریپت قدیمی).
- افزونه دیگری داده را بعد از پاکسازی شما تغییر داده باشد.
- در آپدیت وردپرس، تابع پاکسازی شما رفتار متفاوتی پیدا کرده باشد.
escape خروجی، آخرین لایه دفاعی است. جدول زیر، نگاشت من است برای هر context:
| context خروجی | تابع escape | مثال |
|---|---|---|
| متن بین تگهای HTML | esc_html | <h2><?php echo esc_html( $title ); ?></h2> |
| مقدار attribute | esc_attr | class="<?php echo esc_attr( $class ); ?>" |
| URL در href/src | esc_url | href="<?php echo esc_url( $link ); ?>" |
| درون جاوااسکریپت | esc_js | var name = ; |
| داخل textarea | esc_textarea | <textarea><?php echo esc_textarea( $content ); ?></textarea> |
| HTML مجاز | wp_kses_post | echo wp_kses_post( $content ); |
یک تجربه مستقیم از خودم: در یک پروژه چندزبانه، تیم محتوایی شاکی بود که عنوانهای فارسی، بعد از یک آپدیت، به هم ریخته نمایش داده میشوند. هیچکس شکی به escape نداشت. بعد از بررسی، کشف شد که در قالب، از esc_url برای نمایش عنوان نوشته استفاده شده بود. esc_url برای متن، کاراکترهای خاص و بعضی حروف را حذف میکند. یک خط تغییر از esc_url به esc_html، مشکل را حل کرد. از آن روز، در بازبینی کد، بهجای پرسیدن «آیا escape شده؟»، میپرسم «با context مناسب escape شده؟».
پاکسازی و escape: دو روی یک سکه
در پروژههای خودم، پاکسازی و escape را بهعنوان دو مفهوم جداگانه آموزش میدهم، چون هرکدام یک هدف متفاوت دارند:
- پاکسازی برای ورودی است و در لحظه دریافت یا ذخیره اجرا میشود. هدفش: داده آلوده وارد سیستم نشود.
- Escape برای خروجی است و در لحظه نمایش اجرا میشود. هدفش: داده آلوده به HTML تبدیل نشود.
اگر فقط یکی را داشته باشید:
- فقط پاکسازی: دادههای قدیمی آلوده که در دیتابیس هستند، در نمایش، خطر ایجاد میکنند.
- فقط escape: دادههای آلوده در دیتابیس انبار میشوند و در هر نمایش، escape میشوند؛ ولی ابزارهای دیگر (API، ایمیل، صادرات داده) ممکن است آنها را بدون escape منتشر کنند.
هر دو با هم، سدِ کاملی میسازند. راهنمای تفصیلی این تفکیک را در نوشتن PHP امن برای وردپرس آوردهام.
پاکسازی، درِ ورودیِ خانه است؛ escape، پنجرهها. بازگذاشتنِ یکی از این دو، بهاندازه سرقت از خانه، دردناک است.
پاکسازی در ذخیره متادیتا
متاباکسها، فیلدهای سفارشی و متادیتای نوعنوشته سفارشی، از پرتکرارترین نقاط نفوذ در پروژههای وردپرسی هستند. الگوی استاندارد:
function myplugin_save_metabox( $post_id ) {
// بررسی نانس
if ( ! isset( $_POST['my_nonce'] )
|| ! wp_verify_nonce( $_POST['my_nonce'], 'my_action' ) ) {
return;
}
// جلوگیری از autosave
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
// بررسی دسترسی کاربر
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
// پاکسازی بر اساس نوع
if ( isset( $_POST['my_text'] ) ) {
update_post_meta(
$post_id,
'_my_text',
sanitize_text_field( wp_unslash( $_POST['my_text'] ) )
);
}
if ( isset( $_POST['my_number'] ) ) {
update_post_meta(
$post_id,
'_my_number',
absint( $_POST['my_number'] )
);
}
if ( isset( $_POST['my_html'] ) ) {
update_post_meta(
$post_id,
'_my_html',
wp_kses_post( wp_unslash( $_POST['my_html'] ) )
);
}
}
سه نکته که در پروژهها زیاد دیدهام:
- پیشوند زیرخط در کلید متا:
_my_textبهجایmy_textتا در باکس Custom Fields پیشفرض نمایش داده نشود. - پاکسازی بر اساس نوع داده: اگر فیلد شما یک URL است، از
esc_url_rawاستفاده کنید؛ اگر HTML است، ازwp_kses_post. - استفاده از
wp_unslashقبل از پاکسازی: وردپرس داده POST را با escape خودکار ذخیره میکند؛ قبل از پاکسازی، باید این لایه را حذف کنید.
مسیر کامل پیادهسازی متاباکس را در کار با متاباکسها و توابع متادیتای وردپرس آوردهام.
پاکسازی در آپلود فایل
آپلود فایل، یکی از پرخطرترین نقاط سایتهای وردپرسی است. مهاجم میتواند یک فایل PHP با پسوند دوگانه (مثل image.php.jpg) آپلود کند و اگر پاکسازی درست نباشد، آن را اجرا کند. الگوی استاندارد:
$allowed = array(
'jpg|jpeg' => 'image/jpeg',
'png' => 'image/png',
'webp' => 'image/webp',
'pdf' => 'application/pdf',
);
$check = wp_check_filetype( $_FILES['file']['name'], $allowed );
if ( ! $check['ext'] ) {
wp_die( 'نوع فایل مجاز نیست' );
}
// حذف فایلهای مشکوک
if ( strpos( $_FILES['file']['name'], '\0' ) !== false ) {
wp_die( 'نام فایل غیرمجاز' );
}
$upload = wp_handle_upload( $_FILES['file'], array(
'test_form' => false,
'mimes' => $allowed,
) );
سه تکنیک دفاعی که در پروژههای خودم استفاده میکنم:
- اعتبارسنجی پسوند و MIME: هم پسوند و هم MIME type فایل را بررسی کنید. بعضی مهاجمها، پسوند را جعل میکنند اما MIME را نه.
- محدودیت حجم: فایلهای بالای یک حجم مشخص، همیشه محدود شوند.
- ذخیره خارج از مسیر اجرایی: فایلهای آپلودی حساس (مثل PDF)، هرگز در پوشهای که PHP در آن اجرا میشود، قرار نگیرند.
راهنمای کامل مسائل امنیتی و پاکسازی را در افزونههای امنیتی وردپرس و امنسازی ورود ادمین وردپرس آوردهام.
پاکسازی داده قبل از کوئری
در کوئری مستقیم با $wpdb، همیشه از prepare استفاده کنید. اما اگر مجبور به ساخت کوئری پیچیده با پارامترهای متغیر هستید، پاکسازی اضافه ضروری است:
$status_list = array( 'publish', 'draft' );
$status_list = array_map( 'sanitize_key', $status_list );
$placeholders = implode( ',', array_fill( 0, count( $status_list ), '%s' ) );
$sql = $wpdb->prepare(
"SELECT * FROM {$wpdb->posts}
WHERE post_status IN ($placeholders)",
$status_list
);
$results = $wpdb->get_results( $sql );
یک تکنیک مهم: در کوئریهای پویا با آرایه، prepare وردپرس بهتنهایی کافی نیست. باید مطمئن شوید که دادهها قبل از ورود به prepare، نوعشان مشخص است. مسیر کامل کوئریهای سفارشی را در توابع کوئری سفارشی و بهینهسازی کوئریهای وردپرس آوردهام.
جدول چکلیست پاکسازی
جمعبندی چکلیست پاکسازی داده در وردپرس:
| نقطه پاکسازی | تابع یا ابزار | نکته کلیدی |
|---|---|---|
| دریافت داده از فرم | sanitize_text_field و مشابه | قبل از ذخیره، نه بعد از آن |
| ذخیره در option | sanitize_callback در register_setting | الزامی است، نه اختیاری |
| ذخیره در post meta | پاکسازی بر اساس نوع فیلد | با wp_unslash اولیه |
| نمایش در HTML | esc_html، esc_attr، esc_url | بر اساس context |
| آپلود فایل | wp_check_filetype و wp_handle_upload | لیست mimeهای مجاز مشخص |
| کوئری با wpdb | prepare با placeholder مناسب | حتی برای اعداد |
| پاسخ REST API | sanitize_callback در args | پارامترها جدا تعریف شوند |
ابزارهای بررسی پاکسازی
پس از نوشتن کد، سه ابزار در پروژههای خودم برای بررسی پاکسازی استفاده میکنم:
- PHP_CodeSniffer با استاندارد WordPress: ابزار رسمی که بخش امنیتی آن، خروجی بدون escape و ورودی بدون sanitize را کشف میکند. مسیر پیادهسازی در استفاده از استانداردها در پروژهها.
- Query Monitor: برای پایش کوئریهای اجرا شده و بررسی اینکه آیا از prepare استفاده شده یا نه.
- افزونههای امنیتی اسکن کد: که گاهی حتی خطاهای منطقی در پاکسازی را کشف میکنند. فهرست در افزونههای امنیتی وردپرس.
یک تجربه شخصی: در یکی از پروژههای فروشگاهی، PHP_CodeSniffer بیش از ۸۰ نقطه عدم escape در کد قالب پیدا کرد که در بازبینی چشمی چندین بار نادیده گرفته شده بودند. اگر آن ابزار نبود، احتمالاً همان سایت، در ماه سوم، قربانی یک XSS ساده میشد. از آن پروژه، ابزار PHP_CodeSniffer را در جریان کاری همیشگی تیم گنجاندهام.
اشتباهات رایج در پاکسازی داده
در اشتباهات امنیتی رایج وردپرس و اشتباهات رایج کدنویسی وردپرس فهرست کامل را نوشتهام؛ اما هفت مورد که در پاکسازی داده بیشتر میبینم:
- استفاده از یک تابع پاکسازی برای همه نوع داده:
sanitize_text_fieldبرای همه چیز، داده URL و HTML را نابود میکند. - فراموش کردن
wp_unslash: باعث از دست رفتن بکاسلشها و بههمریختن متون فارسی در بعضی موارد میشود. - escape با تابع اشتباه: استفاده از
esc_urlبرای متن، حروف خاص را حذف میکند. استفاده ازesc_htmlدر attribute، attribute را میشکند. - پاکسازی بعد از ذخیره: اگر داده را در دیتابیس آلوده ذخیره کنید، پاکسازی بعدی، همیشه کامل نیست — بعضی مسیرها مثل API و صادرات، از آن پاکسازی استفاده نمیکنند.
- نادیدهگرفتن REST API: endpointهایی که
sanitize_callbackندارند، مسیر ورود آلودگی هستند. مسیر کامل در ساخت API اختصاصی. - پاکسازی سطحی فایلهای آپلودی: فقط بررسی پسوند کافی نیست؛ MIME و حجم هم لازم است.
- فقدان بازبینی دورهای: پاکسازی، یکبار برای همیشه نیست. با هر افزونه جدید و هر آپدیت، مسیرهای داده جدیدی باز میشوند که باید بازبینی شوند.
یک اشتباه کمتکرار اما گرانقیمت: استفاده از strip_tags بهجای wp_kses_post. strip_tags تمام تگها را حذف میکند؛ wp_kses_post فقط تگهای خطرناک را حذف میکند و ساختار HTML محتوا را نگه میدارد. در پروژههای محتوایی، این تفاوت، بین محتوای سالم و متن بههمریخته است.
دید مهندسی
از منظر مهندسی، پاکسازی داده در وردپرس یک تصمیم معماری پیوسته است، نه یک سری توابع که در انتهای کد اضافه میشوند. سه لایه را در پروژههای حرفهای همیشه مرور میکنم. لایه اول، معماری جریان داده: در پروژههای بزرگ، مسیر داده از فرم تا دیتابیس تا نمایش باید یک نقشه مشخص داشته باشد. هر گره در این نقشه، یک نقطه پاکسازی است. اگر این نقشه را در فاز طراحی بکشید، در فاز پیادهسازی سه برابر سریعتر پیش میروید. تجربهام این است که پروژههایی که نقشه داده دارند، در بازبینی کد، کمتر از یکسوم خطای امنیتی دارند. تفصیل این معماری در ساختاربندی پروژه وردپرس آوردهام. لایه دوم، پاکسازی بهعنوان بخشی از Domain Model: در سیستمهای پیچیده، هر موجودیت داده (User، Order، Product) باید قواعد پاکسازی خودش را داشته باشد. مثال: یک شماره تماس، فقط با اعداد و خط تیره؛ یک قیمت، با دو رقم اعشار. این قواعد باید در لایه منطق کسبوکار متمرکز شوند، نه در فرمهای پراکنده. این الگو، در معماریهای Domain-Driven Design استاندارد است و در پروژههای وردپرسی بزرگ، اثر مستقیم روی صحت داده دارد. لایه سوم، پایش مستمر و بازبینی دورهای: پاکسازی، وضعیت نیست؛ فرآیند است. در پروژههای بلندمدت، هر سه ماه یک بازبینی انجام میدهم: اجرای PHP_CodeSniffer، بررسی کد افزونههای جدید، و پویش دیتابیس برای دادههای مشکوک. مسیر کامل این پایش در تست و دیباگ پروژههای وردپرس و پاکسازی دیتابیس وردپرس آمده است.
یک نکته تکمیلی برای تیمهای فنی: در پروژههای بزرگ، پاکسازی داده باید در سطح قواعد تیمی مستندسازی و در چرخه بازبینی کد اجرا شود. تجربه من این است که در تیمی که چکلیست این مقاله را در بازبینی کد اجرا میکند، در شش ماه، سطح امنیت محسوس بالا میرود. اگر میخواهید این بررسیها را بهصورت خودکار در CI اجرا کنید، مسیر کامل در CI/CD در وردپرس آمده است. تفاوت بین پروژهای که پاکسازی را بهعنوان عادت دارد و پروژهای که ندارد، در روز حادثه، ساعتها کار و اعتبار برند است — نه در ظاهر سایت.
جمعبندی
پاکسازی داده در وردپرس، در چهار لحظه خلاصه میشود: لحظه دریافت از کاربر، لحظه ذخیره در دیتابیس، لحظه خواندن از دیتابیس، و لحظه نمایش به کاربر. سه اصل در پایان تاکید میکنم: اول، پاکسازی را با تابع مناسب نوع داده انجام دهید، نه با یک تابع عمومی. دوم، escape خروجی را با تابع متناسب context انجام دهید؛ esc_url برای URL، esc_html برای متن. سوم، پاکسازی یک جریان است، نه یک نقطه؛ دفاع در عمق، تنها راه محافظت واقعی است.
اگر همین امروز در حال کار روی یک پروژه وردپرسی هستید، پیشنهاد عملی من سه گام است: ابتدا کد فعلی خود را با PHP_CodeSniffer بررسی کنید و خطاهای sanitize و escape را ببینید؛ سپس نقشه جریان داده پروژه خود را بکشید و هر گره را با چکلیست این مقاله بسنجید؛ و در پایان، در همه فرمها، متاباکسها و endpointهای API، پاکسازی بر اساس نوع داده را اضافه کنید. اگر در هر مرحلهای گیر کردید یا تجربهای از یک حادثه ناشی از نبود پاکسازی دارید، در دیدگاهها بنویسید. تجربه شما از یک پروژه با پاکسازی جدی یا یک حادثه امنیتی، برای توسعهدهنده بعدی که در همین نقطه ایستاده، ارزشمندترین راهنماست. 🧼