基於 Serverless 的 Valine 可能並沒有那麼香

公子發表於2020-11-16

Valine 是一款樣式精美,部署簡單的評論系統, 第一次接觸便被它精美的樣式,無服務端的特性給吸引了。它最大的特色是基於 LeanCloud 直接在前端進行資料庫操作而無需服務端,極大的縮減了部署流程,僅需要在靜態頁引入 Valine SDK 即可。

?‍?‍ 初識 Valine

以下是 Valine 官網提供的快速部署指令碼,其中 appIdappKey 是你在 LeanCloud 上建立應用後對應的應用金鑰。也正是基於這對金鑰,Valine 在內部呼叫了 LeanCloud SDK 進行資料的獲取,最終將資料渲染在 #vcomments 這個 DOM 上。這便是 Valine 的大概原理。

<head>
  ..
  <script src='//unpkg.com/valine/dist/Valine.min.js'></script>
  ...
</head>
<body>
  ...
  <div id="vcomments"></div>
  <script>
    new Valine({
      el: '#vcomments',
      appId: 'Your appId',
      appKey: 'Your appKey'
    })
  </script>
</body>

有同學可能會有疑問了,appIdappKey 都直接寫在前端了,那豈不是誰都可以修改資料了?這就需要牽扯到 LeanCloud 的資料安全問題了,官方專門寫了篇文件《資料和安全》 來說明這個問題。簡單的理解就是針對資料設定使用者的讀寫許可權,確保正確的人對資料有且僅有正確的許可權來保證資料的安全。

乍聽一下,保證使用者資料只讀的話,感覺還是挺安全的。可事實真的如此麼,讓我們繼續來看看。

?‍♂️ Valine 的問題

? 閱讀統計篡改

Valien 1.2.0 增加了文章閱讀統計的功能,使用者訪問頁面就會在後臺 Counter 表中根據 url 記錄訪問次數。由於每次訪問頁面都需要更新資料,所以在許可權上必須設定成可寫,才能進行後續的欄位更新。這樣就造成了一個問題,實際上該條資料是可以被更新成任意值的。感興趣的同學可以開啟 https://valine.js.org/visitor... 官網頁面後進入控制檯輸入以下程式碼試試。試完了記得把數改回去哈~

const counter = new AV.Query('Counter');
const resp = await counter.equalTo('url', '/visitor.html').find();
resp[0].set('time', -100001).save();
location.reload();

可以看到該頁面的訪問統計被設定成了 -100000 了。這個問題唯一值得慶幸的是 time 欄位的值是 Number 型別的,其它的值都無法插入。如果是字串型別的話就是一個 XSS 漏洞了。

該問題有一個解決辦法,就是不使用次數累加的儲存方式。更改為每次訪問都儲存一條只讀的訪問記錄,讀取的時候使用 count() 方法進行統計。這樣所有資料都是隻讀的,就不存在篡改的問題了。這種解決方案唯一的問題就是資料量會比較大,對查詢會造成一定壓力。當然如果是在基於原資料不變的情況下,只能是增加一層服務端來做修改許可權的隔離了。

? XSS 安全

從很早的版本開始就有使用者報告了 Valine 的 XSS 問題,社群也在使用各種方法在修復這些問題。包括增加驗證碼,前端XSS過濾等方式。不過後來作者才明白,前端的一切驗證都只能防君子,所以把驗證碼之類的限制去除了。

現有的邏輯裡,前端釋出評論的時候會將 Markdown 轉換成 HTML 然後走一下前端的一個 XSS 過濾方法最後提交到 LeanCloud 中。從 LeanCloud 中拿到資料之後因為是 HTML 直接插入進行顯示即可。很明顯,這個流程是存在問題的。只要直接提交的是 HTML 而且拿到 HTML 之後直接進行展示的話,XSS 從根本上是無法根除的。

