خطر تزریق SQL با وجود placeholder ها در PDO

23 بهمن 1397
درسنامه درس 11 از سری آموزش PDO
PDO-placeholder-sqlinjection

در این قسمت به سراغ مبحثی میرویم که به SQL Injection مربوط است؛ این مبحث شامل نکات ریزی است که از دید اکثر برنامه نویسان دور میماند.

Prepared statements ها و اسامی جدول ها

یکی از SQL Injection هایی که بسیاری از برنامه نویسان به آن توجه نمی کنند به شرح زیر است:

چند وقت پیش کدی در stackoverflow دیدم که به شکل زیر بود:

$params = [];
$setStr = "";
foreach ($data as $key => $value)
{
    if ($key != "id")
    {
        $setStr .= $key." = :".$key.","; 
    }
    $params[':'.$key] = $value;

}
$setStr = rtrim($setStr, ",");
$pdo->prepare("UPDATE users SET $setStr WHERE id = :id")->execute($params);

در ظاهر این کد مشکلی ندارد؛ بسیار راحت و ساده است و از یک آرایه، کوئری ما را می سازد به طوری که کلید ها مساوی نام ستون ها و مقادیر مساوی مقادیری هستند که در کوئری جایگزین می شوند.

این کد یک مشکل وحشتناک دارد!

حتما می گویید مشکل کجاست؟ مگر از placeholder ها استفاده نشده است؟ بله، داده ی ما امن است اما این کد داده های کاربر را گرفته و مستقیما به کوئری اضافه می کند! بله متغیر key$ مستقیما و بدون هیچ عملیاتی وارد کوئری می شود. این مسئله یعنی یک Injection! به مثال زیر توجه کنید:

<form method=POST>
<input type=hidden name="name=(SELECT'hacked!')WHERE`id`=1#" value="">
<input type=hidden name="name" value="Joe">
<input type=hidden name="id" value="1">
<input type=submit>
</form>
<?php
if ($_POST) {
$pdo = new PDO('mysql:dbname=test;host=localhost', 'root', '');
    $params = [];
    $setStr = "";
    foreach ($_POST as $key => $value)
    {
        if ($key != "id")
        {
            $setStr .= $key." = :".$key.","; 
        }
        $params[$key] = $value; 

    }
    $setStr = rtrim($setStr, ",");
    $pdo->prepare("UPDATE users SET $setStr WHERE id = :id")->execute($params);
}

این کد، کوئری زیر را تولید می کند:

UPDATE users SET name=(SELECT'hacked!')WHERE`id`=1# = :name=(SELECT'1')WHERE`id`=1#,name = :name WHERE id = :id

در این حالت تمام مواردی که بعد از # قرار بگیرند، کامنت در نظر گرفته می شوند (در SQL کامنت ها با # مشخص می شوند). بنابراین مقدار name به جای "Joe" با کلمه ی "!hacked"  تنظیم می شود. خیلی ترسناک به نظر نمی آید، درست است؟ مسئله این جاست که هر نوع Injection یک آسیب پذیری به حساب می آید و این مثال تنها برای تفهیم شما بود.

در واقعیت روش های بسیار بیشتری وجود دارند که با آن ها می توان خراب کاری های بیشتر انجام داد. شاید در آینده در مقاله ای جداگانه آن ها را ذکر کنم اما الان باید برگردیم به بحث اصلی خودمان!

متاسفانه PDO هیچ palceholder ای برای identifier ها (اسامی جدول ها و فیلد ها) ندارد بنابراین توسعه دهندگان باید به صورت دستی آن ها را قالب بندی کنند.

برای قالب بندی identifier ها در MySQL باید پیروی دو قانون زیر باشید:

  • identifier ها را بین دو ` قرار دهید (نام این علامت backtick است).
  • backtick ها را با دوبرابر کردنشان، escape دهید.

این دو قانون منجر به کد زیر می شود:

$table = "`".str_replace("`","``",$table)."`";

قبل از ادامه ی مطلب باید این کد را در دو مرحله توضیح بدهم:

  • مرحله ی اول (فنی): تابع ()str_replace را باید از قبل یاد گرفته باشید اما توضیح کوتاهی در مورد آن می دهم. این تابع ساختار کلی زیر را دارد:

str_replace(find,replace,string,count)

به کد زیر توجه کنید:

<!DOCTYPE html>
<html>
<body>

<?php
echo str_replace("world","Peter","Hello world!");
?>

<p>In this example, we search for the string "Hello World!", find the value "world" and then replace the value with "Peter".</p>

</body>
</html>

کد زیر ابتدا رشته ی "!Hello world" را پیدا می کند و سپس رشته ی "world" را با رشته ی "Peter" عوض می کند. بنابراین خروجی کد می شود:

Hello Peter!

  • مرحله ی دوم (کوئری): دوباره به کوئری نگاه کنید:
$table = "`".str_replace("`","``",$table)."`";

این کد ابتدا در نام جدول علامت ` را پیدا کرده و سپس آن را دو برابر می کند (یعنی ``) سپس کل این رشته را داخل دو علامت ` می گذارد. بعد از انجام چنین قالب بندی، وارد کردن table$ در کوئری کاملا بی خطر است.

این قوانین برای پایگاه های داده ی دیگر متفاوت هستند اما نکته ای که شما باید در ذهن داشته باشید این است که جداکننده ها به تنهایی کافی نیستند و خود آن ها را نیز باید escape کرد.

همچنین همیشه سعی کنید identifier های پویا را با لیستی از مقادیر مجاز چک کنید:

$orders  = ["name","price","qty"]; //نام فیلد
$key     = array_search($_GET['sort'],$orders); // ای موجود است؟ name چک کنید که آیا چنین 
$orderby = $orders[$key]; //اگر وجود نداشت اولی به صورت خودکار انتخاب می شود
$query   = "SELECT * FROM `table` ORDER BY $orderby"; //مقدار ما امن است

همانطور که در خود کد به صورت کامنت توضیح داده ام این کد به جای اینکه کور کورانه هر مقداری را از کاربر دریافت کند، آن را با مقادیر از پیش تعیین شده می سنجد که ببیند آیا اصلا چنین مقداری مجاز است یا خیر.

همچنین همین روش را می توانیم برای دستورات INSERT و UPDATE نیز انجام دهیم (MySQL از دستور SET برای هر دوی این موارد پشتیبانی می کند):

$data = ['name' => 'foo', 'submit' => 'submit']; // data for insert
$allowed = ["name", "surname", "email"]; // allowed fields
$values = [];
$set = "";
foreach ($allowed as $field) {
    if (isset($data[$field])) {
        $set.="`".str_replace("`", "``", $field)."`". "=:$field, ";
        $values[$field] = $data[$field];
    }
}
$set = substr($set, 0, -2);

این کد تنها سری مناسب دستور SET را تولید میکند که فقط شامل فیلد ها و placeholder های مجاز است:

`name`=:foo

همچنین می توان آن را برای آرایه ی values$ در دستور ()execute استفاده کرد:

$stmt = $pdo->prepare("INSERT INTO users SET $set");
$stmt->execute($values);

ما هم قبول داریم که شکل کدها تمیز ترین شکل نخواهد شد اما شکل کدها در برابر اهمیت معنایی ندارد، مگر نه؟ در PDO راهی غیر از این نداریم.

خلاصه ی مقاله

در این قسمت متوجه شدیم استفاده از Placeholder ها به تنهایی برای امنیت پایگاه داده و وب سایت ما کافی نیست و نباید کورکورانه هر داده ای را از سمت کاربر قبول کنیم.همچنین راه حل های مناسب را برای فرار از این نوع خطر Injection ارائه دادیم.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری آموزش PDO توصیه می‌کند:
نویسنده شوید
دیدگاه‌های شما (4 دیدگاه)

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

سوران
15 آذر 1398
با سلام. حالا متوجه منظورتون از این مقاله هستم. پس پست قبلی من رو بیخیال شید. باز هم تشکر می کنم. مقالات دیگه ای هم در این مورد خوندم و جالب اینکه در ویدیو های یوتیوب کسی به این مطلب نپرداخته، هرچه هست نوشته است. اما دنبال کردن نوشته کار ساده ای نیست. مخصوصاً اگر در یک مثال ساده به کار نرود. پس بسیار خوب خواهد بود اگر یا یک ویدیو بسازید یا همین مقاله رو گسترش بدهید و ساده تر بیان بفرمایید. این کار شما خدمت بزرگی خواهد بود. لذا قدردانی بنده رو پیشاپیش پذیرا باشید.

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

امیر زوارمی
20 آذر 1398
سلام دوست عزیز خسته نباشید ممنونم از لطف شما... ان شاء الله اگه وقتش باشه در آینده سعی می کنم مطلب رو به صورت ویدیویی هم قرار بدم اما فعلا برام امکان نداره. به هر حال از لطف شما خیلی ممنونم

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

سوران
14 آذر 1398
سلام. مقاله ی شما را خواندم و برایم بسیار جالب بود. متشکرم از کار پخته اتان. یک سؤالی داشتم. آیا توابع درونی خود PHP جلوی چنین مواردی را نمی گیرند؟ توابعی مانند htmlentities htmlspecialchars. فرض بفرمایید ما از یک فرم چیزی را با متد POST دریافت کنیم؛ آنگاه اول این توابع را بر مقادیر دریافتی اعمال کنیم و سپس آنها را در متغییر هایی قرار دهیم. آیا این روش امنیت مورد نظر ما را تأمین می کنند؟ چون خودم از این روش استفاده می کنم می گم. می خواستم بدانم اگر این روش کاربردی نیست به چیزی که شما نگاشته اید سویچ کنم.

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

حسین
09 خرداد 1398
i$value=$_post['value'] i$safe="``".$value."``"i bindparam(:value , $safe) راحت تر نیست به نظر شما؟اینطوری هم میشه دو تا بک تیک اضافه کرد. البته من با تبلت پیام گذاشتم و بک تیک رو پیدا نکردم.منظورم از دو کوتیشن وارونه همون بک تیک هست.

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

امیر زوارمی
09 خرداد 1398
سلام دوست عزیز، روش های مختلفی برای اضافه کردن backtick وجود داره و شما میتونید از روشی استفاده کنید که باهاش راحت تر هستید اما باید حواستون باشه که توی سیستم خودتون و کد هایی که نوشتین، این روش باعث اخلال امنیتی نشه. روشی که ما گفتیم توسط یکی از برنامه نویسان با سابقه ی پایگاه داده و PHP ارائه شده و به خاطر تجربه ی ایشون ما کد ها رو دستکاری نکردیم اما قطعا به بی نهایت روش مختلف می تونیم بنویسیمش.

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

sajjadVa22
23 بهمن 1397
چرا متن اینطوریه؟ علامت backtick رو نمیبینم من

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

امیر زوارمی
24 بهمن 1397
سلام خدمت شما، علامت backtick علامت ` هست. روی کیبوردتون، بالا سمت چپ، بالای کلید tab

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.