如何判断用户是否访问过某个网址

我们经常有这样的需求:想知道用户之前有没有访问过某个网址。有没有什么方法或技术能实现这一点呢?

初步探索

注意到,在大部分浏览器默认设置里,用户访问过的链接和没访问过的链接颜色是不同的,如下图:

即用户访问过的链接,computed color 默认为紫色(或其他在 CSS 中指定的颜色):

而没访问过的链接,computed color 默认为蓝色(或其他在 CSS 中指定的颜色):

那是不是说,我们可以在页面上加上我们感兴趣的链接,然后用 JavaScript 取得这些链接文本实际的颜色,即可知道用户是否访问过指定网址呢?

这个方法真的有效过。2010 年有一篇安全文章上即介绍了这种方法,并将这类方法称为“历史嗅探”(history sniffing)。

遗憾的是,各大浏览器厂商都已经注意到了这个问题,根据我的测试,目前最新的浏览器中都对这个问题进行了防范,获取超链接的 Computed Style 时,无论这个链接是否被访问过,取得的颜色都是未访问过时的那种颜色。

看起来根据颜色获取的方案目前行不通了,不过神奇的是,现在我们有了另外一种方案。

requestAnimationFrame

Context Information Security 公司最近出了一份名为《PIXEL PERFECT TIMING ATTACKS WITH HTML5》的报告,其中提到了一种很有创意的方案:使用 HTML5 中的 requestAnimationFrame,根据浏览器渲染已访问过及未访问过的链接的时间差,判断指定链接是否访问过。

现代浏览器绘图时每一帧的流程大致如下图所示:

大致流程为:JS 修改某个元素的样式,浏览器重新计算对应元素的外观及位置,然后将它们绘制出来,这个过程即是一帧。而 requestAnimationFrame 的作用则是可以注册一个函数,在下一帧开始绘制之前进行调用。

requestAnimationFrame 的初衷是让开发人员可以更好地管理动画,绘制更平滑的动画,如这篇博客中所说的。不过,这个接口也让获取不同元素的渲染时间成为了可能。

工作原理

在开始之前,我们需要了解的是浏览器是如何渲染访问过的链接和未访问过的链接的。

当浏览器渲染一个页面时,浏览器必须区分出某个链接是否曾访问过。每个浏览器都有一个记录访问过的链接的数据库,此时它要做的就是从这个数据库中查询指定的 URL 是否存在。

IE 与 Firefox 中,如果链接已经渲染到页面上了,查询还没完成,浏览器会先使用“未访问过”的样式来渲染,查询结果返回时,如果指定链接是已访问过,那么浏览器就重绘指定的链接。而这个“重绘”是需要时间的,可以使用 requestAnimationFrame 来监测。

Chrome 浏览器和 Firefox、IE 不同,它会一直等到数据库 URL 查询完成才将链接渲染到屏幕上。

除了初始渲染之外,使用 JavaScript 修改链接的 href 也有可能引发浏览器重绘。测试显示,在 Firefox 中,修改一个链接的 href,如果改变了它的“已访问”状态,则会引发重绘。但这 IE 中无效,一个链接一旦创建,改变 href 永远也不会同时改变它的“已访问”状态。

Chrome 中有点例外,只改变 href 并不会引发重绘,不过,如果在改变 href 的同时也“触碰”一下链接的样式(但不修改),则当新 href 需要改变“已访问”状态时,浏览器会重绘对应的链接。

所谓的“触碰”指的是类似这样的操作:

<a href="http://www.google.com" id="link1">############</a>

<script>
	var el = document.getElementById('link1'); 
	el.href = 'http://www.yahoo.com'; 

	// below lines are required for Chrome to force style update 
	el.style.color = 'red'; 
	el.style.color = ''; 
</script>

简单来说,基本原理就是这样:创建链接,改变它的 href,使用 requestAnimationFrame 来监测接下来若干帧的耗时,判断是否发生了重绘,如果发生了重绘,说明指定链接的“已访问”状态发生了变化,即可判断出指定链接是否被访问过。

当然,实际操作过程中还有不少问题需要考虑,比如,浏览器渲染通常都非常快,重绘的时间可能也会非常短,导致完全无法区分。解决方案主要有两个,一是增加链接数,创建多个 A 链接,指向同一个 URL,需要时使用 JS 同时改变这些 A 链接的 href 属性为另一个值。另一个方案是给元素加一些耗时的样式,比如文字阴影,并且让模糊半径尽可能大,这样在重绘时需要的时间就会多很多了。

实践

我写了一个针对 Chrome 浏览器的 demo,你可以使用 Chrome 访问 https://oldj.net/static/history-sniffing/test.html 测试。

这个测试需要一个已知的已访问过的 URL 作为基准链接,既然你在读这篇博客,并且可能会点击上面的测试链接,那我就把上面的那个测试链接作为基准链接了。接下来,依次测试各个链接,看是否会引发重绘,据此判断你是否曾经访问过指定链接。

效果如下图:

已访问过的链接除了会使用紫色显示(这是浏览器自带的功能)外,我还在前面加了一个“[v]”标记,未访问过的链接前面则是“[ ]”。另外,你也可以在下面的输入框中输入新的 URL,看看能不能正确判断。

