跳到主要内容

canvas 污染问题

· 阅读需 10 分钟
Oxygen

背景

前端时间实现了一个用canvas往模板图片绘制数据的功能点,遇到了一个跨域引起canvas污染的问题,仔细发掘下去发现不少的技术点。

tainted canvas

对于canvas污染的问题,这应该是非常常见的在处理跨域资源时会遇到的问题。

一般来说,利用canvas绘制图像的时候需要执行以下步骤:

  1. 创建canvas元素;
  2. 获取canvas元素的二维渲染上下文对象;
  3. 创建Image对象并加载;
  4. 等待图像加载完成的时候绘制在canvas
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");

const img = new Image();
img.src = 'xxxx';
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;

canvas.drawImage(img, 0, 0);
}

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

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

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.

为什么会出现 tainted canvas

tainted canvas 其实还是牵扯到浏览器同源策略的限制问题,一般来说这个问题在使用XMLHttpRequest或者Fetch发起网络请求的情况比较多见,而在这里的目的是禁止使用canvas随意从另一个网站加载图片再转换成数据的强盗行为。

这看起来合情合理,但是现在大型网站一般都会走 CDN 服务器来缓存并代理资源访问,这就导致在动态加载图像的时候实际可能走的是 CDN 服务器域名,这就导致网页域名和资源域名不同源了。本来防别人的,这下连自己人也堵在外面了。

如何解决 tainted canvas 问题

要解决 canvas 污染的问题,需要 CORS + crossOrign 两步配置:

  1. 配置服务端响应头支持跨域请求的域名,请求方法等;
  2. 设置crossorigin属性

cors header 配置

关于服务端 CORS 的配置就不细说了,具体的可以看我的这篇文章:

提示

跨域直通车 —— 跨域与解决方案 | icodex

crossorigin

HTML 规范给crossorigin制定了三个允许值

  • anonymous""(空字符串):匿名请求访问资源,不会在跨域请求的时候携带任何身份凭据;
  • use-credentials:在跨域请求的时候携带身份凭据,仅当服务端响应头返回Access-Control-Allow-Credentials: true的时候才允许使用跨域资源

crossorigin无论设置成哪一个值都会指定浏览器以跨域的模式请求资源,去发送相关CORS相关的请求头,并通过检查服务端是否返回Access-Control-Allow-Orgin等响应头来判断用户是否有权限完全访问响应内容。但是为了客户端安全考虑,一般设置成anonymous更为合适,避免向陌生的服务端发送网页的cookie等身份数据。

放在canvas绘制image的代码里可以这样修改:

const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");

const img = new Image();
img.src = 'xxxx';
img.crossOrigin = 'anonymous';
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
canvas.drawImage(img, 0, 0);
}

如果是img标签可以增加crossorigin属性:

<img src="xxx" crossorigin="anonymous" />

以下资源可能也需要crossorigin来取得数据访问权:

元素限制
img, audio, video当它们被放在canvas元素内部使用时
script使用 window.onerror
link加载webmanifest时必须添加crossorigin属性