Bug 1305237 Expose frameAncestors to webextensions, r=bz,kmag

MozReview-Commit-ID: 8gvEiqJEsP3
This commit is contained in:
Shane Caraveo
2017-10-02 15:11:54 -07:00
parent b1d96d9364
commit a7b37edbc1
13 changed files with 234 additions and 11 deletions

View File

@@ -286,6 +286,18 @@ interface ChannelWrapper : EventTarget {
[Cached, Pure]
readonly attribute nsISupports? browserElement;
/**
* Returns an array of objects that combine the url and frameId from the
* ancestorPrincipals and ancestorOuterWindowIDs on loadInfo.
* The immediate parent is the first entry, the last entry is always the top
* level frame. It will be an empty list for toplevel window loads and
* non-subdocument resource loads within a toplevel window. For the latter,
* originURL will provide information on what window is doing the load. It
* will be null if the request is not associated with a window (e.g. XHR with
* mozBackgroundRequest = true).
*/
[Cached, Frozen, GetterThrows, Pure]
readonly attribute sequence<MozFrameAncestorInfo>? frameAncestors;
/**
* For HTTP requests, returns an array of request headers which will be, or

View File

@@ -12,6 +12,11 @@
let req = new XMLHttpRequest();
req.open("GET", "/xhr_sandboxed");
req.send();
let sandbox = document.createElement("iframe");
sandbox.setAttribute("sandbox", "allow-scripts");
sandbox.setAttribute("src", "file_simple_sandboxed_subframe.html");
document.documentElement.appendChild(sandbox);
</script>
<img src="file_image_great.png"/>
</body>

View File

@@ -0,0 +1,10 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
</body>
</html>

View File

@@ -18,6 +18,7 @@ sandbox.setAttribute("sandbox", "allow-scripts");
sandbox.setAttribute("src", "file_simple_sandboxed_frame.html");
document.documentElement.appendChild(sandbox);
</script>
<img src="file_image_bad.png#2"/>
<img src="file_image_redirect.png"/>
<iframe src="data:text/plain,webRequestTest"/>
</body>
</html>

View File

@@ -40,6 +40,7 @@ support-files =
file_remote_frame.html
file_sample.html
file_simple_sandboxed_frame.html
file_simple_sandboxed_subframe.html
file_simple_xhr.html
file_simple_xhr_frame.html
file_simple_xhr_frame2.html

View File

@@ -45,49 +45,108 @@ let expected = {
"file_image_good.png": {
type: "image",
toplevel: true,
origin: "file_simple_xhr.html",
},
"example.txt": {
type: "xmlhttprequest",
toplevel: true,
origin: "file_simple_xhr.html",
},
// sub frames will have the origin and first ancestor is the
// parent document
"file_simple_xhr_frame.html": {
type: "sub_frame",
toplevelParent: true,
origin: "file_simple_xhr.html",
parent: "file_simple_xhr.html",
},
// a resource in a sub frame will have origin of the subframe,
// but the ancestor chain starts with the parent document
"xhr_resource": {
type: "xmlhttprequest",
origin: "file_simple_xhr_frame.html",
parent: "file_simple_xhr.html",
},
"file_image_bad.png": {
type: "image",
},
"xhr_resource": {
type: "xmlhttprequest",
depth: 2,
origin: "file_simple_xhr_frame.html",
parent: "file_simple_xhr.html",
},
"file_simple_xhr_frame2.html": {
type: "sub_frame",
depth: 2,
origin: "file_simple_xhr_frame.html",
parent: "file_simple_xhr_frame.html",
},
"file_image_bad.png#2": {
"file_image_redirect.png": {
type: "image",
depth: 2,
origin: "file_simple_xhr_frame2.html",
parent: "file_simple_xhr_frame.html",
},
"xhr_resource_2": {
type: "xmlhttprequest",
depth: 2,
origin: "file_simple_xhr_frame2.html",
parent: "file_simple_xhr_frame.html",
},
// This is loaded in a sandbox iframe.
// Last frame tests content policy frame ancestors.
"webRequestTest": {
type: "sub_frame",
depth: 3,
origin: "file_simple_xhr_frame2.html",
parent: "file_simple_xhr_frame2.html",
},
// This is loaded in a sandbox iframe. originUrl is not availabe for that,
// and requests within a sandboxed iframe will additionally have an empty
// url on their immediate parent/ancestor.
"file_simple_sandboxed_frame.html": {
type: "sub_frame",
depth: 3,
parent: "file_simple_xhr_frame2.html",
},
"xhr_sandboxed": {
type: "xmlhttprequest",
sandboxed: true,
depth: 3,
parent: "",
},
"file_image_great.png": {
type: "image",
sandboxed: true,
depth: 3,
parent: "",
},
"file_simple_sandboxed_subframe.html": {
type: "sub_frame",
depth: 4,
parent: "",
},
};
function checkDetails(details) {
let url = new URL(details.url);
let filename = url.pathname.split("/").pop();
let filename = url.pathname.split(url.protocol == "data:" ? "," : "/").pop();
let expect = expected[filename];
is(expect.type, details.type, `${details.type} type matches`);
if (details.parentFrameId == -1) {
is(details.frameAncestors.length, 0, "no ancestors for main_frame requests");
} else if (details.parentFrameId == 0) {
is(details.frameAncestors.length, 1, "one ancestors for sub_frame requests");
} else {
ok(details.frameAncestors.length > 1, "have multiple ancestors for deep subframe requests");
is(details.frameAncestors.length, expect.depth, "have multiple ancestors for deep subframe requests");
}
if (details.parentFrameId > -1) {
ok(!expect.origin || details.originUrl.includes(expect.origin), "origin url is correct");
is(details.frameAncestors[0].frameId, details.parentFrameId, "first ancestor matches request.parentFrameId");
ok(details.frameAncestors[0].url.includes(expect.parent), "ancestor parent page correct");
is(details.frameAncestors[details.frameAncestors.length - 1].frameId, 0, "last ancestor is always zero");
// All our tests should be somewhere within the frame that we set topframe in the query string. That
// frame will always be the last ancestor.
ok(details.frameAncestors[details.frameAncestors.length - 1].url.includes("topframe=true"), "last ancestor is always topframe");
}
if (expect.toplevel) {
is(details.frameId, 0, "expect load at top level");
is(details.parentFrameId, -1, "expect top level frame to have no parent");
@@ -95,8 +154,10 @@ function checkDetails(details) {
ok(details.frameId > 0, "expect sub_frame to load into a new frame");
if (expect.toplevelParent) {
is(details.parentFrameId, 0, "expect sub_frame to have top level parent");
is(details.frameAncestors.length, 1, "one ancestor for top sub_frame request");
} else {
ok(details.parentFrameId > 0, "expect sub_frame to have parent");
ok(details.frameAncestors.length > 1, "sub_frame has ancestors");
}
expect.subframeId = details.frameId;
expect.parentId = details.parentFrameId;
@@ -121,7 +182,7 @@ add_task(async function test_webRequest_main_frame() {
let extension = ExtensionTestUtils.loadExtension(extensionData);
await extension.startup();
let a = addLink(`file_simple_xhr.html?nocache=${Math.random()}`);
let a = addLink(`file_simple_xhr.html?topframe=true&nocache=${Math.random()}`);
a.click();
for (let i = 0; i < Object.keys(expected).length; i++) {
@@ -133,7 +194,6 @@ add_task(async function test_webRequest_main_frame() {
await closed;
await extension.unload();
});
</script>
</head>
<body>

View File

@@ -569,6 +569,60 @@ ChannelWrapper::ParentWindowId() const
return -1;
}
void
ChannelWrapper::GetFrameAncestors(dom::Nullable<nsTArray<dom::MozFrameAncestorInfo>>& aFrameAncestors, ErrorResult& aRv) const
{
nsCOMPtr<nsILoadInfo> loadInfo = GetLoadInfo();
if (!loadInfo || WindowId(loadInfo) == 0) {
aFrameAncestors.SetNull();
return;
}
nsresult rv = GetFrameAncestors(loadInfo, aFrameAncestors.SetValue());
if (NS_FAILED(rv)) {
aRv.Throw(rv);
}
}
nsresult
ChannelWrapper::GetFrameAncestors(nsILoadInfo* aLoadInfo, nsTArray<dom::MozFrameAncestorInfo>& aFrameAncestors) const
{
const nsTArray<nsCOMPtr<nsIPrincipal>>& ancestorPrincipals = aLoadInfo->AncestorPrincipals();
const nsTArray<uint64_t>& ancestorOuterWindowIDs = aLoadInfo->AncestorOuterWindowIDs();
uint32_t size = ancestorPrincipals.Length();
MOZ_DIAGNOSTIC_ASSERT(size == ancestorOuterWindowIDs.Length());
if (size != ancestorOuterWindowIDs.Length()) {
return NS_ERROR_UNEXPECTED;
}
bool subFrame = aLoadInfo->GetExternalContentPolicyType() == nsIContentPolicy::TYPE_SUBDOCUMENT;
if (!aFrameAncestors.SetCapacity(subFrame ? size : size + 1, fallible)) {
return NS_ERROR_OUT_OF_MEMORY;
}
// The immediate parent is always the first element in the ancestor arrays, however
// SUBDOCUMENTs do not have their immediate parent included, so we inject it here.
// This will force wrapper.parentWindowId == wrapper.frameAncestors[0].frameId to
// always be true. All ather requests already match this way.
if (subFrame) {
auto ancestor = aFrameAncestors.AppendElement();
GetDocumentURL(ancestor->mUrl);
ancestor->mFrameId = ParentWindowId();
}
for (uint32_t i = 0; i < size; ++i) {
auto ancestor = aFrameAncestors.AppendElement();
nsCOMPtr<nsIURI> uri;
MOZ_TRY(ancestorPrincipals[i]->GetURI(getter_AddRefs(uri)));
if (!uri) {
return NS_ERROR_UNEXPECTED;
}
MOZ_TRY(uri->GetSpec(ancestor->mUrl));
ancestor->mFrameId = NormalizeWindowID(aLoadInfo, ancestorOuterWindowIDs[i]);
}
return NS_OK;
}
/*****************************************************************************
* Response filtering
*****************************************************************************/
@@ -721,7 +775,7 @@ ChannelWrapper::FinalURI() const
if (nsCOMPtr<nsIChannel> chan = MaybeChannel()) {
NS_GetFinalChannelURI(chan, getter_AddRefs(uri));
}
return uri.forget();;
return uri.forget();
}
void

