震惊!由 Referer 引发的一桩“惨案”?

案件起因

震惊!全员震惊😱!一位程序员哈某举报有人偷走了他两天两夜的时间,经过我们24小时的跟踪暗访,嫌疑人竟是 Mr.Referrer ?请看详细报道:

前端同事的一个 jsp 资源A,通过浏览器去访问第三方业务的某个资源B,资源B会做一个 login 认证请求,如果 login 成功则返回资源B的详细页面信息,反之进入 ErrorPage。在测试环境中,login 是可以成功的;但是在生产环境中,login 请求提示了如下错误:

Provisional headers are shown.

直译是:显示了临时的(暂定的)标题。经抓包查看网络状况是:HTTP Code 200 OK,但是 Response 是空的,更像是浏览器主动取消了这个请求,Google 和 Safari 都是一样的现象。

哈某排查了许久也没有定位到原因,面临客户提出的紧急上线,情况很是焦灼。

FYI: 不想听我讲废话的话,请直接跳转至 Referer 😂

分析过程

1.谷歌一下

相信大部分程序员遇到问题时,第一个排查步骤都是施展 CV 大法粘到谷歌搜索一下。同样,我们和哈某也采取了这一步,搜索 Provisional headers are shown 得到的结果都大同小异。以下几个原因粘贴自这里

具体原因有多种总结如下:
1.请求被某些扩展如 Adblock 拦截了,请求被浏览器插件拦截。解决方案:用 chrome://net-internals 来帮助你查找被屏蔽的请求以及可能的原因。(本人发现现在这个用不了了);
2.请求被墙了;
3.走本地缓存或者 dataurl 的请求。强缓存 from disk cache 或者 from memory cache,此时也不会显示;
4.服务器未及时响应(超时);
5.跨域,请求被浏览器拦截;
6.其他原因

这里的原因6,我们就当他在钻研废话文学吧……总而言之,网友们提供的这几个原因,经尝试,依旧没有定位到问题。

2.检查 Nginx 配置

经过上面“专业”的排查~依旧懵逼。随后哈某联系了服务端的同事,想比对一下测试环境、生产环境中 Nginx 的配置是否有区别,燃鹅并没有区别。为了避开 Nginx 存在的未知干扰,从内网访问资源B,login 的操作显示 404 🙄……

二度懵逼~😳

3.沟通

通过专业手段没有定位到原因,只能尝试和第三方业务的人沟通了。按照我们大部门的管理理念,没有什么事情是沟通解决不了的。

很不幸的是,对端的人只反馈给我们 login 请求并没有到达他们那儿。听到这个回答笑哭了有没有~用哈某的话说:“我都快看他内裤了,还不承认”,今天凌晨躺在床上帮他们分析这个问题时(其实我不懂后端,就是跟着瞎掺和顺便学习新知识),被这句话逗笑了,也算是通宵加班的一个慰藉了。😂

4.Charles

通过对 App 抓包,login 请求同样显示 HTTP Code 200 OK,但是 Response 是空的。但是跟 Google 浏览器中 NetWork 调试器的表现不太一样。

5.cURL

为了排除浏览器对 Response 可能存在特殊的解析规则,亦或者服务器针对浏览器发出的请求做一些限制,可以通过 cURL 作网络请求。cURL入门学习:阮一峰-curl 的用法指南

通过 cURL 访问资源B以及 login 请求,Ummmmmmmm,请求是成功的!经过一位”神棍”的提点,我们从 Google 浏览器中选中目标请求 -> 右击 -> Copy -> Copy as cURL:

比对一下和自己使用 cURL 写的请求区别在哪里,结果如下:

  • 页面关系

    1
    2
    3
    1、先访问资源A: https://hhh.com:0000/aaa/test.jsp
    2、由1跳转到资源B: https://hhh.com:0000/aaa/index.html
    3、由2发起 login 认证请求 https://hhh.com:0000/aaa/loginAction.do
  • Google 发出的请求(失败的情况)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    curl 'https://hhh.com:0000/aaa/loginAction.do' \
    -H 'Connection: keep-alive' \
    -H 'Pragma: no-cache' \
    -H 'Cache-Control: no-cache' \
    -H 'sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96"' \
    -H 'Accept: application/json, text/plain, */*' \
    -H 'sec-ch-ua-mobile: ?0' \
    -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36' \
    -H 'sec-ch-ua-platform: "macOS"' \
    -H 'Sec-Fetch-Site: same-origin' \
    -H 'Sec-Fetch-Mode: cors' \
    -H 'Sec-Fetch-Dest: empty' \
    -H 'Referer: https://hhh.com:0000/aaa/index.html' \
    -H 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8' \
    -H 'Cookie: JSESSIONID=68ED14BCB51A01E5DA08FA2009333BA2' \
    --compressed -i
  • 自己发的 cURL 请求(成功的情况)

    1
    curl 'https://hhh.com:0000/aaa/loginAction.do' -i

所以区别在哪里呢?header !

6.破案

经过修改几个主要的 header 信息,最终确定了真凶就是 Referer 字段。去掉 Referer 之后页面可以正常访问了。无奈由于资源B是三方系统,我们无法要求对方修改代码,采用了临时的解决方案:

修改 DMZ 区 Nginx 的配置,所有经过这台 Nginx 的请求都不携带 Referer 头部。

1
add_header Referrer-Policy no-referrer;

所以 Provisional headers are shown 翻译为”临时的头部信息“更为贴切对吗?不知道是否因为 Referer 涉及用户隐私的问题。

Referrer or Referer ?

Referer 是 HTTP 请求头里一个常见的字段,包含了当前请求页面的来源页面的地址,即表示当前页面是通过此来源页面里的链接进入的。服务端一般使用 Referer 请求头识别访问来源,可能会以此进行统计分析、日志记录以及缓存优化等。

