اجرای کوئری ها و SQL Injection (قسمت دوم)

09 بهمن 1397
درسنامه درس 5 از سری آموزش PDO
PDO-sql-injection-2

با سلام، در قسمت قبلی با بدیهیات و مقدمات اجرای کوئری ها و مبحث SQL Injection آشنا شدیم. در این قسمت به ذکر مثال ها و توضیحات نکات ریزتر این مبحث خواهیم پرداخت.

Prepared statement ها و اجرای کوئری

حتما از جلسه ی قبل یادتان است که این کوئری را:

$sql = "SELECT * FROM users WHERE email = '$email' AND status='$status'";

تبدیل به یکی از کوئری های زیر کردیم:

$sql = 'SELECT * FROM users WHERE email = ? AND status=?';
$sql = 'SELECT * FROM users WHERE email = :email AND status=:status';

حالا ادامه ی مطلب:

برای اجرای یک کوئری که در آن از placeholder ها استفاده شده است باید از متد ()PDO::prepare استفاده شود ( به این کار prepare کردن یا آماده سازی می گوییم). این تابع، همان شیء PDOStatement را که در جلسات قبل به آن اشاره کردیم را بر میگرداند اما هیچ داده ای را به آن الصاق نمی کند.

در آخر نیز برای اجرای کوئری باید از متد ()execute استفاده کرده و مقادیر را به صورت یک آرایه به آن پاس بدهید. سپس می توانید داده های خود را از statement بگیرید (البته در صورتی که کوئری برای گرفتن داده باشد):

$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ? AND status=?');
$stmt->execute([$email, $status]);
$user = $stmt->fetch();
// یا
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND status=:status');
$stmt->execute(['email' => $email, 'status' => $status]);
$user = $stmt->fetch();

همانطور که می بینید برای استفاده از placeholder های موقعیتی (ر.ک جلسه ی قبل) از یک آرایه ی عادی استفاده می کنیم اما زمانی که با placeholder های اسمی کار می کنیم باید از آرایه های متناظر (associative array) استفاده کنیم، بدین صورت که key ها مساوی با نام placeholder در کوئری باشند.

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

نکته ی دوم: توجه داشته باشید که placeholder های موقعیتی به شما کمک می کنند کد های کوتاه تری بنویسید اما نسبت به ترتیب آرگومان های پاس داده شده به ()execute حساس هستند؛ یعنی دقیقا به ترتیب placeholder ها در کوئری اصلی هستند (به طور مثال علامت سوال اول، اولین آرگومان را می گیرد، دومی نیز آرگومان دومی را می گیرد و الی آخر). از طرف دیگر placeholder های اسمی کد را طولانی تر می کنند اما نسبت به موقعیت حساس نیستند (چرا که در آرایه ی متناظر با جفت های key و value تنظیم می شوند - ر.ک مثال بالا).

نکته ی سوم: در placeholder های اسمی، زمانی که بخواهید آرایه را به ()execute تحویل دهید نیازی به گذاشتن علامت دو نقطه نیست (ر.ک مثال بالا).

متد های مربوط به binding

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

  • دستورات LIMIT (یا هر دستور SQL ای که operand1 (عملوند) های رشته ای را قبول نمی کند) در صورتی که emulation mode فعال باشد.
  • کوئری های پیچیده ای که ممکن است با نوع operand (عملوند) اشتباهی، از کار بیوفتند.
  • انواع ستون های غیر عادی مانند BIGINT یا BOOLEAN که تنها operand هایی با نوع خاص را قبول می کنند.

در چنین حالت هایی باید از explicit binding2 استفاده شود که در این جهت می توانید از توابع ()bindValue و ()bindParam استفاده کنید.

1- operand (در فارسی عَمَلوَند): در برنامه نویسی هر شیء ای که قابلیت دستکاری شدن را داشته باشد، عملوَند نام دارد. آن را با عملگر یا اپراتور (operator) اشتباه نگیرید. به طور مثال در عبارت "1 + 2" اعداد 1 و 2 عملوَند و علامت "+" عملگر (operator) است.