View File

@@ -190,6 +190,8 @@ public:
int64_t ParentWindowId() const;
void GetFrameAncestors(dom::Nullable<nsTArray<dom::MozFrameAncestorInfo>>& aFrameAncestors, ErrorResult& aRv) const;
bool IsSystemLoad() const;
void GetOriginURL(nsCString& aRetVal) const;
@@ -262,6 +264,8 @@ private:
uint64_t WindowId(nsILoadInfo* aLoadInfo) const;
nsresult GetFrameAncestors(nsILoadInfo* aLoadInfo, nsTArray<dom::MozFrameAncestorInfo>& aFrameAncestors) const;
static uint64_t GetNextId()
{
static uint64_t sNextId = 1;

View File

@@ -171,7 +171,7 @@ const MAYBE_CACHED_EVENTS = new Set([
const OPTIONAL_PROPERTIES = [
"requestHeaders", "responseHeaders", "statusCode", "statusLine", "error", "redirectUrl",
"requestBody", "scheme", "realm", "isProxy", "challenger", "proxyInfo", "ip",
"requestBody", "scheme", "realm", "isProxy", "challenger", "proxyInfo", "ip", "frameAncestors",
];
function serializeRequestData(eventName) {

View File

@@ -118,6 +118,7 @@ var ContentPolicy = {
let windowId = 0;
let parentWindowId = -1;
let frameAncestors = [];
let mm = Services.cpmm;
function getWindowId(window) {
@@ -152,6 +153,18 @@ var ContentPolicy = {
windowId = getWindowId(window);
if (window.parent !== window) {
parentWindowId = getWindowId(window.parent);
for (let frame = window.parent; ; frame = frame.parent) {
frameAncestors.push({
url: frame.document.documentURIObject.spec,
frameId: getWindowId(frame),
});
if (frame === frame.parent) {
// Set the last frameId to zero for top level frame.
frameAncestors[frameAncestors.length - 1].frameId = 0;
break;
}
}
}
let ir = window.QueryInterface(Ci.nsIInterfaceRequestor)
@@ -172,6 +185,9 @@ var ContentPolicy = {
type: WebRequestCommon.typeForPolicyType(policyType),
windowId,
parentWindowId};
if (frameAncestors.length > 0) {
data.frameAncestors = frameAncestors;
}
if (requestOrigin) {
data.originUrl = requestOrigin.spec;
}

View File

@@ -35,6 +35,7 @@ support-files = file_FinderSample.html
[browser_WebNavigation.js]
skip-if = true # Superseded by WebExtension tests
[browser_WebRequest.js]
[browser_WebRequest_ancestors.js]
[browser_WebRequest_cookies.js]
[browser_WebRequest_filtering.js]
[browser_PageMetadata.js]

View File

@@ -44,6 +44,11 @@ function onBeforeRequest(details) {
let page1id = windowIDs.get(URL);
ok(details.windowId != page1id, "sub-frame gets its own window ID");
is(details.parentWindowId, page1id, "parent window id is correct");
is(details.frameAncestors.length, 1, "correctly has only one ancestor");
let ancestor = details.frameAncestors[0];
ok(ancestor.url.includes("page1"), "parent window url seems correct");
is(ancestor.frameId, page1id, "parent window id is correct");
}
}
if (details.url.indexOf("_bad.") != -1) {

View File

@@ -0,0 +1,54 @@
"use strict";
var { interfaces: Ci, classes: Cc, utils: Cu, results: Cr } = Components;
Cu.importGlobalProperties(["XMLHttpRequest"]);
var {WebRequest} = Cu.import("resource://gre/modules/WebRequest.jsm", {});
var {PromiseUtils} = Cu.import("resource://gre/modules/PromiseUtils.jsm", {});
add_task(async function test_ancestors_exist() {
let deferred = PromiseUtils.defer();
function onBeforeRequest(details) {
info(`onBeforeRequest ${details.url}`);
ok(typeof details.frameAncestors === "object", `ancestors exists [${typeof details.frameAncestors}]`);
deferred.resolve();
}
WebRequest.onBeforeRequest.addListener(onBeforeRequest, {urls: new MatchPatternSet(["http://mochi.test/test/*"])}, ["blocking"]);
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/test/");
await deferred.promise;
await BrowserTestUtils.removeTab(tab);
WebRequest.onBeforeRequest.removeListener(onBeforeRequest);
});
add_task(async function test_ancestors_null() {
let deferred = PromiseUtils.defer();
function onBeforeRequest(details) {
info(`onBeforeRequest ${details.url}`);
ok(details.frameAncestors === undefined, "ancestors do not exist");
deferred.resolve();
}
WebRequest.onBeforeRequest.addListener(onBeforeRequest, null, ["blocking"]);
function fetch(url) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.mozBackgroundRequest = true;
xhr.open("GET", url);
xhr.onload = () => { resolve(xhr.responseText); };
xhr.onerror = () => { reject(xhr.status); };
// use a different contextId to avoid auth cache.
xhr.setOriginAttributes({userContextId: 1});
xhr.send();
});
}
await fetch("http://mochi.test:8888/test/");
await deferred.promise;
WebRequest.onBeforeRequest.removeListener(onBeforeRequest);
});