原理即是上面提到的,修改链接地址后,根据接下来各帧的时间,判断是否发生了重绘(redraw),如果有,则认为链接的已访问状态发生了改变。

这是一个 Chrome 的例子,根据 contextis白皮书,稍加改造,即可适用于 IE 及 Firefox。

更多

这种通过判断渲染时间来获取信息的方式非常可怕,它的强大之处在于可以获取第三方网站的访问记录,并且完全不需要用户知情。比如,可以通过这个方式了解用户是否访问过竞争对手的网站,或者了解用户是否访问过指定的限制级网站。

Princeton 大学一篇 2000 年的论文《Timing Attacks on Web Privacy》就已经指出了这类计时攻击可能会泄露用户隐私。这篇论文里也提出了一种判断用户是否访问过指定网址的方法,做法是在当前页面加载指定网站的固定资源(访问过那个网站的用户都会下载的资源),比如 logo 或 js 文件,如果用户之前访问过,则浏览器会从本地缓存中读取对应的资源,速度会快很多,否则会重新下载。使用 JavaScript 或 Java applet(2000 年的论文,那时 Java applet 还很流行)可以度量这个时间,进而判断用户是否曾访问过指定网址。

这个十几年前提出的方案目前仍然是有效的,不过相对来说,本文中提出的方案更为先进,效率也更高。

另外,除了客户端时间外,服务器端时间也有可能泄露意想不到的数据。比如来自 Stanford 的论文《Exposing Private Information by Timing Web Applications》中提到,由于后台在处理不同类型的数据时使用的逻辑不同,耗费的时间也不同,因此,只要构造适当的请求,反复执行并度量时间,就可能获得很多隐私信息。

如何防范此类计时攻击呢?目前似乎没有很好的办法。对服务器端来说,最好的方案或许是使用一些手段,让所有请求的返回时间常量化,比如统一为 500 毫秒,如果某个请求早于 500 毫秒完成,就休眠一会儿,直到 500 毫秒时再返回。但这样的问题是,整个站点的响应速度将取决于最慢的那个页面,对于效率至上的团队来说,这可能是无法忍受的。如果不是常量化,而是在每个请求之后休眠一段随机的时间也是不行的,因为完全随机意味着统计学上有迹可循,只要有足够多的样本,攻击者完全可以将随机因素剔除出去。

对客户端的计时攻击来说,防范的办法就更少了,或许只有等待浏览器厂商注意到这类问题,并对浏览器特性做出修改,就像无法取得已访问链接的 computed color 一样。

参考链接

分类:编程标签:HTML5JavaScript

相关文章:

评论:

tcdona

这个方法真的太奇葩啦

lovelucy

Win7 Chrome 28.0.1500.95 m
测试无效

oldj

奇怪...
我添加了渲染后10帧的时间显示,可否刷新下再截个图?谢谢!

Sign

不知道no-cache是否不保存history,因为隐身模式下就无效了,毕竟不输出不保留history。

oldj

cache和history应该是独立的,不缓存,仍然会记录访问历史吧?

独孤逸辰

有创意!!

MatheMatrix

确实有创意……也很可怕 不过我在WindowsXP SP3下测试Chrome29有时有效有时失效,Opera15下总有效,可能与我用VPN有关吧

诸葛不亮

泪奔,经济学IT男。。

oldj

T_T

诸葛不亮

我也是经济学IT男哦,碰到个同类真难得。

bombless

直接根据:visited指定样式然后查看样式这个浏览器应该挡不住吧……

Leo

我靠这招好狠啊……

riophae

思路真不错, 让我小小地震惊了一下. 不过在 Chrome 32.0.1700.76 / Win 7 64-bit 下测试不通过.
截图在这里:
http://ww3.sinaimg.cn/large/3e545ebbtw1ecyg8a3b0bj20wy0b7acb.jpg

oldj

仔细看你的截图,链接从已访问到未访问或从未访问到已访问之间切换时,redraw的时间还是有较明显的特征的,应该是我的demo里的判断逻辑写得太简单了。:)

卢林

看完很有收获,怒赞!

Xj

厉害,看来只要浏览器要做了已访问和未访问的记录,无论如何防范,总有被取到的风险。

today

作者能联系下吗?我在Firefox和Chrome测试都可以,在IE上打开是空白,能付费帮我写一个也适用于IE的吗

oldj

这个方法用到了HTML5、CSS3的特性,在老版本IE上实现不了呢。

today

这样呀,那能帮我写个支持IE的新版本的检测吗?649964576这是我的Q

如何判断用户是否访问过某个网址 | 歪布IT笔记

[…] 如何判断用户是否访问过某个网址 […]

LinTFunnny

从b站来的,发现这位博主13年写下的这篇文和demo,厉害,又是发现宝藏blog站的一天
(博主,你的小博客站要火啦,b站视频链接 https://www.bilibili.com/video/BV1sD4y187Kp

oldj

我说怎么最近几天访问量变多了,原来是这么回事。感谢介绍!😄

发表评论: