搜尋此網誌

2012年6月6日 星期三

[教戰守則] NoSQL, No Injection?

logo-mongoDB前一陣子在拜讀 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 也會遭受注入攻擊!

解法是什麼?當然還是回到老手法,程式必須做好輸入的檢查(包含型別、內容數值、範圍等)。如果疏於此道,就算是 MongoDB 也無法讓你的系統刀槍不入外加考試 100 分。

3 則留言:

  1. 這邊出現的問題主要是PHP會把字串"password[$ne]=1"當成陣列array("$ne"=>1)處理所造成的問題,所以也不能算是Mongo的問題
    如果PHP的Driver設計像其他把Monogo的查詢條件都用特定類別而不使用陣列的話就不會出現這種問題。

    回覆刪除
  2. 我想這是屬於 MongoDB 抑或 PHP Driver 的問題,其實並不是真正的重點。真正的重點就是這些條件兜在一起,再加上程式開發人員的疏忽,就會產生安全的問題。從一個系統開發的角度來看,就是有安全的問題,沒有任何推諉的空間。
    根據多層次防禦的觀念,做好輸入的檢查與過濾,是絕對無法偷懶的一件事。系統開發人員無法 100% 控制系統執行時的環境,所以必須以"最保險"的方式加以設計與開發。

    回覆刪除
  3. 廣義來看,所有的注入攻擊其實都是上層沒有針對底層的 context 做好足夠的處理,所以要說是底層的錯還是上層的錯?

    回覆刪除

About