那有沒有根本的解決辦法?其實是有的。針對儲存型的 XSS 攻擊,我們可以使用轉義編碼進行解決。只要效仿早前 BBCode 的做法,提交到資料庫的是 Markdown 內容。前端讀取到內容對所有 HTML 進行編碼後再進行 Markdown 轉換後展示。

function encodeForHTML(str){
  return ('' + str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')    
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;')
    .replace(/\//g, '&#x2F;');
};

由於 Serverless 攻擊者是可以直達儲存階段,所以資料儲存之前的一切防範是無效的,只能在讀取展示過程處理。由於所有的 HTML 轉義後無法解析,Markdown 相當於我們根據自定義的語法解析成 HTML,保證轉換後的 HTML 沒有被插入的機會。

不過這個方法存在一個問題,那就是對老資料存在不相容。因為這相當於修改了儲存和展示的規則,而之前一直儲存的都是 HTML 內容,修復後之前的資料將無法展示 HTML 樣式。而為了能在儲存的還是 HTML 情況下規避 XSS 安全問題,唯一的辦法就是增加服務端中間層。儲存階段增加一道閥門,將轉義階段提前至儲存階段,保證新老資料的通用。

? 隱私洩露

說完了儲存的問題,我們再來看看讀取的問題。攻擊者除了可以直達儲存,也可以直達讀取,當一個資料庫的欄位開放了讀取許可權後,相當於該欄位的內容對攻擊者是透明的。

在評論資料中,有兩個欄位是使用者比較敏感的資料,分別是 IP 和郵箱。燈大甚至專門寫了一篇文章來批判該問題 《請馬上停止使用Valine.js評論系統,除非它修復了使用者隱私洩露問題》。甚至掘金社群在早期使用 LeanCloud 的時候也暴出過洩露使用者手機號的安全問題。

為了規避這個問題,Valine 作者增加了 recordIP 配置用來設定是否允許記錄使用者 IP。由於是 Serverless,目前能想到的也只是不儲存的方式解決了。不過該配置項會存在一個問題,就是該配置項的配置權在網站,隱私的問題是評論者遇到的,也就是說評論者是無權管理自己的隱私的。

除了這個矛盾點之外,還有就是郵箱的問題。郵箱本質上只需要返回 md5 用來獲取 Gravatar 頭像即可。但是由於無服務端的限制,只能返回原始內容由前端計算。而郵箱我們又需要獲取到原始值,方便做評論回覆郵件通知功能。所以我們也不能不儲存,或者儲存 md5 後的值。

該問題的解決方案只能是增加一層服務端,通過服務端過濾敏感資訊解決這個問題。

? Waline!

基於以上原因,我們發現只有增加一層服務端中間層才能很好的解決 Valine 的安全問題,所以 Waline 橫空出世了!Waline 與 Valine 最大的不同就是增加了服務端中間層,解決 Valine 暴露出來的安全問題。同時基於服務端的特性,提供了郵件通知微信通知評論後臺管理、LeanCloud, MySQL, MongoDB, SQLite, PostgreSQL 多儲存服務支援等諸多特性。不僅如此,Waline 預設使用 Vercel 部署,實現完全免費部署!

Waline 最初的目標僅僅是為 Valine 增加上服務端中間層。但是由於作者不知為何從 1.4.0 版本開始只推送編譯後的檔案到 Github 倉庫中,原始檔停止更新。導致我只能連帶前端也實現一遍。當然前端的很多程式碼和邏輯為了和 Valine 的配置保持一致都有參考 Valine,甚至在名字上,我也是從 Valine 上衍生的,讓大家能明白這個專案是 Valine 的衍生版。

? 後記

Serverless 的概念火了非常多年,但技術沒有銀彈,我們在看到它的優點的同時,也要正視它所帶來的問題。而 Serverless 自己可能也意識到了這個問題,從早期的無服務端慢慢轉向了無伺服器,更偏向 BaaS 了。不過由於 Valine 沒有開放原始碼,所以上面說的一些問題和解決方法只能等待作者自己發現這件事了。

相關文章