JS `PostMessage` 跨域通信漏洞与 `Origin Validation Bypass`

各位观众老爷,大家好!我是你们的老朋友,今天咱们来聊聊一个前端老生常谈但又经常被忽略的安全话题:JS PostMessage 跨域通信漏洞与 Origin Validation Bypass。 这玩意儿听起来高大上,其实说白了,就是你家大门没锁好,别人能溜进来偷东西。

咱们先来个简单的背景介绍。

跨域是个啥?

话说浏览器出于安全考虑,搞了个“同源策略”,简单来说,就是协议、域名、端口都一样的才能互相访问。 这就像住在一个小区,你家和隔壁老王家门牌号不一样,你就不能随便进老王家串门,怕你偷东西嘛!

但是!有时候我们就是想串门,比如A网站想把数据传给B网站,怎么办呢? 这时候 PostMessage 就闪亮登场了。

PostMessage是个啥?

PostMessage 是一个安全地实现跨域通信的机制。 它可以让不同源的页面之间传递消息。 就像小区物业允许你给老王家写信,通过物业转交,这样你就不用翻墙进老王家了。

// A网站 (http://a.example.com)
const otherWindow = window.open('http://b.example.com'); // 打开B网站
otherWindow.postMessage('Hello from A!', 'http://b.example.com');

window.addEventListener('message', (event) => {
  if (event.origin !== 'http://b.example.com') {
    return; // 拒绝来自其他域的消息
  }
  console.log('A received: ' + event.data);
});

// B网站 (http://b.example.com)
window.addEventListener('message', (event) => {
  if (event.origin !== 'http://a.example.com') {
    return; // 拒绝来自其他域的消息
  }
  console.log('B received: ' + event.data);
  event.source.postMessage('Got your message A!', event.origin);
});

这段代码中,A网站给B网站发送了一个消息 "Hello from A!",B网站收到后回复 "Got your message A!"。 注意,这里用到了 event.origin 来验证消息的来源,防止有人冒充。

漏洞:Origin Validation Bypass

理论上很安全,但现实往往很骨感。 如果你 Origin Validation 没做好,或者压根没做,那就惨了,相当于你家大门没锁,谁都能进。

1. 缺失 Origin 验证

最常见的错误就是压根没验证 event.origin。 就像小区物业根本不管谁来送信,直接往你家塞,啥牛鬼蛇神都来了。

// 错误示范!
window.addEventListener('message', (event) => {
  console.log('Received: ' + event.data);
  // 缺少 origin 验证!
  // do something with event.data
});

这段代码直接接收所有来源的消息,并处理 event.data。 如果 event.data 包含恶意代码,攻击者就可以为所欲为。 例如,攻击者可以修改页面上的敏感信息,甚至执行恶意脚本。

2. 弱 Origin 验证

有时候,开发者做了验证,但是验证写得太烂,跟没做一样。

  • 只验证格式,不验证具体值

    window.addEventListener('message', (event) => {
      if (typeof event.origin === 'string') {
        console.log('Received from origin: ' + event.origin);
        // do something with event.data
      }
    });

    这段代码只验证了 event.origin 是字符串类型,但没验证具体的值。 攻击者可以发送任何字符串作为 event.origin,比如 "evil.com",也能通过验证。

  • 使用不安全的字符串匹配

    const allowedOrigin = 'http://a.example.com';
    window.addEventListener('message', (event) => {
      if (event.origin.startsWith(allowedOrigin)) {
        console.log('Received from allowed origin: ' + event.origin);
        // do something with event.data
      }
    });

    这段代码使用 startsWith 验证 event.origin 是否以 allowedOrigin 开头。 攻击者可以利用这个漏洞,发送 http://a.example.com.evil.com 作为 event.origin,也能通过验证。

  • Origin 白名单不完整

    const allowedOrigins = ['http://a.example.com', 'http://b.example.com'];
    window.addEventListener('message', (event) => {
      if (allowedOrigins.includes(event.origin)) {
        console.log('Received from allowed origin: ' + event.origin);
        // do something with event.data
      }
    });

    如果你的白名单漏掉了某些可信的域名,或者包含了错误的域名,也会导致安全问题。

3. 利用 Null Origin

有些情况下,event.origin 可能是 null。 比如,当页面从 file:// 协议打开时,或者当 PostMessage 的发送者是 data: URL 时。

