پیش نیاز: برای مطالعه این مقاله باید با مبانی اصلی MongoDB آشنا باشید. همچنین اطلاع از روش کار MySQL نیز به شما کمک زیادی خواهد کرد.
همانطور که می دانید در حوزه وب، پایگاه های داده بسیار زیادی وجود دارد اما این پایگاه های داده معمولا به دو دسته مشهور تقسیم می شوند:
پایگاه های داده SQL پایگاه های داده ای هستند که از زبان SQL برای ارتباط با برنامه شما استفاده می کنند. مشهور ترین این پایگاه های داده، MySQL و PostgreSQL و Microsoft SQL و Oracle هستند. این پایگاه های داده قدمتی بسیار طولانی داشته و شناخته شده هستند.
این پایگاه های داده در سال های اخیر وارد حوزه وب شده اند بنابراین قدمت پایگاه های داده SQL را ندارند اما به سادگی از محبوبیت از بسیاری از پایگاه های داده SQL پیشی گرفته اند. این دسته از پایگاه های داده از زبان SQL برای ارتباط با برنامه شما استفاده نکرده و از مبانی SQL دوری می کنند. از پایگاه های داده مشهور در این گروه می توان به MongoDB و Redis و Cassandra و DynamoDb اشاره کرد.
یکی از حملات بسیار رایج در پایگاه های داده SQL حمله تزریق SQL یا SQL Injection است. این نوع حملات با تزریق کد SQL به جای داده معتبر سعی می کنند تا کدهای SQL شما را تغییر بدهند به شکلی که نتیجه دلخواه خودشان اجرا شود. به زبان ساده تر حملات تزریق SQL به هکرها اجازه می دهد تا در پایگاه داده ما دستورات خودشان را اجرا کنند. به مثال زیر از MySQL توجه کنید:
# Define POST variables uname = request.POST['username'] passwd = request.POST['password'] # SQL query vulnerable to SQLi sql = “SELECT id FROM users WHERE username=’” + uname + “’ AND password=’” + passwd + “’” # Execute the SQL statement database.execute(sql)
این یک کد بسیار ساده است تا مفهوم SQL Injection را راحت تر درک کنید. ما در این کد دو فیلد username و password را از کاربر می گیریم و سپس بر این اساس داده ها، آیدی کاربر را از پایگاه داده دریافت می کنیم. مشکل بزرگ این کد این است که داده ها را مستقیما از سمت کاربر گرفته و بدون اعتبارسنجی در کوئری قرار می دهد. تصور کنید کاربر ما به جای password (رمز عبور) مقدار زیر را تایپ کند:
password' OR 1=1
در این مثال کوئری ما به شکل زیر خواهد بود:
SELECT id FROM users WHERE username='username' AND password='password' OR 1=1'
همانطور که می بینید شرط 1=1 همیشه برقرار خواهد بود بنابراین کوئری بالا همیشه صحیح خواهد بود و اجرا خواهد شد حتی اگر رمز عبور کاربر password نباشد. شاید با خودتان بگویید که اگر کوئری بالا در پایگاه داده اجرا شود چه اتفاقی می افتد؟ مسئله اینجاست که کوئری بالا باعث برگرداندن اولین ردیف از جدول users خواهد شد و از طرفی اولین نفر در پایگاه داده معمولا ادمین آن پایگاه داده است. حالا هکرها می توانند اطلاعات ادمین را برداشته و از احراز هویت نیز عبور کنند. با همین اشتباه ساده کل سیستم شما در اختیار هکرها قرار می گیرد. مثالی که من برایتان آورده ام یک مثال بسیار بسیار ساده است تا درک آن راحت تر باشد اما در دنیای واقعی مسئله بسیار بسیار پیچیده تر است. یک تزریق SQL پیچیده و واقعی تر به شکل زیر خواهد بود:
-- MySQL, MSSQL, Oracle, PostgreSQL, SQLite ' OR '1'='1' -- ' OR '1'='1' /* -- MySQL ' OR '1'='1' # -- Access (using null characters) ' OR '1'='1' %00 ' OR '1'='1' %16
من از MongoDB به عنوان پایگاه داده NoSQL خودمان استفاده می کنم چرا که هر پایگاه داده NoSQL قوانین مربوط به خودش را دارد و نمی توانیم دستوری کلی برای همه آن ها صادر کنیم. MongoDB داده ها را با فرمتی به نام BSON ذخیره می کند. BSON مخفف Binary JSON می باشد؛ از همین نام باید فهمیده باشید که داده ها در MongoDB به صورت باینری ذخیره می شوند و کوئری ها نیز به صورت یک شیء باینری به پایگاه داده می رسند با این حساب تزریق کد به صورت رشته ای (چیزی که در SQL داشتیم) اصلا ممکن نیست اما این به معنای امنیت کامل این پایگاه داده نیست. برای اینکه برنامه شما با MongoDB تعامل داشته باشد باید از درایور های MongoDB استفاده کنید که انواع و اقسام خودشان را دارند. مثلا اگر از دستورات where$ یا mapReduce در درایور MongoDB استفاده کنید، هکرها می توانند کدهای جاوا اسکریپت را مستقیما درون کدهای شما تزریق کنند. در این حالت به جای اینکه کدها به کوئری پایگاه داده تزیق شوند، به سورس کد شما تزریق خواهند شد. این مسئله در documentation رسمی MongoDB نیز ذکر شده است.
من یک مثال ساده از استفاده از PHP و MongoDB را برایتان ذکر می کنم:
$query = array("user" => $_POST["username"], "password" => $_POST["password"]);
حالا هکر می تواند از اپراتور های ne$ یا gt$ به جای داده معتبر استفاده کند تا سیستم احراز هویت شما را دور بزند:
username[$ne]=1&password[$ne]=1
با این حساب، کوئری شما به شکل زیر خواهد بود:
array("username" => array("$ne" => 1), "password" => array("$ne" => 1));
این کوئری می گوید تمام کاربرانی را پیدا کن که username و password آن ها با عدد 1 برابر نباشد و طبیعتا هیچکس یوزرنیم یا رمز عبور 1 را ندارد بنابراین هکر می تواند به سادگی سیستم احراز هویت ما را دور بزند.
حالا مثالی از زبان جاوا اسکریپت را ذکر می کنیم. تصور کنید در پایگاه داده خود کالکشنی به نام students داشته باشیم که داده های زیر را در خود داشته باشد:
{username:'John Doc', email:'example@gmail.com', age:20}, {username:'Rafael Silver', email:'example0@gmail.com', age:30}, {username:'Kevin Smith', email:'example1@gmail.com', age:22}, {username:'Pauline Wagu', email:'exampl2e@gmail.com', age:23}
حالا فرض کنید قسمتی از برنامه شما مسئول دریافت تمام دانش آموزانی است که سن برابر با 20 سال دارند (یا هر سن دیگری که کاربر از برنامه ما بخواهد). در این حالت ممکن است کدی شبیه به کد زیر را نوشته باشید:
app.get(‘/:age’, function(req, res){ db.collections(“students”).find({age: req.params.age}); })
در حالت عادی کاربر عدد 20 را وارد کرده و این کد نیز نتیجه John Doc را برمی گرداند (فقط اوست که 20 سال دارد) اما اگر فردی با اهداف شومی به سایت ما آمده باشد ممکن است به جای ارسال یک عدد، یک شیء را ارسال کند. مثلا:
{‘$gt:0’}
اگر چنین اتفاقی بیفتد کوئری ما به شکل زیر خواهد بود:
db.collections(“students”).find({age: {‘$gt:0’});
اپراتور gt مخفف greater than یا «بزرگ تر از» می باشد بنابراین با اجرای چنین کدی، تمام کاربرانی که سن بالای 0 سال را دارند برگردانده خواهند شد. طبیعتا تمام کاربران ما سنشان از 0 سال بیشتر بوده و عملا پایگاه داده را در دسترس هکرها قرار داده ایم.
البته باید در نظر داشته باشید که در حالت عادی چنین اتفاقی نمی افتد چرا که MongoDB داده ها و کوئری هایش را در حالت باینری مدیریت می کند اما اگر از برخی دستورات MongoDB مانند where$ و group$ و mapReduce$ استفاده کنید، چنین حملاتی کاملا ممکن خواهند بود. مسئله اینجاست که MongoDB داده هایش را parse نمی کند بنابراین خطر چنین حملاتی به شدت کاهش پیدا خواهد کرد اما اگر برنامه شما نیاز به parse کردن داده ها داشته باشد، شما در معرض چنین حملاتی خواهید بود. من چند مثال از تزریق کد با اپراتور های ذکر شده را برایتان آماده کرده ام.
فرض کنید username و password را از طریق فرمی دریافت کرده ایم و حالا می خواهیم کاربر مورد نظر را بر اساس این دو مقدار برگردانیم. برای انجام این کار می توان به شکل زیر عمل کرد:
app.post('/students, function (req, res) { var query = { username: req.body.username, password: req.body.password } db.collection(students).findOne(query, function (err, student) { res(student); }); });
حالا تصور کنید که چنین درخواستی را دریافت کرده باشیم:
POST https://localhost/students HTTP/1.1 Content-Type: application/json { "username": {"$ne": null}, "password": {"$ne": null} }
در چنین حالتی حتما کاربر اول برگردانده می شود. چرا؟ به دلیل اینکه درخواست بالا از اپراتور ne$ استفاده کرده است (مخف not equal یا «برابر نیست با») و از آنجایی که نام کاربری و رمز عبور هیچ کدام از کاربران ما null نیست، شرط صحیح بوده است و اولین کاربر از این کالکشن برگردانده خواهد شد. برای حل این مشکل امنیتی دو راه دارید: یا به صورت دستی داده های کاربر را اعتبارسنجی کنید تا مطمئن شوید از کاراکترهای ویژه مانند $ استفاده نکرده است یا اینکه از پکیج های آماده مانند mongo-sanitize استفاده نمایید که به صورت خودکار از وارد شدن $ در کوئری هایتان جلوگیری می کنند.
npm install mongo-sanitize
با اجرای دستور بالا در ترمینال خود، می توانید این پکیج را نصب کنید. حالا به اسکریپت خود رفته و چنین مکانیسمی را در آن پیاده سازی می کنید:
var sanitize = require(‘mongo-sanitize’); var query = { username: req.body.username, password: req.body.password }
همچنین می توانید از درایور هایی مانند mongoose استفاده کرده و نوع داده ای را برای هر فیلد مورد نظر خود تست کنید.
اپراتور where$ یکی از خطرناک ترین اپراتور های MongoDB است چرا که به رشته ها اجازه می دهد درون سرورِ پایگاه داده (سرور mongodb، نه سرور برنامه تان) evaluate و اجرا شوند. به نمونه کد زیر توجه کنید:
var query = { $where: “this.age > ”+req.body.age } db.collection(students).findOne(query, function (err, student) { res(student); });
حالا اگر درخواست خود را به شکل زیر دریافت کنیم چه می شود؟
‘0; return true’
در این حالت استفاده از ماژول sanitize به شما کمکی نخواهد کرد چرا که کوئری بالا باعث برگرداندن تمام دانش آموزان (تمام داده های کالکشن student) خواهد شد. از رشته های ممکن دیگر برای اجرای حمله تزریق کد می توان به مقادیر زیر اشاره کرد:
‘\’; return \ ‘\’ == \’’ or this.email === ‘’;return ‘’ == ‘’
اپراتور where$ علاوه بر اینکه خطر حملات تزریق کد را بیشتر می کند، از ایندکس ها نیز استفاده نمی کند بنابراین سرعت بسیار کمتری دارد و باید تا حد ممکن از آن دوری شود. یکی دیگر از مشکلات آن نیز پاس دادن یک تابع به where است. با این کار متغیر مورد نظر در mongodb در دسترس نخواهد بود بنابراین برنامه شما Crash خواهد کرد. مثال:
var query = { $where: function() { return this.age > setValue //setValue is not defined } }
برای محافظت پایگاه داده خود در برابر چنین حملاتی باید به چند اصل اساسی وفادار بمانید:
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.