1.Referer 的含义

现实生活中,你一定填过这样的问卷调查:”你从哪里知道了我们?”。这叫做引荐人(referrer),谁引荐了你?对于公司来说,这是很有用的信息。互联网也是一样,你不会无缘无故访问一个网页,总是有人告诉你,可以去那里看看。服务器也想知道,你的 “引荐人” 是谁?

HTTP 协议在请求(request)的头信息里面,设计了一个 Referer 字段,给出 “引荐网页” 的 URL。这个字段是可选的。客户端发送请求的时候,自主决定是否加上该字段。

很有趣的是,这个字段的拼写是错的。Referer 的正确拼写是 Referrer,但是写入标准的时候,不知为何,没人发现少了一个字母r。标准定案以后,只能将错就错,所有头信息的该字段都一律错误拼写成 Referer。Nginx 采用的是正确拼写~

2.Referer 的发生场景

浏览器向服务器请求资源的时候,Referer 字段的逻辑是这样的,用户在地址栏输入网址,或者选中浏览器书签,就不发送 Referer 字段。

主要是以下三种场景,会发送 Referer 字段:

(1)用户点击网页上的链接。

(2)用户发送表单。

(3)网页加载静态资源,比如加载图片、脚本、样式。

1
2
3
4
5
6
<!-- 加载图片 -->
<img src="foo.jpg">
<!-- 加载脚本 -->
<script src="foo.js"></script>
<!-- 加载样式 -->
<link href="foo.css" rel="stylesheet">

上面这些场景,浏览器都会将当前网址作为 Referer 字段,放在 HTTP 请求的头信息发送。

浏览器的 JavaScript 引擎提供 document.referrer 属性,可以查看当前页面的引荐来源。注意,这里采用的是正确拼写。

3.Referer 的作用

Referer 字段实际上告诉了服务器,用户在访问当前资源之前的位置。这往往可以用来用户跟踪。一个典型的应用是,有些网站不允许图片外链,只有自家的网站才能显示图片,外部网站加载图片就会报错。它的实现就是基于 Referer 字段,如果该字段的网址是自家网址,就放行。

由于涉及隐私,很多时候不适合发送 Referer 字段。这里举两个例子,都不适合暴露 URL。一个是功能 URL,即有的 URL 不要登录,可以访问,就能直接完成密码重置、邮件退订等功能。另一个是内网 URL,不希望外部用户知道内网有这样的地址。Referer 字段很可能把这些 URL 暴露出去。

此外,还有一种特殊情况,需要定制 Referer 字段。比如社交网站上,用户在对话中提到某个网址。这时,不希望暴露用户所在的原始网址,但是可以暴露社交网站的域名,让对方知道,是我贡献了你的流量。

4.rel属性

由于上一节的原因,浏览器提供一系列手段,允许改变默认的 Referer 行为。

对于开发者来说,rel="noreferrer" 属性是最简单的一种方法。<a><area><form> 三个标签可以使用这个属性,一旦使用,该元素就不会发送Referer字段。

1
<a href="..." rel="noreferrer" target="_blank">xxx</a>

上面链接点击产生的 HTTP 请求,不会带有 Referer 字段。

注意,rel="noreferrer" 采用的是正确的拼写。

5.Referrer Policy 的值

rel 属性只能定制单个元素的 Referer 行为,而且选择比较少,只能发送或不发送。W3C 为此制定了更强大的 Referrer Policy

Referrer Policy 可以设定 8 个值:

(1)no-referrer

不发送 Referer 字段。

(2)no-referrer-when-downgrade

如果从 HTTPS 网址链接到 HTTP 网址,不发送 Referer 字段,其他情况发送(包括 HTTP 网址链接到 HTTP 网址)。这是浏览器的默认行为。

(3)same-origin

链接到同源网址(协议 + 域名 + 端口 都相同)时发送,否则不发送。注意,https://foo.com 链接到 http://foo.com 也属于跨域。

(4)origin

Referer 字段一律只发送源信息(协议 + 域名 + 端口),不管是否跨域。

(5)strict-origin

如果从 HTTPS 网址链接到 HTTP 网址,不发送 Referer 字段,其他情况只发送源信息。

(6)origin-when-cross-origin

同源时,发送完整的 Referer 字段,跨域时发送源信息。

(7)strict-origin-when-cross-origin

同源时,发送完整的 Referer 字段;跨域时,如果 HTTPS 网址链接到 HTTP 网址,不发送 Referer 字段,否则发送源信息。

(8)unsafe-url

Referer 字段包含源信息、路径和查询字符串,不包含锚点、用户名和密码。

6.Referrer Policy 的用法

Referrer Policy 有多种使用方法

(1)HTTP 头信息

服务器发送网页的时候,通过 HTTP 头信息的 Referrer-Policy 告诉浏览器。

1
Referrer-Policy: origin

(2)<meta> 标签

也可以使用 <meta> 标签,在网页头部设置。

1
<meta name="referrer" content="origin">

(3)referrerpolicy 属性

<a><area><img><iframe><link> 标签,可以设置 referrerpolicy 属性。

1
<a href="..." referrerpolicy="origin" target="_blank">xxx</a>

PS:更多内容见 参考1-MDN参考2-阮一峰

疑惑

资源B的测试环境同样有 Referer 头部,但所有网络请求都是没问题的。虽然这次通过修改 Nginx 的配置临时解决了问题,但生产环境上为何存在这个问题,根本原因依旧没搞清楚。同事也咨询了对方是否针对 Referer 做了类似防盗链的处理,得到的答案是否定的。所以可能是哪里的问题呢?😳