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

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

با سلام، در قسمت های قبلی با نحوه ی صحیح اتصال به پایگاه داده در PDO آشنا شدیم. دلیل تاکید من بر عبارت "نحوه ی صحیح" این است که راه های بسیاری برای اتصال به پایگاه داده وجود دارد اما روشی که در جلسات قبل ارائه شد روش استاندارد و صحیح این کار است. در این قسمت به سراغ اجرای کوئری ها (query) در PDO خواهیم رفت. این مبحث ممکن است چندین قسمت به طول بینجامد و دارای زیر رشته های زیادی باشد، بنابراین اگر شما تازه کار هستید دنبال کردن تک تک قسمت های این دوره بر اساس ترتیب پیشنهادی روکسو بسیار مهم خواهد بود. نحوه جلوگیری از SQL Injection را به شما آموزش می دهیم.

اجرای کوئری ها در PDO با ()PDO::query

دو راه برای اجرای کوئری ها در PDO وجود دارد؛ اگر قرار نیست هیچ متغیری به کوئری داده شود می توانید مستقیما از دستور ()PDO::query استفاده کنید که پس از اجرا به شما شیء خاص PDOStatement را برمیگرداند. این شیء تقریبا شبیه به نتیجه ای است که از دستور ()mysql_query می گیرید، مخصوصا از آن جهت که می توانید به وسیله ی آن، ردیف ها (row) را دریافت کنید:

$stmt = $pdo->query('SELECT name FROM users');
while ($row = $stmt->fetch())
{
    echo $row['name'] . "\n";
}

همچنین باید گفت که دستور ()query به ما اجازه می دهد از method chaining (زنجیره سازی دستور ها) برای کوئری SELECT استفاده کنیم. البته در این مورد بعدا صحبت خواهیم کرد.

Prepared statements و جلوگیری از SQL Injection

ابتدا توضیحی کوتاه در رابطه با تزریق SQL یا همان SQL Injection ؛ باید بدانید که این حمله، از شایع ترین حملات به وب سایت ها است و ممکن است پایگاه داده و یا وب سایت شما یا هر دو را نابود کند!

این نوع حمله زمانی اتفاق می افتد که شما از کاربر می خواهید مقداری را به شما بدهد. مثلا در فرمی از کاربر سن او یا نامش را میپرسید، یا در فرم login از او حساب کاربری و رمز عبور می خواهید، یا حتی در کامنت ها از کاربران نظرشان را می پرسید! در تمامی این حالات شما در معرض این حملات قرار می گیرید.

مثالی کوتاه و ساده از این حملات را برای شما توضیح می دهم. فرض کنید یکی از کوئری های ما به شکل زیر باشد:

txtUserId = getRequestString("UserId");
txtSQL = "SELECT * FROM Users WHERE UserId = " + txtUserId;

منطق برنامه نویسی در SQL به این شکل است که عبارت 1=1 همیشه صحیح یا true تلقی می شود. حالا فرض کنید از کاربر بخواهیم مقداری را (مثلا username اش را) به ما بدهد و کاربر به جای تایپ آن مقدار، عبارت 105 OR 1=1 را در فرم وارد کند. در چنین حالتی کوئری ما به شکل زیر در خواهد آمد:

SELECT * FROM Users WHERE UserId = 105 OR 1=1;

آیا می بینید چه اتفاقی افتاد؟ بنابر این فرض که 1=1 همیشه صحیح است، این کوئری نیز همیشه صحیح است و طبیعتا همیشه اجرا شده و تمام Row (ردیف) ها را از جدول "users" برمی گرداند!!! حالا اگر جدول users دارای نام کاربران و رمز عبورشان یا اطلاعات مهم دیگر بود چه؟

در واقع کوئری بالا، برای SQL دقیقا مساوی با کوئری پایین است:

SELECT UserId, Name, Password FROM Users WHERE UserId = 105 or 1=1;

به این ترتیب یک هکر مبتدی به تمام اطلاعات کاربران شما دست پیدا می کند!!

مسئله این جاست که اکثر کاربران یک رمز واحد را برای اکثر وب سایت ها و کارهایشان انتخاب می کنند (این کار اصلا توصیه نمی شود و شما باید رمز های متفاوتی داشته باشید)

