إذا سبق لك أن رأيت مرورًا عبر سجلاتك سأعين إلى admin-ajax.php مع ملف باسم shell.php (أو ما هو أسوأ من ذلك، image.jpg.php)، لقد اقتربت بالفعل من السيناريو الكلاسيكي: التحميل "العملي" الذي يتحول إلى تنفيذ التعليمات البرمجية عن بعد.

التهديد

تسمح ثغرة أمنية في تحميل الملفات للمهاجم بتحميل ملف إلى خادمك. أسوأ سيناريو ليس "ملفًا غير مرغوب فيه"، بل ملف قابل للتنفيذ (أو قابل للتفسير) يفتح الباب أمام:

  • RCE (تنفيذ التعليمات البرمجية عن بُعد) : تنفيذ PHP إذا انتهى الأمر بالملف في دليل قابل للتنفيذ ويمكن الوصول إليه بشكل عام.
  • شبكة ويب مستمرة : يمكن للمهاجم العودة متى شاء، حتى بعد تغيير كلمة المرور.
  • تشويه المواقع، عمليات إعادة توجيه لتحسين محركات البحث : حقن صفحات البريد العشوائي، وإعادة التوجيه المشروط، والتخفي.
  • exfiltration سرقة wp-config.php، من قاعدة البيانات (عبر الوصول إلى قاعدة البيانات)، ومفاتيح واجهة برمجة التطبيقات، وصادرات WooCommerce.
  • الحركة الجانبية : الانتقال إلى مضيفات افتراضية أخرى على نفس الخادم المعزول بشكل سيئ.

التردد الفعلي مرتفع لأن عمليات التحميل موجودة في كل مكان: نماذج الاتصال، "أرسل صورتك الرمزية"، "أسقط ملف PDF"، استيراد ملفات CSV، استيراد القوالب، استيراد مكتبة Divi.Elementorإلخ. في تجربتي، نادراً ما تنشأ الحوادث الأكثر تكلفة في القلب. WordPress (يُعتبر ووردبريس 6.9.4 صارمًا نوعًا ما فيما يتعلق بتحميل الوسائط)، لكن الإضافات "المطورة محليًا" أو المقتطفات المنسوخة تتجاوز إجراءات الحماية الخاصة بـ wp_handle_upload().

يكمن الخطر ببساطة في اعتقادك أنك تقبل "صورة"، بينما أنت في الواقع تقبل "ملفًا" - وقد يكون الملف برنامجًا. يكمن الأمن في جعلهما متطابقين. ما تعتقد أنك ستحصل عليه مع ما سيقوم الخادم بتخزينه وتقديمه فعليًا.

ملخص سريع

  • لا تثق أبدا à $_FILES['name']، إلى التمديد، ولا إلى Content-Type تم إرسالها بواسطة المتصفح.
  • التحقق من صحة البيانات على جانب الخادم : نوع MIME الفعلي + الامتداد المتوقع + الحجم + الأبعاد (إذا كانت صورة) + المحتوى (إذا لزم الأمر).
  • استخدم واجهات برمجة تطبيقات ووردبريس : wp_handle_upload(), wp_check_filetype_and_ext(), media_handle_upload() والقدرات/الخصائص.
  • قم بالتخزين خارج نطاق Webroot إن أمكن، وإلا تنفيذ الكتلة في uploads (Apache/Nginx) وتشغيلها في وضع القراءة فقط.
  • مجلة التحميل (من، ماذا، عنوان IP، الحجم، النتيجة) ومراقبة الامتدادات المحظورة.
  • حماية نقاط النهاية (REST/AJAX): nonce، والقدرات، وتحديد المعدل، ومنع التحميلات للزوار المجهولين إلا عند الضرورة القصوى.

الكود المعرض للثغرات الأمنية - ما لا يجب فعله

إليكم مثالًا واقعيًا ما زلت أواجهه حتى عام 2026: نقطة نهاية AJAX "بسيطة" تأخذ ملفًا وتنقله إلى wp-content/uploadsإنه يعمل... حتى اليوم الذي يتوقف فيه عن العمل.

<?php
/**
 * Exemple VULNÉRABLE : ne copiez pas ce code.
 * Problèmes : pas de nonce, pas de capability, validation naïve par extension,
 * chemin prévisible, et move_uploaded_file() sans contrôle WordPress.
 */
add_action('wp_ajax_nopriv_bpcab_upload_avatar', 'bpcab_upload_avatar_vulnerable');
add_action('wp_ajax_bpcab_upload_avatar', 'bpcab_upload_avatar_vulnerable');

function bpcab_upload_avatar_vulnerable() {
	// Aucune vérification d'intention (nonce) : n'importe qui peut appeler l'endpoint
	// Aucune vérification de droits : même un visiteur non connecté

	if ( empty($_FILES['file']) ) {
		wp_send_json_error(['message' => 'Aucun fichier reçu'], 400);
	}

	$file = $_FILES['file'];

	// Validation dangereuse : se base sur l'extension déclarée
	$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
	if ( ! in_array($ext, ['jpg', 'jpeg', 'png', 'gif'], true) ) {
		wp_send_json_error(['message' => 'Extension non autorisée'], 400);
	}

	$uploads = wp_upload_dir();
	$dest = trailingslashit($uploads['basedir']) . 'avatars/' . $file['name'];

	wp_mkdir_p(dirname($dest));

	// move_uploaded_file() sans validation MIME réelle, ni contrôle de collisions
	if ( ! move_uploaded_file($file['tmp_name'], $dest) ) {
		wp_send_json_error(['message' => 'Échec de l’upload'], 500);
	}

	// Retourne une URL publique immédiatement
	$url = trailingslashit($uploads['baseurl']) . 'avatars/' . rawurlencode($file['name']);
	wp_send_json_success(['url' => $url]);
}

