Laravel Bindings 的一處安全隱患

李銘昕發表於2021-01-22

相關 PR 如下

#35865
#35972

以下的程式碼使用 8.x 的程式碼測試,這個問題 6.x7.x 版本同樣存在

原因

我們先看一下未修改前的程式碼表現,我們使用 laravel/framework 元件的 8.22.0 版本進行實現。

表結構如下

CREATE TABLE `test` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `uid` int(10) unsigned NOT NULL DEFAULT '0',
  `type` int(10) unsigned NOT NULL DEFAULT '0',
  `created_at` datetime DEFAULT NULL,
  `updated_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

如果我們對入參都檢測的很清楚,比如以下情況

Route::get('/', function () {
    DB::enableQueryLog();
    $data = Test::query()->where('uid', 1)->where('type', 0)->get();
    var_dump(count($data));
    var_dump(DB::getQueryLog());
    return 'Hello World.';
});

顯然結果是符合預期的

int(1)
array(1) {
  [0]=>
  array(3) {
    ["query"]=>
    string(51) "select * from `test` where `uid` = ? and `type` = ?"
    ["bindings"]=>
    array(2) {
      [0]=>
      int(1)
      [1]=>
      int(0)
    }
    ["time"]=>
    float(10.07)
  }
}
Hello World.

但,如果我們的 uid 是通過介面傳入進來,但又沒有對其進行檢測,那麼就會導致 uid 可能傳入一個陣列。

Route::get('/', function () {
    try {
        DB::enableQueryLog();
        $data = Test::query()->where('uid', [1, 1])->where('type', 0)->get();
        var_dump(count($data));
        var_dump(DB::getQueryLog());
        return 'Hello World.';
    } catch (\Throwable $exception) {
        return 'Server Error';
    }
});

那麼結果就會天差地別

int(2)
array(1) {
  [0]=>
  array(3) {
    ["query"]=>
    string(51) "select * from `test` where `uid` = ? and `type` = ?"
    ["bindings"]=>
    array(3) {
      [0]=>
      int(1)
      [1]=>
      int(1)
      [2]=>
      int(0)
    }
    ["time"]=>
    float(9.24)
  }
}
Hello World.

第一個查詢的 SQL 是 select * from test where uid = 1 and type = 0
而後來的查詢卻是 select * from test where uid = 1 and type = 1

當然,你可能認為這並沒有什麼,但如果你的 SQL 是更新操作呢,又如果你的 user_id 不幸放到了後面呢?那豈不是所有的使用者都可以改改自己的介面入參,修改到別人的資料?

Laravel 的修改

接下來,讓我們看一下 Laravel 的修改辦法,其實核心就是一個,那就是如果發現入參是 Array,就主動使用 head 方法取 Array 的第一個元素。

讓我們更新 laravel/framework 元件到 8.24.0 再看。

int(1)
array(1) {
  [0]=>
  array(3) {
    ["query"]=>
    string(51) "select * from `test` where `uid` = ? and `type` = ?"
    ["bindings"]=>
    array(2) {
      [0]=>
      int(1)
      [1]=>
      int(0)
    }
    ["time"]=>
    float(11.33)
  }
}
Hello World.

從結果上看,確實似乎解決了上面的問題,但實則引入了一個更加嚴重的隱患,將安全隱患變成了事故隱患。

可能存在的事故隱患

以下程式碼都是基於這次修改,我本人是不會這麼寫程式碼的

首先,我們假定,開發者已經知道了上述問題,而且他認為上述情況也是合理的,也是這麼用的。

那麼 Laravel 的這次修改,並不會報任何錯,但程式碼含義卻完全不同,本來使用者就想修改 type=1, user_id=1 的記錄,一旦框架去掉了後面的資料,那搞不好就修改成了 type=1, user_id=0 的記錄。

如果 user_id = 0 代表的含義是沒有使用者ID的記錄,豈不是一口氣將所有非使用者產生的訊息全部修改掉。。

而且最可怕的是,開發者並不知情,就算哪天發現了這個問題,資料也已經很難回滾了。

結論

所以,我認為還是應該直接丟擲錯誤

    /**
     * Get a scalar type value from an unknown type of input.
     *
     * @param  mixed  $value
     * @return mixed
     */
    protected function flattenValue($value)
    {
        if(is_array($value)){
            throw new \InvalidArgumentException();
        }

        return $value;
    }

那麼剛剛的程式碼就會出現以下情況

Server Error

寫在最後

Hyperf 是基於 Swoole 4.4+ 實現的高效能、高靈活性的 PHP 協程框架,內建協程伺服器及大量常用的元件,效能較傳統基於 PHP-FPM 的框架有質的提升,提供超高效能的同時,也保持著極其靈活的可擴充套件性,標準元件均基於 PSR 標準 實現,基於強大的依賴注入設計,保證了絕大部分元件或類都是 可替換 與 可複用 的。

框架元件庫除了常見的協程版的 MySQL 客戶端、Redis 客戶端,還為您準備了協程版的 Eloquent ORM、WebSocket 服務端及客戶端、JSON RPC 服務端及客戶端、GRPC 服務端及客戶端、Zipkin/Jaeger (OpenTracing) 客戶端、Guzzle HTTP 客戶端、Elasticsearch 客戶端、Consul 客戶端、ETCD 客戶端、AMQP 元件、Apollo 配置中心、阿里雲 ACM 應用配置管理、ETCD 配置中心、基於令牌桶演算法的限流器、通用連線池、熔斷器、Swagger 文件生成、Swoole Tracker、Blade 和 Smarty 檢視引擎、Snowflake 全域性ID生成器 等元件,省去了自己實現對應協程版本的麻煩。

Hyperf 還提供了 基於 PSR-11 的依賴注入容器、註解、AOP 面向切面程式設計、基於 PSR-15 的中介軟體、自定義程式、基於 PSR-14 的事件管理器、Redis/RabbitMQ 訊息佇列、自動模型快取、基於 PSR-16 的快取、Crontab 秒級定時任務、Translation 國際化、Validation 驗證器 等非常便捷的功能,滿足豐富的技術場景和業務場景,開箱即用。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.

相關文章