بنابراین با اینکه ممکن است سایت شما، سایت لطیفه و جوک باشد اما رمز ایمیل یا رمز بانکی آن کاربر الان به دست هکر افتاده است چرا که رمز عبور آن کاربر در سایت شما با رمز عبورش در سایت بانک یکی بوده است!

شما هیچ وقت نباید مسئولیت اخلاقی خود را به عنوان یک توسعه دهنده و برنامه نویس از یاد ببرید. در واقع کاربران به شما اعتماد کرده اند و شما حافظ اطلاعات آن ها هستید.

یکی دیگر از روش های SQL Injection، بر این اساس است که در SQL عبارت ""="" همیشه معادل true خواهد بود. حالا اگر یک کوئری به شکل زیر داشته باشیم:

uName = getRequestString("username");
uPass = getRequestString("userpassword");

sql = 'SELECT * FROM Users WHERE Name ="' + uName + '" AND Pass ="' + uPass + '"'

نتیجه اش می شود:

SELECT * FROM Users WHERE Name ="John Doe" AND Pass ="myPass"

تا اینجای کار مشکلی نیست اما اگر کاربر/هکر، به طور مثال، به جای موارد خواسته شده مقدار "="" OR " را وارد کند چه می شود؟

Username: " or ""="

Password: " or ""="

نتیجه ی این کد، کوئری زیر است:

SELECT * FROM Users WHERE Name ="" or ""="" AND Pass ="" or ""=""

این کوئری بنابر چیزی که گفتیم همیشه صحیح یا true است و اطلاعات جدول users را در اختیار کاربر/هکر قرار خواهد داد. روش های تزریق SQL  یا SQL Injection بسیار زیاد و بعضا پیچیده هستند. ما در اینجا تنها یک مثال ساده از آن را ذکر کردیم تا شما با این مبحث آشنا شوید.

خب برمیگردیم به اصل مطلب خودمان،

مبارزه با SQL Injection یا تزریق SQL از بزرگترین دلایلی است که ما می گوییم باید از PDO استفاده کرد. در واقع بسیاری از برنامه نویسان اعتقاد دارند مبارزه با تزریق SQL مهم ترین و تنها دلیل دور انداختن ()mysql_query و استفاده از PDO است. PDO به طور پیش فرض از prepared statements (به معنی دستورات آماده) پشتیبانی می کند و قابل ذکر است که prepared statement ها تنها راه صحیح و اصولی اجرای کوئری ها هستند (در صورتی که کوئری ما دارای متغیر باشد).

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

قئم اول، تغییر کوئری و جاگذاری placeholder به جای متغیر هاست. برای مثال کوئری زیر:

$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';

اگر به مثال توجه کرده باشید، فهمیده اید که PDO از placeholder های موقعیتی یا positional (در مثال -> ?) و اسمی یا named (در مثال -> email:) پشتیبانی می کند. برای استفاده از placeholder های اسمی از دو نقطه استفاده می کنید (مانند مثال). نامی که برای آن انتخاب می کنید باید از بین حروف، اعداد و آندرلاین انتخاب شده باشد. همچنین توجه داشته باشد که هیچ نوع quotation ای این نوع از placeholder ها را احاطه نکرده است.

خلاصه ی مقاله

تا اینجای کار با مفهوم prepared statement ها (به طور مبتدی و ساده) و آماده سازی کوئری ها آشنا شدیم. در قسمت های بعدی به سراغ مثال های عملی از این بحث خواهیم رفت و نکات بیشتری را به شما توضیح خواهیم داد.

در پناه حق

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

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

Ahmad Tavakoli
09 بهمن 1397
سلام اقای زوارمی میدونم اینجا جاش نیس ولی تو دوره ی پی اچ پی از namespace ها هیچی نگفتی اونا چین؟ بعدا مقاله میدی براشون؟

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

امیر زوارمی
09 بهمن 1397
سلام ان شاء الله اگه وقت و عمری باشه حتما... مبحث namespace ها مبحث نسبتا جدیدی هست و با اینکه یاد گرفتنش بسیار خوب هست اما آنچنان ضروری نیست. به این دلیل و همچنین به دلیل طولانی بودن این بحث من ازش در دوره ی PHP شیء گرا صحبتی نکردم. در حال حاضر در حال نوشتن PDO و JavaScript هستم. اگر خدا بخواد حتما سراغ اون مبحث هم میرم و سری آموزشی جدا براش مینویسیم.

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