window.addEventListener('message', (event) => {
  if (event.origin === 'null') {
    console.log('Received from null origin');
    // do something with event.data
  }
});

如果你的代码允许 null origin,攻击者就可以通过本地文件或者 data: URL 发送恶意消息。

4. 协议绕过

某些情况下,开发者只验证了域名,而忽略了协议。

const allowedDomain = 'example.com';
window.addEventListener('message', (event) => {
  if (new URL(event.origin).hostname === allowedDomain) {
    console.log('Received from allowed domain: ' + event.origin);
    // do something with event.data
  }
});

这段代码只验证了域名是 example.com,而没有验证协议是 http 还是 https。 攻击者可以通过 http://example.com 发送消息,绕过 HTTPS 的安全保护。

5. Content Security Policy (CSP) 绕过

即使你设置了 CSP,也可能存在绕过 PostMessage 的情况。 例如,如果你的 CSP 允许 unsafe-inline,攻击者就可以通过 PostMessage 注入恶意脚本。

攻击示例

假设A网站 (http://a.example.com) 有一个功能,允许用户修改个人资料。 用户提交的资料通过 PostMessage 发送给一个嵌入在A网站的iframe,这个iframe来自B网站 (http://b.example.com)。 B网站负责处理用户资料,并更新数据库。

<!-- A网站 (http://a.example.com) -->
<iframe src="http://b.example.com/profile.html"></iframe>
<script>
  const iframe = document.querySelector('iframe');
  document.getElementById('updateButton').addEventListener('click', () => {
    const name = document.getElementById('name').value;
    const email = document.getElementById('email').value;
    iframe.contentWindow.postMessage({ name: name, email: email }, 'http://b.example.com');
  });
</script>

<!-- B网站 (http://b.example.com/profile.html) -->
<script>
  window.addEventListener('message', (event) => {
    // 错误的 origin 验证!
    if (event.origin !== 'http://a.example.com') {
      return;
    }
    const data = event.data;
    console.log('Received profile data:', data);
    // 处理用户资料,更新数据库
    // ...
  });
</script>

这段代码看起来很正常,A网站发送用户资料给B网站,B网站验证 event.originhttp://a.example.com。 但是,如果B网站的验证逻辑存在漏洞,攻击者就可以伪造 event.origin,发送恶意数据。

例如,攻击者可以创建一个恶意网站 (http://evil.com),包含以下代码

<!-- 恶意网站 (http://evil.com) -->
<iframe src="http://b.example.com/profile.html"></iframe>
<script>
  const iframe = document.querySelector('iframe');
  iframe.onload = () => {
    // 伪造 origin,发送恶意数据
    iframe.contentWindow.postMessage({ name: '<script>alert("XSS")</script>', email: '[email protected]' }, 'http://a.example.com');
  };
</script>

这段代码首先加载 B网站的 profile.html,然后伪造 event.originhttp://a.example.com,发送包含 XSS 攻击的恶意数据。 由于B网站的验证逻辑存在漏洞,恶意数据被成功接收,并可能导致 XSS 攻击。

如何防范?

防止 PostMessage 跨域通信漏洞,需要做到以下几点:

1. 严格验证 Origin

这是最重要的一点! 必须严格验证 event.origin,确保消息来自可信的源。

  • 使用精确匹配

    const allowedOrigin = 'http://a.example.com';
    window.addEventListener('message', (event) => {
      if (event.origin === allowedOrigin) {
        console.log('Received from allowed origin: ' + event.origin);
        // 安全地处理 event.data
      } else {
        console.warn('Received message from untrusted origin: ' + event.origin);
      }
    });

    使用 === 进行精确匹配,确保 event.originallowedOrigin 完全一致。

  • 使用 Origin 白名单

    const allowedOrigins = ['http://a.example.com', 'https://a.example.com', 'http://b.example.com'];
    window.addEventListener('message', (event) => {
      if (allowedOrigins.includes(event.origin)) {
        console.log('Received from allowed origin: ' + event.origin);
        // 安全地处理 event.data
      } else {
        console.warn('Received message from untrusted origin: ' + event.origin);
      }
    });

    维护一个可信的 Origin 白名单,只允许白名单中的域名发送消息。

  • 使用 URL 对象进行验证

    const allowedOrigin = 'https://a.example.com';
    window.addEventListener('message', (event) => {
      try {
        const originURL = new URL(event.origin);
        if (originURL.protocol === 'https:' && originURL.hostname === 'a.example.com') {
          console.log('Received from allowed origin: ' + event.origin);
          // 安全地处理 event.data
        } else {
          console.warn('Received message from untrusted origin: ' + event.origin);
        }
      } catch (e) {
        console.error('Invalid origin: ' + event.origin);
      }
    });

    使用 URL 对象解析 event.origin,并验证协议和域名,防止协议绕过。

2. 验证 Message 的格式和内容

除了验证 event.origin,还要验证 event.data 的格式和内容,防止恶意数据注入。

  • 使用 JSON Schema 验证数据格式

    const schema = {
      type: 'object',
      properties: {
        name: { type: 'string' },
        email: { type: 'string', format: 'email' }
      },
      required: ['name', 'email']
    };
    
    window.addEventListener('message', (event) => {
      if (event.origin === 'http://a.example.com') {
        try {
          const data = JSON.parse(event.data);
          const valid = Ajv.validate(schema, data); // 使用 Ajv 库进行验证
          if (valid) {
            console.log('Received valid data:', data);
            // 安全地处理 data
          } else {
            console.error('Invalid data format:', Ajv.errors);
          }
        } catch (e) {
          console.error('Invalid JSON format: ' + event.data);
        }
      }
    });

    使用 JSON Schema 定义数据格式,并使用验证库(例如 Ajv)验证 event.data 是否符合规范。

  • 使用正则表达式验证数据内容

    const nameRegex = /^[a-zA-Z ]+$/;
    const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
    
    window.addEventListener('message', (event) => {
      if (event.origin === 'http://a.example.com') {
        const data = JSON.parse(event.data);
        if (nameRegex.test(data.name) && emailRegex.test(data.email)) {
          console.log('Received valid data:', data);
          // 安全地处理 data
        } else {
          console.error('Invalid data content:', data);
        }
      }
    });

    使用正则表达式验证 event.data 的内容,确保数据符合预期格式。

3. 避免使用 eval()Function()

永远不要使用 eval()Function() 来处理 PostMessage 收到的数据,因为它们可以执行任意代码。

// 错误示范!
window.addEventListener('message', (event) => {
  // 极其危险!
  eval(event.data);
});

4. 使用 Content Security Policy (CSP)

CSP 可以限制页面可以加载的资源,以及可以执行的脚本,从而减少 XSS 攻击的风险。

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">

这个 CSP 策略只允许从同源加载资源和执行脚本,可以有效防止 XSS 攻击。

5. 使用 Subresource Integrity (SRI)

SRI 可以验证从 CDN 加载的资源的完整性,防止 CDN 被攻击后,你的网站也被感染。

<script src="https://cdn.example.com/script.js" integrity="sha384-..." crossorigin="anonymous"></script>

6. 最小化 PostMessage 的使用

尽量减少 PostMessage 的使用,如果可以用其他方式实现跨域通信,尽量选择更安全的方式。 例如,可以使用 CORS 或者 JSONP。

7. 安全审查和代码审计

定期进行安全审查和代码审计,查找潜在的安全漏洞。

总结

PostMessage 是一个强大的跨域通信工具,但如果不小心使用,也可能导致严重的安全问题。 记住,安全是一个持续的过程,需要不断学习和实践。

防御措施 描述
严格验证 Origin 使用精确匹配、Origin 白名单或者 URL 对象进行验证,确保消息来自可信的源。
验证 Message 的格式和内容 使用 JSON Schema 或正则表达式验证数据格式和内容,防止恶意数据注入。
避免使用 eval()Function() 永远不要使用 eval()Function() 来处理 PostMessage 收到的数据。
使用 Content Security Policy (CSP) 限制页面可以加载的资源和执行的脚本,减少 XSS 攻击的风险。
使用 Subresource Integrity (SRI) 验证从 CDN 加载的资源的完整性,防止 CDN 被攻击后,你的网站也被感染。
最小化 PostMessage 的使用 尽量减少 PostMessage 的使用,如果可以用其他方式实现跨域通信,尽量选择更安全的方式。
安全审查和代码审计 定期进行安全审查和代码审计,查找潜在的安全漏洞。

希望今天的讲座能帮助大家更好地理解 PostMessage 跨域通信漏洞,并采取有效的措施进行防范。 记住,安全无小事,多一份小心,少一份风险! 感谢大家的收听,我们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注