2- explicit binding: فعل bind در فارسی معنیِ "جفت کردن" یا "به هم چسباندن" می دهد. منظور از binding در برنامه نویسی، چسباندن یک مقدار به متغیر آن است.

به طور مثال، چند خط بالاتر گفتیم که هنگام اجرای کوئری با ()execute یک سری آرگومان را به ان پاس می دادیم. این آرگومان ها همان مقادیری بودند که در کوئری باید به جای placeholder قرار می گرفتند یا به عبارت دیگر مقادیری بودند که باید به placeholder خودشان bind میشدند یا میچسبیدند. آن حالت، یک binding عادی یا غیر صریح بود. چرا؟

چون ما مستقیما و صریحا تعیین نمیکردیم که کدام مقدار جای کدام placeholder را بگیرد؛ مثلا اولین مقدار جای اولین علامت سوال را می گرفت و الی آخر. همچنین در حالت placeholder های اسمی نیز با یک آرایه ی متناظر خود به خود جای placeholder را می گرفتند (یا bind می شدند).

توجه کنید که در هر حال ما به عنوان برنامه نویس تعیین می کنیم که کدام مقدار جای کدام placeholder را بگیرد اما در این حالت ما چنین چیزی را به صورت صریح و دستی تعیین نمیکردیم.

حالا explicit binding یا "binding صریح" به این معنی است که ما به طور دستی و صریح چنین کاری را انجام دهیم.

مثلا بالاتر گفتیم که می توان از تابع ()bindValue استفاده کرد. مثال زیر را ببینید تا متوجه شوید:

binding غیر صریح (در حالت پیش فرض):

$stmt->execute(['email' => $email, 'status' => $status]);

binding صریح (با استفاده از توابعی مثل ()bindValue):

