TamperMonkey 对于内容的保护

前段时间做了个油猴插件,把项目组常用的项目在不同环境的url给整理一下,方便使用。
最近又被各个项目在各个环境的db配置搞得有点烦,就想着把db的配置也放进脚本里,但是这些db配置中还有些敏感的信息,贸然放进去可能会有安全的问题,为了研究一下TamperMonkey的安全性,就研究了下TamperMonkey的文档以及它的一点原理。

独立的运行环境

首先粘一段国外论坛上的结论:

See “Are Chrome user-scripts separated from the global namespace like Greasemonkey scripts?”. Both Chrome userscripts/content-scripts and Greasemonkey scripts are isolated from the page’s javascript. This is done to help keep you from being hacked, but it also reduces conflicts and unexpected side-effects.

However, the methods are different for each browser…

Firefox:

  1. Runs scripts in an XPCNativeWrapper sandbox, unless @grant none is in effect (as of GM 1.0).
  2. Wraps the script in an anonymous function by default.
  3. Provides unsafeWindow to access the target page’s javascript. But beware that it is possible for hostile webmasters to follow unsafeWindow usage back to the script’s context and thus gain elevated privileges to pwn you with.

Chrome:

  1. Runs scripts in an “isolated world”.

  2. Wraps the script in an anonymous function.

  3. Strictly blocks any access to the page’s JS by the script and vice-versa.

    Recent versions of Chrome now provide an object named unsafeWindow, for very-limited compatibility, but this object does not provide any access to the target page’s JS. It is the same as window in the script scope (which is not window in the page scope).

上面这段结论大致可以总结为,TamperMonkey的运行环境与页面的JS脚本不是一个,TamperMonkey中的脚本运行在一个沙箱环境中,但是二者都可以操作页面。

沙箱的环境也是有所不同的,如果你的@grant的值是none,那么如果你向window对象上挂载了一些属性,其他脚本理论上也是可以访问到的。

但如果你的@grant不是none,那么就连window对象都是分开的,你在脚本中挂载的属性在其他脚本中是无法访问的。

闭包

我们看一下TamperMonkey脚本的基本格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ==UserScript==
// @name New Userscript
// @namespace http://tampermonkey.net/
// @version 0.1
// @description try to take over the world!
// @author You
// @match https://developer.mozilla.org/en-US/docs/Mozilla/Tech/Xray_vision
// @grant none
// ==/UserScript==

(function() {
'use strict';

// Your code here...
})();

我们可以一眼就看出,这是个闭包,而且是个立即执行函数,所以定义在其中所有变量,除非我们有意识地挂载到window上,否则在外部是无法访问地。

结论

综上所述,TamperMonkey中的脚本是运行在完全独立于page的JS的另一个环境中,各个脚本之间不会相互影响,同时由于脚本的实际内容都是在一个闭包中,只有在grant是none且我们有意识地向window对象上挂载一个属性,不然这个属性是不会被外部访问到的。

拓展

Chrome Xray

Gecko从各种不同的来源并以各种不同的特权级别运行JavaScript:

  • 与C ++核心一起实现浏览器本身的JavaScript代码称为chrome代码,并使用系统特权运行。 如果chrome特权代码遭到破坏,则攻击者可以接管用户的计算机。

  • 从普通网页加载的JavaScript称为内容代码。 因为此代码是从任意网页加载的,所以对于其他网站和用户而言,它均被视为不受信任且可能具有敌意。

  • 除了这两个特权级别,chrome代码还可以创建沙箱。 为沙箱定义的安全主体确定其特权级别。 如果使用扩展主体,则沙箱将获得对内容代码的某些特权,并且受到内容代码的直接访问保护。

Gecko中的安全机制可确保不同特权级别的代码之间存在非对称访问:例如,内容代码无法访问由chrome代码创建的对象,但是chrome代码可以访问由内容创建的对象。

但是,即使访问内容对象的能力也可能会对chrome代码造成安全风险。 JavaScript是一种高度可扩展的语言。 在网页中运行的脚本可以为DOM对象添加额外的属性(也称为expando属性),甚至可以重新定义标准的DOM对象以执行意外的操作。 如果chrome代码依赖于此类修改后的对象,则可以欺骗它做不应做的事情。

