Skip to content

rrweb 录制 canvas 被污染

背景

团队内部自研的前端监控 sdk 有对错误进行录屏,这个录屏采用的是 rrweb,并且开启了对 canvas 的录屏。最近接到 2 个反馈说,由于监控 sdk 的原因导致应用本身的某个按钮不可点击。经过排查,这些项目的共性都是使用了 canvas。由此便第一时间便怀疑 rrweb 录屏导致的。

具体报错信息

测试环境报错信息如下:

error

这个错误会不停地报,导致页面里的按钮被点击后没有响应。

浏览器里定位到的代码位置如下:

browser-source

源码定位

rrweb 对应的源码如下:

rrweb-source

因为 rrweb 会一直录屏,它会不停地调用 is2DCanvasBlank 方法去判断 2d canvas 是否空白,结果就是因为源 canvas 图像被污染了,导致一直报错。

原因分析

当使用 drawImage 方法绘制一个不同源的图像时,此时并不会报错,但是 canvas 会变成 tainted(被污染),之后如果在当前被污染的 canvas 上调用以下方法时就会抛出 SecurityError 的错误。

API

  • HTMLCanvasElement.toDataURL()
  • HTMLCanvasElement.toBlob()
  • CanvasRenderingContext2D.getImageData()

CAUTION

  • Uncaught SecurityError: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.
  • Uncaught SecurityError: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data.

在源码里能看到 rrweb 有通过对 canvas 内容进行提取,最后导致了这个错误。 那么问题的根源在于加载图片的时候没有设置跨域,图片本身的响应头如下:

text
access-control-allow-methods: GET, POST, OPTIONS
access-control-allow-origin: *

即图片资源本身来源于 CDN,默认都是支持了 CORS 的,那么这个问题就在于渲染的时候缺乏对应设置了。

解决方案

方案一:设置图片 crossOrigin

使用 fabric 加载图片的时候,设置图片 crossOrigin 属性:

js
fabric.Image.fromURL('https://example.com/image.png', function(img) {
    img.set({
        crossOrigin: 'anonymous',
    });
    canvas.add(img);
}, { crossOrigin: 'anonymous' })

刚开始没有设置第三个配置项,结果发现并没有生效。后来查看了 fabric 源码,fromURL 方法定义如下:

fromURL

然而官方文档里并没有说明这第三个参数:

fromURL-usage

我们在回调函数里去设置图片的 crossOrigin,已经没有意义了,需要在加载图片之前进行设置,否则就会被污染。继续查看 loadImage 源码就可以看到:

loadImage

方案二:将图片转为 base64

loadImage 源码里有这么一段判断:

js
      // data-urls appear to be buggy with crossOrigin
      // https://github.com/kangax/fabric.js/commit/d0abb90f1cd5c5ef9d2a94d3fb21a22330da3e0a#commitcomment-4513767
      // see https://code.google.com/p/chromium/issues/detail?id=315152
      //     https://bugzilla.mozilla.org/show_bug.cgi?id=935069
      // crossOrigin null is the same as not set.
      if (url.indexOf('data') !== 0 &&
        crossOrigin !== undefined &&
        crossOrigin !== null) {
        img.crossOrigin = crossOrigin;
      }

只有 url 不是 data-url 格式的才会给加上 crossOrigin 属性,因为 data-url 格式的图像同源不需要跨域,此时就不会出现图像被污染这种情况。