کوئری‌های امن پایگاه داده

حمله تزریق SQL (SQL injection) نوعی آسیب‌پذیری است که در آن مهاجم می‌تواند با وارد کردن محتوای تحت کنترل خود، یک کوئری SQL را دستکاری کند.

کد نمونه زیر را در نظر بگیرید:

 
$query = $this->db->getQuery(true);
$username = $user->username;
$newPassword = password_hash($newPassword, PASSWORD_DEFAULT);

$query->update('#__users')->set('password = "' . $newPassword . '"')->where('username = "' . $username . '"');

$this->db->setQuery($query)->execute();

این کد قرار است رمز عبور کاربری که با نام کاربری تعیین شده وارد شده را به‌روزرسانی کند. مثلاً برای کاربر "foobar" کوئری ایجاد شده چنین به نظر می‌رسد:

 
UPDATE jos_users SET password = "{HASH}" WHERE username = "foobar";

حال فرض کنید کاربر نام کاربری خود را به شکل زیر انتخاب کند:

`foobar" OR username="admin`

در این صورت کوئری خیلی متفاوت خواهد بود:

 
UPDATE jos_users SET password = "{HASH}" WHERE username = "foobar" OR username="admin";

که مهاجم با این ورودی توانسته دستورات دلخواه خود را وارد کوئری کرده و نه تنها رمز عبور خودش بلکه رمز عبور کاربر ادمین را نیز تغییر دهد.

راه پیشگیری 

استفاده از دستورهای آماده (Prepared Statements)

این بهترین و استانداردترین روش جلوگیری از حملات SQLi است.

اصل کار ساده است: به جای ادغام مقادیر ورودی کاربر در رشته کوئری داخل کد PHP، کوئری و داده‌ها به صورت جداگانه به سرور پایگاه داده ارسال می‌شوند.

مثال Prepared Statement:

Query: SELECT foobar FROM bar WHERE foo = ?

Data:  [? = 'bar']

سرور پایگاه داده کوئری و مقادیر را در کنار هم قرار می‌دهد و جایگزینی لازم و فراردهی کاراکترها را انجام می‌دهد. این باعث می‌شود تزریق کد غیرممکن شود.

پیاده‌سازی Prepared Statements در JDatabaseDriver جوملا

به راحتی و به صورت چند سکویی (cross-platform) انجام می‌شود:

 
$query = $this->db->getQuery(true)
    ->select($this->db->quoteName(['id', 'password']))
    ->from($this->db->quoteName('#__users'))
    ->where($this->db->quoteName('username') . ' = :username')
    ->bind(':username', $credentials['username']);

در کوئری یک جایگزین نام‌گذاری شده (named placeholder) با پیشوند : تعریف می‌کنید و مقدار واقعی آن را با متد `bind()` به سرور پایگاه داده می‌دهید.

توابع زیر نیز آرایه می‌پذیرند تا فراخوانی توابع بهینه شود:

- bind()

- bindArray()

- wherein()

- whereNotIn()

اگر ممکن است، حتماً از prepared statements استفاده کنید!

برای اطلاعات بیشتر:

- مستندات PHP درباره Prepared statements 

- یک درخواست_pull ساده که پیاده‌سازی Prepared Statement در جوملا را نشان می‌دهد

- مستندات API فریم‌ورک جوملا

فراردهی ورودی‌های تحت کنترل کاربر

با فراردهی کاراکترهایی که در کوئری SQL نقش کنترل را دارند، می‌توان جلوگیری کرد که مهاجم از محصور شدن در دابل کوتیشن‌های کوئری فرار کند:

 
$query = $this->db->getQuery(true);
$username = $user->username;
$newPassword = password_hash($newPassword, PASSWORD_DEFAULT);

$query->update('#__users')
      ->set('password = "' . $this->db->escape($newPassword) . '"')
      ->where('username = "' . $this->db->escape($username) . '"');

$this->db->setQuery($query)->execute();

علاوه بر این می‌توانید دابل کوتیشن‌های دستی اضافه شده را حذف کنید و به جای آن از متدهای `quote()` و `quoteName()` استفاده نمایید که به صورت پیش‌فرض ورودی را فراردهی می‌کنند.