كيف يعمل الهجوم (بدون "تعليمات تشغيل")

تكمن المشكلة في الثقة الممنوحة للإضافة. يمكن للمهاجم أن:

  • أرسل ملفًا لا يحتوي على صورة (مثل ملف PHP) ولكن ينتهي اسمه بـ .jpg أو صيغة مشابهة لها.
  • اللعب على امتدادات مزدوجة (avatar.jpg.php) إذا تم التحقق من صحة البيانات بشكل غير صحيح (هنا، يتم أخذ الامتداد الأخير، لذلك php سيتم حظرها... لكن العديد من المقاطع البرمجية تأخذ الامتداد الأول أو تقوم بـ strpos).
  • استغلال خادم مُهيأ بشكل خاطئ يعمل بنظام PHP في uploads (لا يزال هذا يحدث في الاستضافة المشتركة أو الحزم "المخصصة").
  • استبدال الملفات الموجودة (تعارض) أو حذف الملفات ذات الأسماء الخاصة (.htaccess, index.php) إذا لم تكن تتحكم في الاسم النهائي.

وحتى لو لم يكن الخادم يعمل بنظام PHP uploadsيمكن للمهاجم إسقاط HTML/JS (إذا سمحت بذلك عن طريق الخطأ) ويهدف إلى سرقة الجلسات عبر XSS المخزنة التي يتم تقديمها من نطاقك.

الكود الآمن - التنفيذ الصحيح

الهدف: نفس الوظيفة (تحميل الصورة الرمزية)، ولكن مع سلسلة كاملة من إجراءات الحماية. سأعرض لكم إحدى الطرق...المساعد"جاهز" متوافق مع WordPress 6.9.4+ و PHP 8.1+، مع نقاط امتداد نظيفة.

بنية النظام: خدمة + نقطة نهاية REST (بدلاً من AJAX) + تحقق صارم

أميل إلى تفضيل واجهة برمجة تطبيقات REST لهذا النوع من الوظائف: فهي أسهل في الاختبار، وأكثر وضوحًا من حيث المصادقة، وتتيح ربط عناصر التحكم (الصلاحيات، القيم العشوائية) بسلاسة. وإذا اضطررتَ إلى استخدام AJAX (كما تفعل Divi/Avada أحيانًا مع الوحدات النمطية)، فإن المبادئ نفسها تنطبق.

1) خدمة تحميل الملفات (التحقق + التخزين)

<?php
/**
 * Plugin: BPCAB Secure Uploads
 * Cible: WordPress 6.9.4+, PHP 8.1+
 */

namespace BPCABSecurity;

use WP_Error;

final class Upload_Service {

	/**
	 * Types autorisés pour un avatar.
	 * Note : évitez SVG pour un avatar si vous n'avez pas une chaîne de sanitization dédiée.
	 */
	private array $allowed_mimes = [
		'jpg|jpeg|jpe' => 'image/jpeg',
		'png'          => 'image/png',
		'gif'          => 'image/gif',
		'webp'         => 'image/webp',
	];

	private int $max_bytes;

	public function __construct(int $max_bytes = 2_000_000) {
		$this->max_bytes = $max_bytes;
	}

	/**
	 * Upload sécurisé d'un avatar utilisateur.
	 *
	 * @param array $file Un élément de $_FILES (ex: $_FILES['file'])
	 * @param int   $user_id ID utilisateur cible
	 * @return array|WP_Error Résultat wp_handle_upload + métadonnées
	 */
	public function handle_avatar_upload(array $file, int $user_id) {
		if ( $user_id <= 0 ) {
			return new WP_Error('bpcab_invalid_user', 'Utilisateur invalide.');
		}

		// 1) Vérifications basiques PHP upload
		if ( empty($file['tmp_name']) || ! is_uploaded_file($file['tmp_name']) ) {
			return new WP_Error('bpcab_invalid_upload', 'Upload invalide (tmp_name manquant).');
		}

		if ( ! empty($file['error']) ) {
			return new WP_Error('bpcab_upload_error', 'Erreur PHP lors de l’upload : ' . (int) $file['error']);
		}

		// 2) Taille max
		$size = (int) ($file['size'] ?? 0);
		if ( $size <= 0 || $size > $this->max_bytes ) {
			return new WP_Error('bpcab_file_too_large', 'Fichier trop volumineux.');
		}

		// 3) Vérification MIME + extension par WordPress (basée sur le contenu + nom)
		// wp_check_filetype_and_ext() compare extension/mime déclarés vs détection.
		$check = wp_check_filetype_and_ext(
			$file['tmp_name'],
			$file['name'],
			$this->allowed_mimes
		);

		if ( empty($check['type']) || empty($check['ext']) ) {
			return new WP_Error('bpcab_disallowed_type', 'Type de fichier non autorisé.');
		}

		// 4) Vérification image réelle (dimensions + entête)
		$img = @getimagesize($file['tmp_name']);
		if ( $img === false ) {
			return new WP_Error('bpcab_not_an_image', 'Le fichier ne semble pas être une image valide.');
		}

		[$width, $height] = $img;
		if ( $width < 64 || $height < 64 || $width > 4000 || $height > 4000 ) {
			return new WP_Error('bpcab_bad_dimensions', 'Dimensions d’image non acceptées.');
		}

		// 5) Renommage : ne jamais réutiliser le nom fourni
		$final_name = sprintf('avatar-%d-%s.%s', $user_id, wp_generate_password(12, false, false), $check['ext']);
		$file['name'] = $final_name;

		// 6) Utiliser wp_handle_upload() (déplace, gère collisions, applique filtres)
		require_once ABSPATH . 'wp-admin/includes/file.php';

		$overrides = [
			'test_form' => false, // On gère l'auth nous-mêmes (REST/nonce), pas via $_POST['action']
			'mimes'     => $this->allowed_mimes,
		];

		add_filter('upload_dir', [$this, 'filter_avatar_upload_dir']);
		$result = wp_handle_upload($file, $overrides);
		remove_filter('upload_dir', [$this, 'filter_avatar_upload_dir']);

		if ( isset($result['error']) ) {
			return new WP_Error('bpcab_upload_failed', $result['error']);
		}

		// 7) Belt & suspenders : permissions fichier (dépend de l'hébergement)
		@chmod($result['file'], 0644);

		// 8) Enregistrer la référence (ex: user meta)
		update_user_meta($user_id, 'bpcab_avatar_url', esc_url_raw($result['url']));
		update_user_meta($user_id, 'bpcab_avatar_path', sanitize_text_field($result['file']));

		return $result;
	}

