OWASP Top 10是開放網路軟體安全計畫(Open Web Application Security Project, OWASP)的「OWASP十大網路應用系統安全安全弱點」,很多程式碼弱點掃描像是Fortify等,也會依據OWASP Top 10來檢視原始碼是否有風險。有些被報告出來的可能是真實風險,也可能是誤報,針對兩個案例來講解解決方法。

Math.random()

問題

有一個白箱的原始碼弱點掃描報告顯示了大概是以下這樣的問題,意思是使用Math.random()函數不安全。

Insecure RandomnessHigh
Sink: FunctionPointerCall: random

分析

Math.random()是在JavaScript產生隨機數的一個常見方法,但經過一些白箱的原始碼弱點掃描後會標註為問題,理由是Math.random()產生的隨機數不夠隨機,容易被攻擊者猜中。問題是我們有時候只是想拿Math.random()做一些簡單的隨機而已,沒有被猜中、不被猜中的問題,很多Dependencies套件也基於一樣理由用了這個隨機函數,該怎麼解決呢?

解決

首先作為替換的亂數函數,可以使用window.crypto,是一個比較安全的方法亂數產生方法。使用時可以呼叫crypto.getRandomValues(new Uint32Array(1))[0],他會返回一個隨機整數。可是原本的Math.random()返回的是返回0到1之間的隨機浮點數,為了可以無痛相容原本的程式碼,因此可以透過以下方法將他變成浮點數。

parseFloat('0.' + crypto.getRandomValues(new Uint32Array(1))[0])

下面寫一個函數可將文字內所有Math.random()置換成parseFloat('0.' + crypto.getRandomValues(new Uint32Array(1))[0]),可以透過NodeJS等方式將JavaScript原始碼通過這個函數轉換後再使用。根據Mozilla self.crypto這篇文件說明來看,現代瀏覽器在非常早期的版本就開始支援window.crypto,用起來應該沒什麼相容性問題。

/**
 * 置換 `Math.random()` 方法。
 * 參考 https://klab.tw/
 * @param {string} str 
 */
function changeMathRandom(str) {
    const mathStr = "Math.random()"
    const mathNew = "parseFloat('0.'+crypto.getRandomValues(new Uint32Array(1))[0])"
    return str.replaceAll(mathStr, mathNew)
}

Axios setAttribute

問題

一些白箱原始碼弱點掃描可能會出現以下這樣的報告,意指使用setAttribute的方式不安全。

Cross-Site Scripting: DOMHigh
Sink: ~JS_Generic.setAttribute()

分析

瀏覽器上JavaScript的DOM有個setAttribute(key, value)方法,直接使用沒有問題,但是當key是”href”的時候原始碼弱掃會跳出來說不安全。可以參考這篇Axios GitHub Issue,Axios的axios\lib\helpers\isURLSameOrigin.js使用了setAttribute("href", href),因此被弱掃軟體判斷為不安全,可是Axios使用這個函數指定href不是真的要拿來產生連結給使用者點選,因此被開發團隊判斷為弱掃軟體誤報,不會修復。

解決

雖然安全上應該沒問題,但有些弱掃軟體就是會跳出報告說不安全,偏偏還是個高風險跨網域請求風險,因此想一個折衷方案繞過檢查,將setAttribute("href", href)變成
setAttribute(String.fromCharCode(104, 114, 101, 102), href),利用fromChartCode的方式重新生成字串來繞過檢查,想要更安全的話也可以直接把整段程式碼刪除。

我是用Parcel來打包程式碼,也有人透過WebPack或是其他軟體來打包,通常都會進行JavaScript程式碼最佳化(Optimize),經過最佳化後程式碼的內容會不太一樣,因此以下寫了一個方法透過正規表達降低誤殺機率。JavaScript程式碼打包與最佳化之後,再透過NodeJS執行以下函數把程式碼修改一下。

/**
 * 置換Axios的 `lib/helpers/isURLSameOrigin.js` 內的方法。
 * 參考 https://klab.tw/
 * @param {string} str 
 * @returns 
 */
function changeSetAttrHref(str) {
    const r = /([\w]+)\.setAttribute\("href",[ ]*([\w]+)\)/g
    const t = "$1.setAttribute(String.fromCharCode(104,114,101,102),$2)"
    return str.replaceAll(r, t)
}

想要更安全一點的話,直接const t = ""將風險程式碼變不見就好了,目前測起來少了這段Axios運作起來也沒問題。

自動化修正

Math.random()的亂數方法、Axios的setAttribute方法,這些問題除了自己會發生,很多第三方套件也會發生,沒辦法一個個改,就算點開node_modules資料夾修改了原始碼,下載更新套件後又沒了,因此可以從build後的程式碼著手。

透過NPM開發時會有一個package.json檔案,通常裡面會寫一個build指令,可以在這個build指令最後面加上一個而外的JavaScript程式,讓NodeJS修正build後的程式。就省得從原始碼修改的話,有一些第三方套件會不好搞定的問題。

"scripts": {
    "build": "parcel build && node fix.js"
}

因為我用Parcel,所以這邊build指令使用了parcel build,可以自己修改,重點只是後面的&& node fix.js,這個fix.js檔案要放在專案根目錄,將以上的修正函數放進去即可。以下可以提供一個範本,操控NodeJS讀取打包過的index.js檔案,以及程式碼寫回去的方法。

/**
 * 讀取打包好的index.*.js檔案,處理後再重新儲存。
 * 參考 https://klab.tw/
 */
const fs = require('fs')

// 打包後的程式碼的位置
const folder = './dist/'
fs.readdirSync(folder).forEach(file => {
    // 因為檔名通常會加上hash,因此透過正規表達尋找檔案
    // index.js 與 index.hash.js 都可以匹配
    if(file.match(/index\.?[\w\d]*.js/)) {
        const filename = `${folder}${file}`
        // 讀取檔案
        let data = fs.readFileSync(filename).toString()
        // 在這邊進行你要的處理
        // 將檔案儲存回去
        fs.writeFileSync(filename, data)
    }
})