例如:window.confirm()是一个DOM API,应该让用户确认一个动作,并根据他们单击“确定”还是“取消”返回一个布尔值。 网页可以重新定义它以返回true:

任何调用此函数并期望其结果代表用户确认的特权代码都会被欺骗。 当然,这是非常幼稚的,但是从chrome访问内容对象会导致安全问题的方法更细微。

这是Xray旨在解决的问题。 当脚本使用Xray访问对象时,它只会看到该对象的本机版本。 任何expandos都是不可见的,并且如果已重新定义对象的任何属性,它将看到原始的实现,而不是重新定义的版本。

因此,在上面的示例中,调用内容的window.confirm()的chrome代码将获得Confirm()的原始版本,而不是重新定义的版本。

特权代码在访问属于特权较少的代码的对象时自动获得Xray。 因此,当chrome代码访问内容对象时,它会通过Xray看到它们:

1
2
3
// chrome code
var transfer = gBrowser.contentWindow.confirm("Transfer all my money?");
// calls the native implementation

禁止Xray

Xray是一种安全启发式设计,旨在使对不受信任的对象的最常见操作变得简单和安全。 但是,有些操作对它们的限制过于严格:例如,如果需要查看DOM对象上的expandos。 在这种情况下,您可以放弃Xray防护,但随后您将不再依赖于正在或正在执行的任何属性或功能。 其中的任何一个,甚至是setter和getter,都可能已被不受信任的代码重新定义。

要放弃对象的Xray,可以使用Components.utils.waiveXrays(object),也可以使用对象的wrappedJSObject属性

1
2
3
4
5
6
7
8
// chrome code
var waivedWindow = Components.utils.waiveXrays(gBrowser.contentWindow);
var transfer = waivedWindow.confirm("Transfer all my money?");
// calls the redefined implementation
// chrome code
var waivedWindow = gBrowser.contentWindow.wrappedJSObject;
var transfer = waivedWindow.confirm("Transfer all my money?");
// calls the redefined implementation

豁免是可传递的:因此,如果您放弃某个对象的Xray,则将自动放弃其所有对象属性。 例如,window.wrappedJSObject.document可让您放弃文档版本。

要再次撤消放弃,请调用Components.utils.unwaiveXrays(waivedObject):

Xray For DOM

Xray的主要用途是用于DOM对象:即代表网页各部分的对象。

在Gecko中,DOM对象具有双重表示形式:规范表示形式采用C ++,并且为了体现JavaScript代码的优势,这被反映到JavaScript中。 对这些对象的任何修改(例如添加expandos或重新定义标准属性)都保留在JavaScript反射中,并且不影响C ++表示形式。

双重表示形式实现了Xrays的优雅实现:Xray只是直接访问原始对象的C ++表示形式,而根本不涉及内容的JavaScript反射。 Xray不会完全过滤出内容所做的修改,而是将内容完全短路。

这也使Xrays对DOM对象的语义更加清晰:它们与DOM规范相同,因为它是使用WebIDL定义的,并且WebIDL还定义了C ++表示形式。

Xray For JavaScript Object

直到最近,通过特权更高的代码访问时,不属于DOM的内置JavaScript对象(例如Date,Error和Object)都无法获得Xray。

在大多数情况下,这不是问题:Xrays解决的主要问题是处理不受信任的Web内容操纵对象,并且Web内容通常与DOM对象一起工作。 例如,如果内容代码创建一个新的Date对象,则通常将其创建为DOM对象的属性,然后由DOM Xray过滤掉:

1
2
3
4
5
6
7
8
9
10
11
// content code

// redefine Date.getFullYear()
Date.prototype.getFullYear = function() {return 1000};
var date = new Date();
// chrome code

// contentWindow is an Xray, and date is an expando on contentWindow
// so date is filtered out
gBrowser.contentWindow.date.getFullYear()
// -> TypeError: gBrowser.contentWindow.date is undefined

Extensions

Extensions are made of different, but cohesive, components. Components can include background scripts, content scripts, an options page, UI elements and various logic files. Extension components are created with web development technologies: HTML, CSS, and JavaScript. An extension’s components will depend on its functionality and may not require every option

Architecture

An extension’s architecture will depend on its functionality, but many robust extensions will include multiple components:

Manifest