	/**
	 * Isoler les avatars dans un sous-dossier dédié.
	 * Avantage : règles serveur plus strictes sur /uploads/avatars/.
	 */
	public function filter_avatar_upload_dir(array $dirs): array {
		$subdir = '/avatars';
		$dirs['subdir'] = $dirs['subdir'] . $subdir;
		$dirs['path']   = $dirs['path'] . $subdir;
		$dirs['url']    = $dirs['url'] . $subdir;
		return $dirs;
	}
}

2) نقطة نهاية REST مع nonce + إمكانية

بالنسبة للمستخدم المسجل دخوله، يمكنك استخدام رمز REST (الرأس) X-WP-Nonce). فيما يتعلق بالأذونات، أطبق قدرة صارمة: يجب أن يكون المستخدم قادراً على تعديل ملفه الشخصي (أو قدرة مخصصة).

<?php
namespace BPCABSecurity;

use WP_REST_Request;
use WP_REST_Response;
use WP_Error;

final class Upload_Controller {

	public function __construct(
		private Upload_Service $service
	) {}

	public function register_hooks(): void {
		add_action('rest_api_init', function () {
			register_rest_route('bpcab/v1', '/avatar', [
				'methods'             => 'POST',
				'callback'            => [$this, 'handle'],
				'permission_callback' => [$this, 'permissions_check'],
				'args'                => [
					'user_id' => [
						'type'              => 'integer',
						'required'          => false,
						'sanitize_callback' => 'absint',
					],
				],
			]);
		});
	}

	public function permissions_check(WP_REST_Request $request): bool|WP_Error {
		if ( ! is_user_logged_in() ) {
			return new WP_Error('bpcab_auth_required', 'Authentification requise.', ['status' => 401]);
		}

		$user_id = (int) ($request->get_param('user_id') ?: get_current_user_id());

		// Autoriser uniquement l'utilisateur lui-même, ou un admin qui peut éditer l'utilisateur.
		if ( $user_id !== get_current_user_id() && ! current_user_can('edit_user', $user_id) ) {
			return new WP_Error('bpcab_forbidden', 'Accès refusé.', ['status' => 403]);
		}

		// Protection CSRF : le nonce REST est géré par WP via X-WP-Nonce.
		// Si vous appelez depuis un front custom, utilisez wp_create_nonce('wp_rest').
		return true;
	}

	public function handle(WP_REST_Request $request): WP_REST_Response {
		$user_id = (int) ($request->get_param('user_id') ?: get_current_user_id());

		$files = $request->get_file_params();
		if ( empty($files['file']) ) {
			return new WP_REST_Response(['message' => 'Champ fichier manquant.'], 400);
		}

		$result = $this->service->handle_avatar_upload($files['file'], $user_id);

		if ( is_wp_error($result) ) {
			return new WP_REST_Response([
				'code'    => $result->get_error_code(),
				'message' => $result->get_error_message(),
			], 400);
		}

		return new WP_REST_Response([
			'url'  => esc_url_raw($result['url']),
			'type' => sanitize_text_field($result['type']),
		], 200);
	}
}

3) تنظيف Bootstrap باستخدام حاوية صغيرة (DI)

في المواقع الإلكترونية المتقدمة، أرى غالبًا رمز التحميل مُلصقًا في functions.phpيعمل النظام بشكل جيد، ثم يقوم أحدهم بتغيير القالب، فتفقد الحماية. ضعه في إضافة، وقم بتضمين التبعيات الخاصة بك.

<?php
/**
 * Fichier principal du plugin (ex: bpcab-secure-uploads.php)
 */

use BPCABSecurityUpload_Service;
use BPCABSecurityUpload_Controller;

if ( ! defined('ABSPATH') ) {
	exit;
}

require_once __DIR__ . '/src/Upload_Service.php';
require_once __DIR__ . '/src/Upload_Controller.php';

