與頁面指令碼共享物件

注意:本節介紹的技術僅在 Firefox 中可用,並且僅從 Firefox 49 及更高版本開始。

警告:作為擴充套件開發者,您應該考慮到在任意網頁中執行的指令碼都是有害程式碼,其目的是竊取使用者的個人資訊、損壞使用者的計算機或以其他方式攻擊使用者。

內容指令碼和網頁載入的指令碼之間的隔離旨在使有害網頁更難做到這一點。

由於本節介紹的技術打破了這種隔離,因此它們本質上是危險的,應謹慎使用。

正如 內容指令碼指南所述,內容指令碼看不到網頁載入的指令碼對 DOM 所做的更改。這意味著,例如,如果一個網頁載入了 jQuery 這樣的庫,內容指令碼將無法使用它,而必須載入自己的副本。反之,網頁載入的指令碼也看不到內容指令碼所做的更改。

然而,Firefox 提供了一些 API,允許內容指令碼

  • 訪問頁面指令碼建立的 JavaScript 物件
  • 將自己的 JavaScript 物件暴露給頁面指令碼。

Firefox 中的 Xray Vision

在 Firefox 中,內容指令碼和頁面指令碼之間的部分隔離是透過一種稱為“Xray Vision”的功能實現的。當一個特權範圍較高的指令碼訪問一個定義在特權範圍較低的指令碼中的物件時,它只能看到該物件的“原生版本”。任何 擴充套件屬性 都是不可見的,如果物件的任何屬性已被重新定義,它將看到原始實現,而不是重新定義後的版本。

此功能旨在讓特權範圍較低的指令碼更難透過重新定義物件的原生屬性來混淆特權範圍較高的指令碼。

因此,例如,當內容指令碼訪問頁面的 window 物件時,它看不到頁面指令碼新增到 window 物件的任何屬性,如果頁面指令碼重新定義了 window 的任何現有屬性,內容指令碼將看到原始版本。

從內容指令碼訪問頁面指令碼物件

在 Firefox 中,內容指令碼中的 DOM 物件會獲得一個額外的屬性 wrappedJSObject。這是該物件的“解開”版本,包括頁面指令碼對該物件所做的任何更改。

我們舉個例子。假設一個網頁載入了一個指令碼

html
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <script src="main.js"></script>
  </body>
</html>

該指令碼向全域性 window 物件添加了一個擴充套件屬性

js
// main.js

let foo = "I'm defined in a page script!";

Xray Vision 意味著,如果內容指令碼嘗試訪問 foo,它將是未定義的

js
// content-script.js

console.log(window.foo); // undefined

在 Firefox 中,內容指令碼可以使用 window.wrappedJSObject 來檢視擴充套件屬性

js
// content-script.js

console.log(window.wrappedJSObject.foo); // "I'm defined in a page script!"

請注意,一旦您這樣做,您就無法再依賴該物件的任何屬性或函式是否如您所預期那樣存在或執行。其中任何一個,包括 setter 和 getter,都可能被不受信任的程式碼重新定義。

另請注意,解開過程是傳遞性的:當您使用 wrappedJSObject 時,解開物件的所有屬性本身也會被解開(因此不可靠)。因此,一旦您獲得了所需的物件,最好將其重新包裝起來,您可以這樣做:

js
XPCNativeWrapper(window.wrappedJSObject.foo);

有關更多詳細資訊,請參閱關於 Xray Vision 的文件。

將內容指令碼物件與頁面指令碼共享

Firefox 還提供 API,允許內容指令碼將物件提供給頁面指令碼。這裡有幾種方法:

  • exportFunction():將函式匯出到頁面指令碼。
  • cloneInto():將物件匯出到頁面指令碼。
  • 來自頁面上下文的建構函式

exportFunction

給定在內容指令碼中定義的函式,exportFunction() 會將其匯出到頁面指令碼的範圍,以便頁面指令碼可以呼叫它。

例如,我們來看一個有如下後臺指令碼的擴充套件

js
/*
Execute content script in the active tab.
*/
function loadContentScript() {
  browser.tabs.executeScript({
    file: "/content_scripts/export.js",
  });
}

/*
Add loadContentScript() as a listener to clicks
on the browser action.
*/
browser.browserAction.onClicked.addListener(loadContentScript);

/*
Show a notification when we get messages from
the content script.
*/
browser.runtime.onMessage.addListener((message) => {
  browser.notifications.create({
    type: "basic",
    title: "Message from the page",
    message: message.content,
  });
});

這做了兩件事:

  • 當用戶單擊瀏覽器操作時,在當前標籤頁中執行內容指令碼
  • 監聽來自內容指令碼的訊息,並在訊息到達時顯示一個 通知

內容指令碼如下所示:

js
/*
Define a function in the content script's scope, then export it
into the page script's scope.
*/
function notify(message) {
  browser.runtime.sendMessage({ content: `Function call: ${message}` });
}

exportFunction(notify, window, { defineAs: "notify" });

這定義了一個名為 notify() 的函式,它只是將引數傳送到後臺指令碼。然後,它將該函式匯出到頁面指令碼的範圍。現在頁面指令碼可以呼叫此函數了

js
window.notify("Message from the page script!");

cloneInto

給定在內容指令碼中定義的某個物件,cloneInto() 會在頁面指令碼的範圍內建立一個該物件的克隆,從而使該克隆可供頁面指令碼訪問。預設情況下,這使用 結構化克隆演算法 來克隆物件,這意味著物件中的函式不包含在克隆中。要包含函式,請傳遞 cloneFunctions 選項。

例如,這是一個內容指令碼,它定義了一個包含函式的物件,然後將其克隆到頁面指令碼的範圍:

js
/*
Create an object that contains functions in
the content script's scope, then clone it
into the page script's scope.

Because the object contains functions,
the cloneInto call must include
the `cloneFunctions` option.
*/
let messenger = {
  notify(message) {
    browser.runtime.sendMessage({
      content: `Object method call: ${message}`,
    });
  },
};

window.wrappedJSObject.messenger = cloneInto(messenger, window, {
  cloneFunctions: true,
});

現在頁面指令碼在 window 物件上看到一個新屬性 messenger,它包含一個名為 notify() 的函式。

js
window.messenger.notify("Message from the page script!");

來自頁面上下文的建構函式

在經過 Xray 處理的 window 物件上,可以訪問一些內建 JavaScript 物件(如 ObjectFunctionProxy)以及各種 DOM 類的原始建構函式。XMLHttpRequest 的行為不同,有關詳細資訊,請參閱 XHR 和 fetch 部分。它們將建立屬於頁面全域性物件層次結構的例項,然後返回一個 Xray 包裝器。

由於以這種方式建立的物件已經屬於頁面而不是內容指令碼,因此將它們傳遞迴頁面將不需要額外的克隆或匯出。

js
/* JavaScript built-ins */

const objA = new Object();
const objB = new window.Object();

console.log(
  objA instanceof Object, // true
  objB instanceof Object, // false
  objA instanceof window.Object, // false
  objB instanceof window.Object, // true
  "wrappedJSObject" in objB, // true; xrayed
);

objA.foo = "foo";
objB.foo = "foo"; // xray wrappers for plain JavaScript objects pass through property assignments
objB.wrappedJSObject.bar = "bar"; // unwrapping before assignment does not rely on this special behavior

window.wrappedJSObject.objA = objA;
window.wrappedJSObject.objB = objB; // automatically unwraps when passed to page context

window.eval(`
  console.log(objA instanceof Object);           // false
  console.log(objB instanceof Object);           // true

  try {
    console.log(objA.foo);
  } catch (error) {
    console.log(error);                       // Error: permission denied
  }
 
  try {
    objA.baz = "baz";
  } catch (error) {
    console.log(error);                       // Error: permission denied
  }

  console.log(objB.foo, objB.bar);               // "foo", "bar"
  objB.baz = "baz";
`);

/* other APIs */

const ev = new Event("click");

console.log(
  ev instanceof Event, // true
  ev instanceof window.Event, // true; Event constructor is actually inherited from the xrayed window
  "wrappedJSObject" in ev, // true; is an xrayed object
);

ev.propA = "propA"; // xray wrappers for native objects do not pass through assignments
ev.propB = "wrapper"; // define property on xray wrapper
ev.wrappedJSObject.propB = "unwrapped"; // define same property on page object
Reflect.defineProperty(
  // privileged reflection can operate on less privileged objects
  ev.wrappedJSObject,
  "propC",
  {
    // getters must be exported like regular functions
    get: exportFunction(() => "propC", window),
  },
);

window.eval(`
  document.addEventListener("click", (e) => {
    console.log(e instanceof Event, e.propA, e.propB, e.propC);
  });
`);

document.dispatchEvent(ev); // true, undefined, "unwrapped", "propC"

Promise 克隆

Promise 無法直接使用 cloneInto 進行克隆,因為 Promise 不受 結構化克隆演算法 支援。但是,可以透過使用 window.Promise 而不是 Promise 來實現所需的結果,然後像這樣克隆解析值:

js
const promise = new window.Promise((resolve) => {
  // if just a primitive, then cloneInto is not needed:
  // resolve("string is a primitive");

  // if not a primitive, such as an object, then the value must be cloned
  const result = { exampleKey: "exampleValue" };
  resolve(cloneInto(result, window));
});
// now the promise can be passed to the web page