https://developer.chrome.com/extensions/manifest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"name": "Getting Started Example",
"version": "1.0",
"description": "Build an Extension!",
"permissions": ["activeTab", "declarativeContent", "storage"],
"options_page": "options.html",
"background": {
"scripts": ["background.js"],
"persistent": false
},
"content_scripts": [],
"page_action": {
"default_popup": "popup.html",
"default_icon": {
"16": "images/get_started16.png",
"32": "images/get_started32.png",
"48": "images/get_started48.png",
"128": "images/get_started128.png"
}
},
"icons": {
"16": "images/get_started16.png",
"32": "images/get_started32.png",
"48": "images/get_started48.png",
"128": "images/get_started128.png"
},
"manifest_version": 2
}

Content Script

Content script 是你扩展的一部分,运行于一个特定的网页环境(而并不是后台脚本,后台脚本是扩展的一部分,也不是该网页利用<script>加载的一个脚本,<script> 加载的脚本是网页的一部分)。

后台脚本可以访问所有WebExtension JavaScript APIS,但是他们不能直接访问网页的内容,所以如果你需要Content Scripts来做到这点。

就像通常的网页加载的脚本一样,Content Scripts 可以使用standard DOM APIS 读取和修改页面内容

Content Script 只能访问WebExtension APIS 的一个小的子集,但是它们可以使用通信系统与后台脚本进行通信,从而间接的访问WebExtension APIS。

DOM访问

Content scripts 可以访问和修改页面的DOM,就像普通的页面脚本一样。他们也可以察觉页面脚本对页面做出的任何修改。

不过,content scripts 得到的是一个“干净的DOM视图”,这意味着:

  • content scripts 不能看见页面脚本定义的javascript 变量。
  • 如果一个页面脚本重定义了一个DOM内置属性,content scripts将获取到这个属性的原始版本,而不是重定义版本。

在 Gecko, 这种行为被称为射线视觉(Xray)

举个例子,考虑一个网页如下:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>

<body>
<script src="page-scripts/page-script.js"></script>
</body>
</html>

脚本文件“page-script.js”如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// page-script.js

// add a new element to the DOM
var p = document.createElement("p");
p.textContent = "This paragraph was added by a page script.";
p.setAttribute("id", "page-script-para");
document.body.appendChild(p);

// define a new property on the window
window.foo = "This global variable was added by a page script";

// redefine the built-in window.confirm() function
window.confirm = function() {
alert("The page script has also redefined 'confirm'");
}

现在一个扩展插入一个content script 如下:

1
2
3
4
5
6
7
8
9
10
11
// content-script.js

// can access and modify the DOM
var pageScriptPara = document.getElementById("page-script-para");
pageScriptPara.style.backgroundColor = "blue";

// can't see page-script-added properties
console.log(window.foo); // undefined

// sees the original form of redefined properties
window.confirm("Are you sure?"); // calls the original window.confirm()

相反的情况也是成立的:页面脚本不能察觉到通过content scripts 添加的JavaScript 属性

这意味着content script 可以依靠DOM属性获取可预期的行为

这种行为造成的一个结果是content script不能获取任何通过页面加载的Javascript 库。所以,如果这个页面包含JQuery,content script 将不会在意它。

如果一个content script 想要使用Javascript库,这个库本身就必须像一个content script一样在这个content script旁被插入:

1
2
3
4
5
6
"content_scripts": [
{
"matches": ["*://*.mozilla.org/*"],
"js": ["jquery.js", "content-script.js"]
}
]
WebExtensions APIs

除了standard DOM APIS,content script还能使用以下WebExtension APIS:

From extension:

  • getURL()
  • inIncognitoContext

From runtime:

  • connect()
  • getManifest()
  • getURL()
  • onConnect
  • onMessage
  • sendMessage()

From i18n:

  • getMessage()
  • getAcceptLanguages()
  • getUILanguage()
  • detectLanguage()

所有 storage.

参考链接:

https://developer.mozilla.org/en-US/docs/Mozilla/Tech/Xray_vision

https://stackoverflow.com/questions/10824697/why-is-window-and-unsafewindow-not-the-same-from-a-userscript-as-from-a-scrip

https://developer.chrome.com/extensions/content_scripts#execution-environment

https://developer.chrome.com/extensions

https://developer.mozilla.org/zh-CN/docs/Mozilla/Add-ons/WebExtensions/Content_scripts

https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_shadow_DOM