add_action('plugins_loaded', function () {
	$service    = new Upload_Service(max_bytes: 2_000_000);
	$controller = new Upload_Controller($service);
	$controller->register_hooks();
});

لماذا يُعد هذا التطبيق سليماً (بسيط ثم تقني)

بلغة بسيطة : أنت تحدد من يمكنه التحميل، وتتحقق من أن الملف هو صورة بالفعل، وتعطيه اسمًا آمنًا، وتخزنه في موقع خاضع للتحكم، وتمنع "المفاجآت" (الأنواع الغريبة، والأحجام الضخمة، والتصادمات).

فنيا :

  • is_uploaded_file() + $_FILES['error'] الحماية من المدخلات التي لا تنشأ من عملية تحميل PHP.
  • wp_check_filetype_and_ext() هذه هي النقطة المحورية: يحاول ووردبريس مطابقة الامتداد/MIME ويمكنه تصحيح الامتداد إذا لزم الأمر. المرجع: developer.wordpress.org — wp_check_filetype_and_ext().
  • getimagesize() يمنع هذا العديد من ملفات "image/*" المزيفة (مع أنه ليس دليلاً قاطعاً على التشفير). وللحصول على حماية إضافية، يمكنك إعادة إنشاء الصورة باستخدام GD/Imagick (إعادة ترميزها) والتخلص من النسخة الأصلية.
  • wp_handle_upload() يطبق منطق ووردبريس (مجلد التحميل، التعارضات، الخطافات). المرجع: developer.wordpress.org — wp_handle_upload().
  • تؤدي إعادة التسمية إلى إيقاف الهجمات القائمة على الأسماء (الاختراق، والتصادمات، وتصنيف الملفات). .htaccess، وما إلى ذلك).

في حالة "PDF / ZIP / CSV": لا تقم بنسخ استراتيجية الصورة

بالنسبة للوثائق، getimagesize() لا جدوى من ذلك. يجب أن يصبح التحقق من صحة البيانات "مراعيًا للتنسيق":

  • PDF : تحقق من نوع MIME الحقيقي والحجم، ومن الأفضل تحليل جانب الخادم (لكن احذر من المحللات الضعيفة).
  • CSV : تعامل معها كنص، وارفض الترميزات غير المتوقعة، واحمها من حقن CSV (صيغ Excel) إذا قمت بإعادة التصدير.
  • ZIP خطر كبير (قنابل مضغوطة، اختراق عملية الاستخراج). تجنب قبول ملفات ZIP إذا لم تكن لديك طريقة استخراج آمنة.

التوافق مع Divi 5 / Elementor / Avada (نقاط ملموسة)

غالباً ما تُفعّل أدوات إنشاء الصفحات عمليات التحميل عبر طلبات AJAX/REST داخلية. هناك مأزقان:

  • الراهبات يستخدم كل من Elementor و Divi قيمًا عشوائية خاصة بهما لبعض الإجراءات، ولكن إذا قمت بعرض نقطة نهاية REST الخاصة بك، فاستخدم القيمة العشوائية القياسية لـ REST (wp_rest) للحفاظ على التوافق.
  • مخبأ يمكن لـ Avada/Divi تخزين النصوص البرمجية مؤقتًا. إذا أرسل واجهة المستخدم الخاصة بك رمزًا عشوائيًا منتهي الصلاحية، فستتلقى أخطاء 401 متقطعة. غالبًا ما أحل هذه المشكلة عن طريق فرض تحديث الرمز العشوائي عبر طلب REST خفيف.

إذا كنت بحاجة إلى توفير رمز مختصر لتضمين نموذج تحميل في وحدة Divi/Elementor، فافعل ذلك، ولكن اترك عملية التحميل الفعلية لنقطة نهاية REST الآمنة. لا تقم بإعادة تنفيذ التحقق من الصحة في JavaScript.

تكوين الخادم

لا يكفي رمز التطبيق وحده. القاعدة التي أتبعها هي: حتى لو وصل ملف PHP في قسم التحميلات، فلا ينبغي أن يكون قادراً على التنفيذ..

أباتشي (.htaccess): حظر تنفيذ PHP في عمليات التحميل