<?php
/* Execute a prepared statement by binding PHP variables */
$calories = 150;
$colour = 'red';
$sth = $dbh->prepare('SELECT name, colour, calories
    FROM fruit
    WHERE calories < :calories AND colour = :colour');
$sth->bindValue(':calories', $calories, PDO::PARAM_INT);
$sth->bindValue(':colour', $colour, PDO::PARAM_STR);
$sth->execute();
?>

در این مثال با استفاده از تابع bindValue، هر کدام از مقادیر را به صورت دستی و تک تک به placeholder خود bind کردیم. به این قسمت از کد دقت کنید:

$sth->bindValue(':calories', $calories, PDO::PARAM_INT);
$sth->bindValue(':colour', $colour, PDO::PARAM_STR);

در این کد ها ما برای مقادیر calories (به معنی کالری) و colour (به معنی رنگ) به صورت جداگانه و دستی تعریف کرده ایم که به کدام placeholder متصل شوند.

حال سوالی که پیش می آید این است که چرا باید از این روش استفاده کرد؟ علاوه بر مواردی که در بالا ذکر شد، در برخی از دستورات MySQL (مانند UPDATE) به این مشکل برخورد میکنیم. در جلسات بعدی در مورد آن ها صحبت خواهیم کرد.

سوال: چه پارامتر هایی را می توان bind کرد؟

پاسخ: تنها رشته ها و اعداد (فقط به صورت literal) می توانند bind شوند. مقادیر literal مقادیری هستند که مستقیما تعیین می شوند نه از طریق محاسبات و توابع دیگر. به طور مثال، عدد 1 در عبارت x = 1 از نوع literal است (مستقیما وارد سورس کد شده است) اما با اینکه عبارت (x = cos(0 (یعنی کوسینوسِ صفر) برابر با 1 است اما دیگر این 1 از نوع literal نیست، چرا که مستقیما در سورس کد تعریف نشده بلکه طی یک عملیات محاسباتی یا توسط توابع دیگر به عنوان خروجی برگردانده شده است (به عبارت دیگر، در سورس کد وارد نشده، بلکه به وجود آمده). در موارد دیگر اصلا نمی توانید از prepared statement ها در PDO استفاده کنید. ر

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

خلاصه ی مقاله

در این قسمت از سری آموزشی کار با رابط شیء گرای PDO به تکمیل مبحث prepared statement ها و اجرای کوئری ها پرداختیم. همچنین مثال هایی در این رابطه ذکر کرده و تفاوت هایی جزئی و فنی اما مهم و کاربردی را توضیح دادیم. امیدوارم از این قسمت استفاده کرده باشید.

در پناه حق

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

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

Jafar
31 خرداد 1399
با سلام میخواستم ببینم امنیت placeholder های موقعیتی بالاتره یا اسمی ؟ یا اصلا تفاوتی ندارن ؟

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

hadi
28 بهمن 1397
سلام اول از همه خیلی ممنون بابت آموزش های خوب و کاملتون. من یک مشکلی با prepare کردن دارم بصورتی که وقتی آرگومان به execute میدم اصلا انگار متغغیری دریافت نکرده. arge=4 stmt=pdo->prepare(SELECT id FROM user WHERE arge=?)s stmt->execute($arge)s result=$stmt->fetchAll()s اینم قسمتی که مشکل دارم پ.ن: s اخر خط برای درست نشون دادن است جز کد نیست.

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

امیر زوارمی
29 بهمن 1397
سلام به شما دوست عزیز، لطف دارید. چند نکته میگم در مورد کد شما که احتمال زیاد مشکلتون رو حل می کنه: قســـمت اول در رابطه با syntax کد هاتون (مشکل بزرگ کدتون): ما توی همین مقاله توضیح دادیم که تابع ()execute فقط یک آرایه رو قبول می کنه نه یک متغیر عادی. به این صفحه مراجعه کنید: http://php.net/manual/en/pdostatement.execute.php مشکل اینجاست که شما یک متغیر عادی رو که داخلش عدد 4 داره، به تابع ()execute دادید. قســـمت دوم در رابطه با جزئیات کد هاتون (مشکلات متفرقه و احتمالی): 1- فکر میکنم شما سعی کردید متغیری به نام arge رو تعریف کنید. درسته؟ اول اینکه arge رو صحیح تعریف نکردید و یک علامت $ جا افتاده. در زبان PHP متغیر ها به این شکل تعریف میشن: variableName$ در ضمن نقطه ویرگولِ آخر دستور رو هم نزاشتین که البته فکر می کنم اینجا به جاش S گذاشتین تا کد تمیز تر بشه، فقط گفتم که از قلم نیوفته. 2- دستوری که داخل prepare می نویسید یک کوئری SQL هست بنابراین نمیتونین متغیر دلخواهی به نام arge اونجا بزارین. در واقع arge باید یک ستون از جدولِ user در پایگاه داده ی شما باشه. اینطوری دستی نمیشه تعریفش کرد. دستور شما (یعنی -> SELECT id FROM user WHERE arge) میگه ستونِ arge رو از جدول user به من بده. اگه جدول شما چنین ستونی نداشته باشه به خطا برخورد می کنید. با این تفاسیر و قبول اینکه جدول شما دارای ستونی به اسم arge هست، شکل صحیح کد شما به این صورته: $params = array(4); stmt=pdo->prepare(SELECT id FROM user WHERE arge=?); stmt->execute($params); result=$stmt->fetchAll(); البته به این شکل هم میتونین بنویسین: $params = 4; stmt=pdo->prepare(SELECT id FROM user WHERE arge=?); stmt->execute(array($params)); result=$stmt->fetchAll(); چون کد شکلش بهم ریخته، اینجا براتون آپلود کردم: https://codesandbox.io/s/722kv6yx96

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