يتم وضعها في wp-content/uploads/.htaccess (أو الأفضل من ذلك، في wp-content/uploads/avatars/.htaccess (إذا قمت بعزله).

# Bloquer l'exécution de scripts dans uploads (Apache)
# À déposer dans wp-content/uploads/.htaccess

<IfModule mod_php.c>
  php_flag engine off
</IfModule>

# Empêcher l'exécution via handlers (selon config)
RemoveHandler .php .phtml .php3 .php4 .php5 .php7 .phar
RemoveType .php .phtml .php3 .php4 .php5 .php7 .phar

<FilesMatch ".(php|phtml|phar|cgi|pl|asp|aspx)$">
  Require all denied
</FilesMatch>

# Optionnel : empêcher l'upload de .htaccess via un mauvais code
<FilesMatch "^.ht">
  Require all denied
</FilesMatch>

يرجى الملاحظة: يعتمد ذلك على نوع الإقامة، php_flag قد يكون ذلك محظورًا. في هذه الحالة، الجزء FilesMatch لا يزال مفيدًا. اختبره دائمًا بعد التثبيت.

Nginx: موقع التحميلات (مثال)

قم بتكييف هذا مع مضيفك الافتراضي. الفكرة: منع تنفيذ جميع البرامج النصية في /wp-content/uploads/.

# Nginx : bloquer l'exécution de scripts dans uploads
location ^~ /wp-content/uploads/ {
  location ~* .(php|phtml|phar|cgi|pl|asp|aspx)$ {
    return 403;
  }
  add_header X-Content-Type-Options "nosniff" always;
}

ملف wp-config.php: تحسينات مفيدة للأمان (دون تعطيل لوحة التحكم الإدارية)

لا تعمل هذه الإعدادات على "إصلاح" عملية التحميل، ولكنها تقلل من تأثيرها.

<?php
// Empêche l'édition de fichiers depuis l'admin (réduit l'impact si compte compromis)
define('DISALLOW_FILE_EDIT', true);

// Optionnel : bloque l'installation/maj de plugins/thèmes depuis l'admin (workflow via CI/CD)
define('DISALLOW_FILE_MODS', false); // Mettez true seulement si vous maîtrisez votre pipeline

// Forcer SSL admin si disponible
define('FORCE_SSL_ADMIN', true);

رؤوس HTTP: الحد من الضرر من جانب المتصفح

فيما يتعلق بالملفات التي يتم تقديمها من نطاقك (بما في ذلك الملفات المرفوعة)، nosniff يحد من بعض الحلول البديلة لـ MIME. كما يجب إضافة سياسة أمان محتوى (CSP) متسقة (على الأقل في لوحة التحكم والصفحات الحساسة).

# Apache (extrait vhost ou .htaccess si autorisé)
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"

تحقق مما إذا كان موقعك عرضة للاختراق

إشارات في قاعدة البيانات: مرفقات مشبوهة

إذا كنت تستخدم مكتبة الوسائط، فغالبًا ما ينتهي الأمر بالتحميلات في attachmentابحث عن الامتدادات المحظورة والأسماء غير المألوفة.

# Liste des pièces jointes avec GUID suspect (WP-CLI)
wp db query "
SELECT ID, post_date, post_title, guid
FROM wp_posts
WHERE post_type='attachment'
AND (
  guid REGEXP '\\.(php|phtml|phar|js|html)(\\?|$)'
  OR post_title REGEXP '\\.(php|phtml|phar)$'
)
ORDER BY post_date DESC
LIMIT 200;
"

القيد: يمكن لعملية تحميل بيانات ضعيفة أن تُودع الملفات دون المرور عبر مكتبة الوسائط. في هذه الحالة، تكون قاعدة البيانات سليمة... لكن القرص ليس كذلك.

فحص مُستهدف لمجلد التحميلات (بدون "أداة هجوم")

أُفضّل إجراء فحص بسيط باستخدام الامتدادات، يليه فحص يدوي. على الخادم، ابحث عن الملفات القابلة للتنفيذ في uploads :

# Rechercher des fichiers potentiellement exécutables dans uploads
find wp-content/uploads -type f ( -iname "*.php" -o -iname "*.phtml" -o -iname "*.phar" -o -iname "*.cgi" ) -print

# Rechercher des doubles extensions fréquentes
find wp-content/uploads -type f -regextype posix-extended -regex ".*.(jpg|png|gif|webp).(php|phtml|phar)$" -print

السجلات: ما الذي يجب البحث عنه

  • سأعين إلى admin-ajax.php, wp-json/ أو نقاط نهاية المكونات الإضافية، ذات أحجام طلبات كبيرة.
  • 200 إجابة ثم يتبع ذلك للحصول على مباشرةً إلى ملف تم تحميله حديثًا (نمط "التحميل ثم التنفيذ/القراءة").
  • الرموز 403/404 في /wp-content/uploads/*.php ومن المفارقات، أن منع التنفيذ علامة جيدة، ولكنه أيضاً مؤشر على وجود هجوم.

مخطط تشخيصي (أعراض واقعية)

عرض سبب على الأرجح التحقق الحلول
ملفات .php موجود في wp-content/uploads تحميل غير مُصفّى عبر إضافة/مقتطف برمجي، أو موقع مخترق find + سجلات الوصول حول تاريخ الإنشاء حظر التنفيذ (أباتشي/إنجينكس)، حذف الملفات، إصلاح كود التحميل، تدوير البيانات السرية
الصور "معطلة" بعد إضافة التحقق تم اكتشاف نوع MIME مختلف (مثل HEIC أو AVIF) أو أن الخادم الوكيل/شبكة توصيل المحتوى (CDN) يقوم بتغيير الرؤوس فحص wp_check_filetype_and_ext() والنوع المُعاد قم بالسماح صراحةً بالتنسيقات الضرورية (مع توخي الحذر) أو قم بتحويلها من جانب الخادم.
أخطاء متقطعة من نوع 401/403 عند تحميل البيانات من نقطة النهاية انتهت صلاحية رمز التحقق (Nonce)، أو ذاكرة تخزين مؤقتة قوية (Divi/Avada)، أو خاصية رد الاتصال الخاصة بالأذونات (permission_callback) صارمة للغاية تحقق من رأس الصفحة X-WP-Nonceساعة الخادم، ذاكرة التخزين المؤقت قم بتحديث قيمة nonce الخاصة بالواجهة الأمامية، واستبعد نقطة النهاية من ذاكرة التخزين المؤقت، وقم بتعديل الأذونات.
خطأ: "عذراً، لا يُسمح لك بتحميل هذا النوع من الملفات." نوع MIME غير مسموح به / الامتداد غير مُعرّف قارن بين النوع الفعلي والقائمة mimes إضافة MIME ضرورية للغاية عبر upload_mimes (إذا كان ذلك مبرراً)
تم التحميل بنجاح ولكن الملف غير قابل للوصول (403) قواعد الخادم صارمة للغاية فيما يتعلق بالمجلدات الفرعية اختبر الوصول المباشر، وتحقق من قواعد Nginx/Apache اضبط القواعد للسماح بتشغيل الصور مع حظر البرامج النصية

أخطاء شائعة في مجال الأمن

خطأ صعب الحلول
تحقق من صحة الامتداد فقط (.jpg) تجاوز عبر محتوى غير صوري، وامتداد مزدوج، ولغات متعددة استعمال wp_check_filetype_and_ext() + عناصر التحكم في التنسيق (على سبيل المثال، getimagesize())
أن تثق $_FILES['type'] MIME الذي يمكن تزويره من جانب العميل الكشف عن جانب الخادم (WP/PHP) وتقييد القائمة mimes
تجاهل nonce/capabilities في AJAX/REST تحميل مجهول الهوية / CSRF REST Nonce (X-WP-Nonce) + permission_callback صارم
نسخ المقتطف إلى المكان الخطأ (السمة بدلاً من الإضافة) فقدان الحماية عند تغيير السمة ضع المنطق في إضافة ذات إصدارات متعددة.
استخدام خطاف غير مناسب (على سبيل المثال، تنفيذ) wp_handle_upload قبل plugins_loaded) لم يتم تحميل الوظائف، سلوك غير متناسق شواحن wp-admin/includes/file.php استخدم في الوقت المناسب rest_api_init
الاختبار على بيئة الإنتاج بدون نسخ احتياطية تم حظر التحميلات المشروعة، وفقد المحتوى. الاختبار في وضع التجهيز، النسخ الاحتياطي للملفات وقاعدة البيانات، خطة التراجع
نسيان فاصلة منقوطة/قوس في إضافة مقتطفات خطأ فادح، الموقع معطل قم بالنشر عبر Git + CI، أو على الأقل قم بتمكين جزء من التعليمات البرمجية في بيئة الاختبار أولاً.
تعارض في ذاكرة التخزين المؤقت / نسيان مسح ذاكرة التخزين المؤقت للمتصفح/شبكة توصيل المحتوى (CDN) قيمة nonce منتهية الصلاحية، جافا سكريبت قديمة ترسل رأسًا غير صالح تجاوز ذاكرة التخزين المؤقت على نقاط النهاية، ومسح شبكة توصيل المحتوى (CDN)، وإصدار الأصول
لم يتم إعادة إنشاء الروابط الدائمة بعد إضافة المسارات خطأ 404 على نقاط النهاية متحقق wp-jsonأعد حفظ الروابط الدائمة إذا لزم الأمر
السماح باستخدام SVG "لأنه مناسب" تم تخزين XSS عبر SVG (script, foreignObject) تجنب استخدام ملفات SVG أو استخدم أدوات تنظيف قوية، بالإضافة إلى تقديمها مع رؤوس صارمة.
الكود المأخوذ من درس تعليمي قديم غير متوافق مع ووردبريس 6.9+ / PHP 8.1 عناصر تحكم مفقودة، تحذيرات، سلوك غير متوقع مراجعة: الراحة + wp_check_filetype_and_ext + أنواع صارمة + اختبار

قائمة التحقق من التصلب

  • النهاية جميع نقاط التحميل النهائية تحتوي على قيمة عشوائية (nonce) بالإضافة إلى فحص القدرة.
  • التحقق : أنت تستخدم wp_check_filetype_and_ext() مع قائمة mimes الحد الأدنى.
  • إعادة التسمية : أنت تتجاهل الاسم الأصلي وتولد اسمًا لا يمكن التنبؤ به.
  • حجم : الحد الأقصى بالبايتات على جانب الكود + حدود الخادم المتسقة (PHP/NGINX/Apache).
  • صور منشأة بالذكاء الاصطناعي : خيار التحكم في الأبعاد + إعادة الترميز (إعادة التوليد) إذا كان الخطر مرتفعًا.
  • تخزين : مجلد فرعي مخصص (/uploads/avatars) مع قواعد خادم محددة.
  • إعدام : يُحظر تنفيذ البرامج النصية في uploads (أباتشي/نجينكس).
  • أذونات : الملفات 0644، المجلدات 0755 (يتم تعديلها)، رقم 0777.
  • تسجيل : سجلات التطبيق (معرف المستخدم، عنوان IP، الحجم، النوع، النتيجة)، تنبيهات بشأن الامتدادات المحظورة.
  • مراجعة الإضافة : يتم تدقيق أي مكون إضافي يقوم "بالتحميل" (النماذج، والشرائح، والمستوردين، والمنشئين).
  • النسخ الاحتياطي : النسخ الاحتياطي اليومي للملفات وقاعدة البيانات، واختبار الاستعادة.
  • جدار حماية تطبيقات الويب/شبكة توصيل المحتوى : قواعد تحميل البيانات من نقاط النهاية (تحديد المعدل، حظر الأنماط الواضحة).

ماذا لو كان الموقع مخترقاً بالفعل؟

  1. ضع الموقع في وضع الصيانة (أو على الأقل حظر المسؤول) لمنع إعادة الإصابة أثناء التحليل.
  2. عزل قم بتعطيل الوصول المشترك إلى بروتوكول نقل الملفات (FTP)، وقم بتغيير كلمات مرور الاستضافة، وتأكد من عدم وجود مواقع أخرى متأثرة على نفس الحساب.
  3. يحفظ للتحليل قم بعمل نسخة كاملة (الملفات + قاعدة البيانات) قبل الحذف. ستحتاج إليها لفهم المتجه.
  4. حدد نقطة الدخول :
    • سجلات الوصول المتعلقة بتاريخ إنشاء الملفات المشبوهة،
    • الإضافات المضافة/المحدثة مؤخراً،
    • نقاط نهاية التحميل المخصصة.
  5. قم بإزالة الملفات الضارة :
    • احذف جميع الملفات التنفيذية في uploads,
    • تحقق wp-content/mu-plugins, wp-content/plugins, wp-content/themes للأبواب الخلفية،
    • ابحث عن الملفات التي تم تعديلها مؤخراً.
  6. أعد تثبيت نظام ووردبريس الأساسي (ملفات) من مصدر نظيف (بدون تعديل) wp-config.phpثم أعد تثبيت الإضافات/القوالب من ملفات ZIP الرسمية.
  7. دورة كاملة للأسرار :
    • كلمات مرور ووردبريس (كلمات مرور المسؤولين أولاً)،
    • كلمات مرور قواعد البيانات، وبروتوكول نقل الملفات/بروتوكول SSH،
    • المفاتيح والأملاح في wp-config.php (عن طريق خدمة المفتاح السري لموقع WordPress.org),
    • رموز واجهة برمجة التطبيقات (SMTP، CDN، المدفوعات).
  8. تحقق من القاعدة :
    • مشرفون مجهولون،
    • الخيارات المُضافة (على سبيل المثال، siteurl, homeوارد في wp_options),
    • محتوى غير مرغوب فيه في المنشورات/الصفحات.
  9. خادم هاردن قم بتطبيق قواعد "التحميلات غير القابلة للتنفيذ" وأضف الرؤوس. nosniff.
  10. تمت إعادة تشغيلها تدريجياً مراقبة السجلات، وتفعيل مراقبة سلامة الملفات (التجزئة)، ومتابعة الملفات الجديدة التي يتم إنشاؤها في uploads.

نصائح الصيانة والتوافق

في ووردبريس 6.9.4، واجهات برمجة تطبيقات التحميل وواجهة برمجة تطبيقات REST مستقرة. يكمن الخطر بشكل أساسي في طبقاتك: الإضافات، والقوالب البرمجية، وتكاملات أدوات البناء.

تجنب "قوائم السماح" الواسعة للغاية

كثيراً ما رأيت مواقع تضيف upload_mimes السماح باستخدام "أي شيء وكل شيء" (SVG، HTML، JS) لمجرد أن المطور أو العميل "يحتاج إليه". هذا يُعدّ ثغرة أمنية فورية. إذا كان لا بد من السماح بنوع معين:

  • افعلها uniquement على نقطة نهاية موثقة،
  • قم بتخزينها في مجلد فرعي مخصص،
  • قم بالتقديم باستخدام رؤوس صارمة (ويفضل أن يكون ذلك عبر نطاق ثابت منفصل).

الأداء/تحسين محركات البحث: يجب ألا يؤدي تقديم الملفات المرفوعة عبر شبكة توصيل المحتوى (CDN) إلى الإخلال بالأمان.

يمكن لشبكة توصيل المحتوى (CDN) تعديل رؤوس الرسائل أو إجراء "استشعار المحتوى". احتفظ بها X-Content-Type-Options: nosniff على جانب المنشأ، وتحقق من اتساق Content-Type تم تقديمه. سيء Content-Type يمكن أن يحول خطأ التحميل إلى هجوم XSS.

الحالات الاستثنائية: ظروف السباق والاصطدامات

إذا قمت بإنشاء أسماء يمكن التنبؤ بها (على سبيل المثال، avatar-123.jpgقد يؤدي تحميل ملفين في وقت واحد إلى تداخل بينهما. استخدم لاحقة عشوائية (كما في المثال)، أو خزّن الملفات حسب التاريخ/معرّف UUID. wp_handle_upload() يتعامل مع بعض حالات التصادم، لكنني أفضل عدم الاعتماد على اسم ثابت.

توافق أداة البناء: حيث يحدث الخلل الحقيقي

  • ديفي 5 إذا قمت بتغليف عملية التحميل في وحدة نمطية، فلا تضع منطق PHP في عرض الوحدة النمطية. احتفظ بنقطة نهاية REST واستدعها باستخدام JavaScript.
  • Elementor احذر من التعارضات مع إضافات الأمان التي تحظر wp-jsonاختبر عملية التحميل في وضع الاتصال وفي وضع التحرير.
  • فتح تقوم بعض الإعدادات بتقليص حجم الملفات ودمجها بشكل مكثف. لذا، قم بإصدار نسخ من البرامج النصية الخاصة بك واستبعد نقاط نهاية التحميل من ذاكرة التخزين المؤقت.

الموارد

الأسئلة الشائعة

هل wp_handle_upload() هل هذا كافٍ بحد ذاته؟

لا. wp_handle_upload() إنها لبنة، وليست سياسة. أنت بحاجة إلى إطار. هنا يمكن التحميل، ما (قائمة MIME) حيث (ملف)، و التعليق (إعادة التسمية، الحدود، السجلات).

لماذا لا تستخدم فقط media_handle_upload() ?

media_handle_upload() هذا ممتاز إذا كنت ترغب في إنشاء مرفق وتوليد بيانات وصفية. بالنسبة للصورة الرمزية، ليس من الضروري إثقال مكتبة الوسائط. إذا كنت ترغب في استخدام مكتبة الوسائط، فاستخدمها، ولكن احتفظ بمعلومات التحقق (أنواع MIME، الحجم، الأذونات) في البداية. مرجع: developer.wordpress.org — media_handle_upload().

هل يمكنني السماح باستخدام ملفات SVG إذا قمت بـ"تنظيفها"؟

من الناحية التقنية، نعم، لكن عمليًا، يُعدّ مصدرًا للحوادث. إنّ تنظيف ملفات SVG بشكل موثوق ليس بالأمر الهيّن (مثل ثغرات XSS، والروابط الخارجية، والبرامج النصية، foreignObjectإذا كان لا بدّ من فعل ذلك، فقم بتقديم ملفات SVG مع رؤوس صارمة، وفكّر في استخدام نطاق ثابت منفصل. أما بالنسبة للصور الرمزية، فلا أنصح بذلك.

لماذا إعادة تسمية الملف إذا كان ووردبريس يعرف بالفعل كيفية التعامل مع التعارضات؟

تؤدي إعادة التسمية إلى إيقاف الهجمات القائمة على الأسماء وتقليل التعارضات المنطقية (مثل الكتابة فوق صورة شخصية لشخص آخر). حتى لو قام ووردبريس بإعادة التسمية في -1، عليك الاحتفاظ باسم متحكم فيه وغير متوقع.

ما مدى جودة اكتشاف أنواع MIME بواسطة PHP؟fileinfo) مقارنةً بـ WordPress؟

يعتمد ووردبريس على آليات PHP وقواعدها الداخلية. إذا كانت لديك حالة حساسة (مستندات)، يمكنك إضافة طبقة حماية. finfo_file() وقارنها بقائمة المسموح به لديك. المرجع: php.net — finfo_open().

نقطة نهاية REST الخاصة بي تُرجع 401 على الرغم من أنني متصل: لماذا؟

في 80% من الحالات: يكون السبب هو فقدان أو انتهاء صلاحية رمز التحقق (nonce)، أو أن ذاكرة التخزين المؤقت تعرض صفحةً برمز تحقق قديم. تحقق من ترويسة الصفحة. X-WP-Nonce واستبعاد نقطة النهاية من ذاكرة التخزين المؤقت (المكون الإضافي، الخادم، شبكة توصيل المحتوى).

هل من الجيد تخزين الملفات المرفوعة خارج مجلد webroot؟

نعم، إذا كانت بنيتك التحتية تسمح بذلك. إنها إحدى أفضل الإجراءات: حتى لو تم تحميل ملف خطير، فلن يكون الوصول إليه مباشرًا. ستحتاج حينها إلى إعداد برنامج نصي "تنزيل" يتحقق من الأذونات ويبث الملف (مع readfile / تيارات) — ويجب القيام بذلك بشكل صحيح.

ماذا لو كانت إضافة خارجية تتولى عملية التحميل ولم أتمكن من تعديل الكود؟

ابدأ بطبقة الخادم: قم بحظر التنفيذ في uploadsبعد ذلك، قلل من مساحة الهجوم: عطّل عمليات التحميل المجهولة، وحدّد أنواع التحميل عبر إعدادات الإضافة، وفعّل جدار حماية تطبيقات الويب/حدًّا لمعدل التحميل على نقطة النهاية المتأثرة. إذا كانت الإضافة قيد الصيانة، فافتح تذكرة دعم مع دليل عملي على وجود ثغرة أمنية (وصف + سجلات) بدلاً من عرض تفاصيل الاستغلال.

Pourquoi test_form => false في wp_handle_upload ?

لأنه في سياق REST، ليس بالضرورة أن يكون لديك الحقل action أو بنية "النموذج" المتوقعة. يمكنك التعويض عن ذلك بإجراء فحوصات المصادقة (الرقم العشوائي/القدرة) والتحقق الصارم بنفسك.

لقد نسخت الكود وأواجه خطأً فادحاً: "لم يتم العثور على الفئة".

خطأ كلاسيكي: ملف مُضمّن بشكل غير صحيح، أو مساحة اسم غير صحيحة، أو كود مُلصق في functions.php بدون تحميل تلقائي. ضع الكود في إضافة، وتحقق من require_onceوتأكد من تطابق مساحة الاسم مع المسار. اختبر في بيئة تجريبية، وليس في بيئة الإنتاج.

هل يُحدث إصدار ووردبريس 6.9.4 أي تغييرات في عمليات التحميل مقارنةً بالإصدارات الأقدم؟

لقد شدد النظام الأساسي تدريجيًا عملية التحقق من صحة MIME/الامتداد عبر الإصدارات، لكن النموذج يبقى كما هو: إذا تجاوزت واجهات برمجة التطبيقات (على سبيل المثال، move_uploaded_file() (مباشرةً)، تفقد الحماية. الأمر "الجديد" عمليًا في عام 2026 هو بشكل أساسي الاستخدام الأكثر منهجية لـ REST ومجموعات التطبيقات غير الرأسية - وبالتالي المزيد من نقاط النهاية المخصصة للتدقيق.