--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -197,16 +197,17 @@
#include "nsScriptSecurityManager.h"
#include "nsSerializationHelper.h"
#include "nsStreamUtils.h"
#include "nsTextEditorState.h"
#include "nsTextFragment.h"
#include "nsTextNode.h"
#include "nsThreadUtils.h"
#include "nsUnicodeProperties.h"
+#include "nsURLHelper.h"
#include "nsViewManager.h"
#include "nsViewportInfo.h"
#include "nsWidgetsCID.h"
#include "nsIWindowProvider.h"
#include "nsWrapperCacheInlines.h"
#include "nsXULPopupManager.h"
#include "xpcprivate.h" // nsXPConnect
#include "HTMLSplitOnSpacesTokenizer.h"
@@ -2328,16 +2329,65 @@ nsContentUtils::PrincipalHasPermission(n
// static
bool
nsContentUtils::CallerHasPermission(JSContext* aCx, const nsIAtom* aPerm)
{
return PrincipalHasPermission(SubjectPrincipal(aCx), aPerm);
}
+// static
+nsIPrincipal*
+nsContentUtils::GetAttrTriggeringPrincipal(nsIContent* aContent, const nsAString& aAttrValue,
+ nsIPrincipal* aSubjectPrincipal)
+{
+ nsIPrincipal* contentPrin = aContent ? aContent->NodePrincipal() : nullptr;
+
+ // If the subject principal is the same as the content principal, or no
+ // explicit subject principal was provided, we don't need to do any further
+ // checks. Just return the content principal.
+ if (contentPrin == aSubjectPrincipal || !aSubjectPrincipal) {
+ return contentPrin;
+ }
+
+ // If the attribute value is empty, it's not an absolute URL, so don't bother
+ // with more expensive checks.
+ if (!aAttrValue.IsEmpty() &&
+ IsAbsoluteURL(NS_ConvertUTF16toUTF8(aAttrValue))) {
+ return aSubjectPrincipal;
+ }
+
+ return contentPrin;
+}
+
+// static
+bool
+nsContentUtils::IsAbsoluteURL(const nsACString& aURL)
+{
+ nsAutoCString scheme;
+ if (NS_FAILED(net_ExtractURLScheme(aURL, scheme))) {
+ // If we can't extract a scheme, it's not an absolute URL.
+ return false;
+ }
+
+ // If it parses as an absolute StandardURL, it's definitely an absolute URL,
+ // so no need to check with the IO service.
+ if (net_IsAbsoluteURL(aURL)) {
+ return true;
+ }
+
+ nsCOMPtr<nsIIOService> ios = services::GetIOService();
+ uint32_t flags;
+ if (ios && NS_SUCCEEDED(ios->GetProtocolFlags(scheme.get(), &flags))) {
+ return flags & nsIProtocolHandler::URI_NORELATIVE;
+ }
+
+ return false;
+}
+
//static
bool
nsContentUtils::InProlog(nsINode *aNode)
{
NS_PRECONDITION(aNode, "missing node to nsContentUtils::InProlog");
nsINode* parent = aNode->GetParentNode();
if (!parent || !parent->IsNodeOfType(nsINode::eDOCUMENT)) {
@@ -10403,23 +10453,27 @@ nsContentUtils::AppendNativeAnonymousChi
if (!(aFlags & nsIContent::eSkipDocumentLevelNativeAnonymousContent) &&
aContent == aContent->OwnerDoc()->GetRootElement()) {
AppendDocumentLevelNativeAnonymousContentTo(aContent->OwnerDoc(), aKids);
}
}
/* static */ bool
nsContentUtils::GetLoadingPrincipalForXULNode(nsIContent* aLoadingNode,
+ nsIPrincipal* aDefaultPrincipal,
nsIPrincipal** aLoadingPrincipal)
{
MOZ_ASSERT(aLoadingNode);
MOZ_ASSERT(aLoadingPrincipal);
bool result = false;
- nsCOMPtr<nsIPrincipal> loadingPrincipal = aLoadingNode->NodePrincipal();
+ nsCOMPtr<nsIPrincipal> loadingPrincipal = aDefaultPrincipal;
+ if (!loadingPrincipal) {
+ loadingPrincipal = aLoadingNode->NodePrincipal();
+ }
nsAutoString loadingStr;
aLoadingNode->GetAttr(kNameSpaceID_None, nsGkAtoms::loadingprincipal,
loadingStr);
if (loadingStr.IsEmpty()) {
// Fall back to mContent's principal (SystemPrincipal) if 'loadingprincipal'
// isn't specified.
loadingPrincipal.forget(aLoadingPrincipal);
return result;
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -596,16 +596,46 @@ public:
// Check if the principal is chrome or an addon with the permission.
static bool PrincipalHasPermission(nsIPrincipal* aPrincipal, const nsIAtom* aPerm);
// Check if the JS caller is chrome or an addon with the permission.
static bool CallerHasPermission(JSContext* aCx, const nsIAtom* aPerm);
/**
+ * Returns the triggering principal which should be used for the given URL
+ * attribute value with the given subject principal.
+ *
+ * If the attribute value is not an absolute URL, the subject principal will
+ * be ignored, and the node principal of aContent will be used instead.
+ * If aContent is non-null, this function will always return a principal.
+ * Otherewise, it may return null if aSubjectPrincipal is null or is rejected
+ * based on the attribute value.
+ *
+ * @param aContent The content on which the attribute is being set.
+ * @param aAttrValue The URL value of the attribute. For parsed attribute
+ * values, such as `srcset`, this function should be called separately
+ * for each URL value it contains.
+ * @param aSubjectPrincipal The subject principal of the scripted caller
+ * responsible for setting the attribute, or null if no scripted caller
+ * can be determined.
+ */
+ static nsIPrincipal* GetAttrTriggeringPrincipal(nsIContent* aContent,
+ const nsAString& aAttrValue,
+ nsIPrincipal* aSubjectPrincipal);
+
+ /**
+ * Returns true if the given string is guaranteed to be treated as an absolute
+ * URL, rather than a relative URL. In practice, this means any complete URL
+ * as supported by nsStandardURL, or any string beginning with a valid scheme
+ * which is known to the IO service, and has the URI_NORELATIVE flag.
+ */
+ static bool IsAbsoluteURL(const nsACString& aURL);
+
+ /**
* GetDocumentFromCaller gets its document by looking at the last called
* function and finding the document that the function itself relates to.
* For example, consider two windows A and B in the same origin. B has a
* function which does something that ends up needing the current document.
* If a script in window A were to call B's function, GetDocumentFromCaller
* would find that function (in B) and return B's document.
*
* @return The document or null if no JS Context.
@@ -3054,17 +3084,25 @@ public:
* (which is System Principal).
*
* Return true if aLoadingPrincipal has 'loadingprincipal' attributes, and
* the value 'loadingprincipal' is also successfully deserialized, otherwise
* return false.
*/
static bool
GetLoadingPrincipalForXULNode(nsIContent* aLoadingNode,
- nsIPrincipal** aLoadingPrincipal);
+ nsIPrincipal* aDefaultPrincipal,
+ nsIPrincipal** aTriggeringPrincipal);
+
+ static bool
+ GetLoadingPrincipalForXULNode(nsIContent* aLoadingNode,
+ nsIPrincipal** aTriggeringPrincipal)
+ {
+ return GetLoadingPrincipalForXULNode(aLoadingNode, nullptr, aTriggeringPrincipal);
+ }
/**
* Returns the content policy type that should be used for loading images
* for displaying in the UI. The sources of such images can be <xul:image>,
* <xul:menuitem> on OSX where we load the image through nsMenuItemIconX, etc.
*/
static void
GetContentPolicyTypeForUIImageLoading(nsIContent* aLoadingNode,
--- a/dom/base/nsImageLoadingContent.cpp
+++ b/dom/base/nsImageLoadingContent.cpp
@@ -892,17 +892,18 @@ nsImageLoadingContent::UnblockOnload(img
/*
* Non-interface methods
*/
nsresult
nsImageLoadingContent::LoadImage(const nsAString& aNewURI,
bool aForce,
bool aNotify,
- ImageLoadType aImageLoadType)
+ ImageLoadType aImageLoadType,
+ nsIPrincipal* aTriggeringPrincipal)
{
// First, get a document (needed for security checks and the like)
nsIDocument* doc = GetOurOwnerDoc();
if (!doc) {
// No reason to bother, I think...
return NS_OK;
}
@@ -926,27 +927,29 @@ nsImageLoadingContent::LoadImage(const n
// Parse the URI string to get image URI
nsCOMPtr<nsIURI> imageURI;
nsresult rv = StringToURI(aNewURI, doc, getter_AddRefs(imageURI));
NS_ENSURE_SUCCESS(rv, rv);
// XXXbiesi fire onerror if that failed?
NS_TryToSetImmutable(imageURI);
- return LoadImage(imageURI, aForce, aNotify, aImageLoadType, false, doc);
+ return LoadImage(imageURI, aForce, aNotify, aImageLoadType, false, doc,
+ nsIRequest::LOAD_NORMAL, aTriggeringPrincipal);
}
nsresult
nsImageLoadingContent::LoadImage(nsIURI* aNewURI,
bool aForce,
bool aNotify,
ImageLoadType aImageLoadType,
bool aLoadStart,
nsIDocument* aDocument,
- nsLoadFlags aLoadFlags)
+ nsLoadFlags aLoadFlags,
+ nsIPrincipal* aTriggeringPrincipal)
{
MOZ_ASSERT(!mIsStartingImageLoad, "some evil code is reentering LoadImage.");
if (mIsStartingImageLoad) {
return NS_OK;
}
// Pending load/error events need to be canceled in some situations. This
// is not documented in the spec, but can cause site compat problems if not
@@ -1036,17 +1039,17 @@ nsImageLoadingContent::LoadImage(nsIURI*
}
RefPtr<imgRequestProxy>& req = PrepareNextRequest(aImageLoadType);
nsCOMPtr<nsIContent> content =
do_QueryInterface(static_cast<nsIImageLoadingContent*>(this));
nsCOMPtr<nsIPrincipal> triggeringPrincipal;
bool result =
- nsContentUtils::GetLoadingPrincipalForXULNode(content,
+ nsContentUtils::GetLoadingPrincipalForXULNode(content, aTriggeringPrincipal,
getter_AddRefs(triggeringPrincipal));
// If result is true, which means this node has specified 'loadingprincipal'
// attribute on it, so we use favicon as the policy type.
nsContentPolicyType policyType = result ?
nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON:
PolicyTypeForLoad(aImageLoadType);
--- a/dom/base/nsImageLoadingContent.h
+++ b/dom/base/nsImageLoadingContent.h
@@ -98,19 +98,22 @@ protected:
* into this superclass.
*
* @param aNewURI the URI spec to be loaded (may be a relative URI)
* @param aForce If true, make sure to load the URI. If false, only
* load if the URI is different from the currently loaded URI.
* @param aNotify If true, nsIDocumentObserver state change notifications
* will be sent as needed.
* @param aImageLoadType The ImageLoadType for this request
+ * @param aTriggeringPrincipal Optional parameter specifying the triggering
+ * principal to use for the image load
*/
nsresult LoadImage(const nsAString& aNewURI, bool aForce,
- bool aNotify, ImageLoadType aImageLoadType);
+ bool aNotify, ImageLoadType aImageLoadType,
+ nsIPrincipal* aTriggeringPrincipal = nullptr);
/**
* ImageState is called by subclasses that are computing their content state.
* The return value will have the NS_EVENT_STATE_BROKEN,
* NS_EVENT_STATE_USERDISABLED, and NS_EVENT_STATE_SUPPRESSED bits set as
* needed. Note that this state assumes that this node is "trying" to be an
* image (so for example complete lack of attempt to load an image will lead
* to NS_EVENT_STATE_BROKEN being set). Subclasses that are not "trying" to
@@ -130,21 +133,33 @@ protected:
* @param aNotify If true, nsIDocumentObserver state change notifications
* will be sent as needed.
* @param aImageLoadType The ImageLoadType for this request
* @param aLoadStart If true, dispatch "loadstart" event.
* @param aDocument Optional parameter giving the document this node is in.
* This is purely a performance optimization.
* @param aLoadFlags Optional parameter specifying load flags to use for
* the image load
+ * @param aTriggeringPrincipal Optional parameter specifying the triggering
+ * principal to use for the image load
*/
nsresult LoadImage(nsIURI* aNewURI, bool aForce, bool aNotify,
ImageLoadType aImageLoadType, bool aLoadStart = true,
nsIDocument* aDocument = nullptr,
- nsLoadFlags aLoadFlags = nsIRequest::LOAD_NORMAL);
+ nsLoadFlags aLoadFlags = nsIRequest::LOAD_NORMAL,
+ nsIPrincipal* aTriggeringPrincipal = nullptr);
+
+ nsresult LoadImage(nsIURI* aNewURI, bool aForce, bool aNotify,
+ ImageLoadType aImageLoadType,
+ nsIPrincipal* aTriggeringPrincipal)
+ {
+ return LoadImage(aNewURI, aForce, aNotify, aImageLoadType,
+ true, nullptr, nsIRequest::LOAD_NORMAL,
+ aTriggeringPrincipal);
+ }
/**
* helpers to get the document for this content (from the nodeinfo
* and such). Not named GetOwnerDoc/GetCurrentDoc to prevent ambiguous
* method names in subclasses
*
* @return the document we belong to
*/
--- a/dom/html/HTMLImageElement.cpp
+++ b/dom/html/HTMLImageElement.cpp
@@ -413,16 +413,19 @@ HTMLImageElement::AfterMaybeChangeAttr(i
// Both cases handle unsetting src in AfterSetAttr
if (aNamespaceID == kNameSpaceID_None &&
aName == nsGkAtoms::src) {
// Mark channel as urgent-start before load image if the image load is
// initaiated by a user interaction.
mUseUrgentStartForChannel = EventStateManager::IsHandlingUserInput();
+ mSrcTriggeringPrincipal = nsContentUtils::GetAttrTriggeringPrincipal(
+ this, aValue.String(), aSubjectPrincipal);
+
if (InResponsiveMode()) {
if (mResponsiveSelector &&
mResponsiveSelector->Content() == this) {
mResponsiveSelector->SetDefaultSource(aValue.String());
}
QueueImageLoadTask(true);
} else if (aNotify && OwnerDoc()->IsCurrentActiveDocument()) {
// If aNotify is false, we are coming from the parser or some such place;
@@ -438,17 +441,18 @@ HTMLImageElement::AfterMaybeChangeAttr(i
// network if it's set to be not cacheable.
// Potentially, false could be passed here rather than aNotify since
// UpdateState will be called by SetAttrAndNotify, but there are two
// obstacles to this: 1) LoadImage will end up calling
// UpdateState(aNotify), and we do not want it to call UpdateState(false)
// when aNotify is true, and 2) When this function is called by
// OnAttrSetButNotChanged, SetAttrAndNotify will not subsequently call
// UpdateState.
- LoadImage(aValue.String(), true, aNotify, eImageLoadType_Normal);
+ LoadImage(aValue.String(), true, aNotify, eImageLoadType_Normal,
+ mSrcTriggeringPrincipal);
mNewRequestsWillNeedAnimationReset = false;
}
} else if (aNamespaceID == kNameSpaceID_None &&
aName == nsGkAtoms::crossorigin &&
aNotify) {
if (aValueMaybeChanged && GetCORSMode() != AttrValueToCORSMode(aOldValue)) {
// Force a new load of the image with the new cross origin policy.
@@ -997,17 +1001,18 @@ HTMLImageElement::LoadSelectedImage(bool
}
}
// If we have a srcset attribute or are in a <picture> element,
// we always use the Imageset load type, even if we parsed no
// valid responsive sources from either, per spec.
rv = LoadImage(src, aForce, aNotify,
HaveSrcsetOrInPicture() ? eImageLoadType_Imageset
- : eImageLoadType_Normal);
+ : eImageLoadType_Normal,
+ mSrcTriggeringPrincipal);
}
}
mLastSelectedSource = selectedSource;
mCurrentDensity = currentDensity;
if (NS_FAILED(rv)) {
CancelImageRequests(aNotify);
}
--- a/dom/html/HTMLImageElement.h
+++ b/dom/html/HTMLImageElement.h
@@ -137,23 +137,23 @@ public:
void GetAlt(nsAString& aAlt)
{
GetHTMLAttr(nsGkAtoms::alt, aAlt);
}
void SetAlt(const nsAString& aAlt, ErrorResult& aError)
{
SetHTMLAttr(nsGkAtoms::alt, aAlt, aError);
}
- void GetSrc(nsAString& aSrc)
+ void GetSrc(nsAString& aSrc, nsIPrincipal&)
{
GetURIAttr(nsGkAtoms::src, nullptr, aSrc);
}
- void SetSrc(const nsAString& aSrc, ErrorResult& aError)
+ void SetSrc(const nsAString& aSrc, nsIPrincipal& aSubjectPrincipal, ErrorResult& aError)
{
- SetHTMLAttr(nsGkAtoms::src, aSrc, aError);
+ SetHTMLAttr(nsGkAtoms::src, aSrc, aSubjectPrincipal, aError);
}
void GetSrcset(nsAString& aSrcset)
{
GetHTMLAttr(nsGkAtoms::srcset, aSrcset);
}
void SetSrcset(const nsAString& aSrcset, ErrorResult& aError)
{
SetHTMLAttr(nsGkAtoms::srcset, aSrcset, aError);
@@ -421,16 +421,17 @@ private:
void AfterMaybeChangeAttr(int32_t aNamespaceID, nsIAtom* aName,
const nsAttrValueOrString& aValue,
const nsAttrValue* aOldValue,
nsIPrincipal* aSubjectPrincipal,
bool aValueMaybeChanged, bool aNotify);
bool mInDocResponsiveContent;
RefPtr<ImageLoadTask> mPendingImageLoadTask;
+ nsCOMPtr<nsIPrincipal> mSrcTriggeringPrincipal;
// Last URL that was attempted to load by this element.
nsCOMPtr<nsIURI> mLastSelectedSource;
// Last pixel density that was selected.
double mCurrentDensity;
};
} // namespace dom
--- a/dom/webidl/HTMLImageElement.webidl
+++ b/dom/webidl/HTMLImageElement.webidl
@@ -16,17 +16,17 @@ interface imgIRequest;
interface URI;
interface nsIStreamListener;
[HTMLConstructor,
NamedConstructor=Image(optional unsigned long width, optional unsigned long height)]
interface HTMLImageElement : HTMLElement {
[CEReactions, SetterThrows]
attribute DOMString alt;
- [CEReactions, SetterThrows]
+ [CEReactions, NeedsSubjectPrincipal, SetterThrows]
attribute DOMString src;
[CEReactions, SetterThrows]
attribute DOMString srcset;
[CEReactions, SetterThrows]
attribute DOMString? crossOrigin;
[CEReactions, SetterThrows]
attribute DOMString useMap;
[CEReactions, SetterThrows]
--- a/image/test/browser/browser_docshell_type_editor.js
+++ b/image/test/browser/browser_docshell_type_editor.js
@@ -104,15 +104,17 @@ add_task(async function() {
}
image.onerror = function() {
ok(true, "APP_TYPE_UNKNOWN is *not* allowed to acces privileged image");
// restore appType of rootDocShell before moving on to the next test
rootDocShell.appType = defaultAppType;
resolve();
}
doc.body.appendChild(image);
- image.src = "chrome://test1/skin/privileged.png";
+ // Set the src via wrappedJSObject so the load is triggered with
+ // the content page's principal rather than ours.
+ image.wrappedJSObject.src = "chrome://test1/skin/privileged.png";
});
});
});
Components.manager.removeBootstrappedManifestLocation(manifestDir);
});
--- a/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html
@@ -22,17 +22,19 @@ SimpleTest.registerCleanupFunction(() =>
let image = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
"ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=");
const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer;
async function testImageLoading(src, expectedAction) {
let imageLoadingPromise = new Promise((resolve, reject) => {
let cleanupListeners;
let testImage = document.createElement("img");
- testImage.setAttribute("src", src);
+ // Set the src via wrappedJSObject so the load is triggered with the
+ // content page's principal rather than ours.
+ testImage.wrappedJSObject.setAttribute("src", src);
let loadListener = () => {
cleanupListeners();
resolve(expectedAction === "loaded");
};
let errorListener = () => {
cleanupListeners();
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_triggeringPrincipal.js
@@ -0,0 +1,512 @@
+"use strict";
+
+/**
+ * Tests that various types of inline content elements initiate requests
+ * with the triggering pringipal of the caller that requested the load.
+ */
+
+const {escaped} = Cu.import("resource://testing-common/AddonTestUtils.jsm", {});
+
+Cu.importGlobalProperties(["URL"]);
+
+// ExtensionContent.jsm needs to know when it's running from xpcshell,
+// to use the right timeout for content scripts executed at document_idle.
+ExtensionTestUtils.mockAppInfo();
+
+const server = createHttpServer();
+server.registerDirectory("/data/", do_get_file("data"));
+
+/**
+ * Registers a static HTML document with the given content at the given
+ * path in our test HTTP server.
+ *
+ * @param {string} path
+ * @param {string} content
+ */
+function registerStaticPage(path, content) {
+ server.registerPathHandler(path, (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html");
+ response.write(content);
+ });
+}
+
+const BASE_URL = `http://localhost:${server.identity.primaryPort}`;
+
+/**
+ * A set of tags which are automatically closed in HTML documents, and
+ * do not require an explicit closing tag.
+ */
+const AUTOCLOSE_TAGS = new Set(["img"]);
+
+/**
+ * An object describing the elements to create for a specific test.
+ *
+ * @typedef {object} ElementTestCase
+ * @property {Array} element
+ * A recursive array, describing the element to create, in the
+ * following format:
+ *
+ * ["tagname", {attr: "attrValue"},
+ * ["child-tagname", {attr: "value"}],
+ * ...]
+ *
+ * For each test, a DOM tree will be created with this structure.
+ * A source attribute, with the name `test.srcAttr` and a value
+ * based on the values of `test.src` and `opts`, will be added to
+ * the first leaf node encountered.
+ * @property {string} src
+ * The relative URL to use as the source of the element. Each
+ * load of this URL will have a separate set of query parameters
+ * appended to it, based on the values in `opts`.
+ * @property {string} [srcAttr = "src"]
+ * The attribute in which to store the element's source URL.
+ * @property {string} [srcAttr = "src"]
+ * The attribute in which to store the element's source URL.
+ * @property {boolean} [liveSrc = false]
+ * If true, changing the source attribute after the element has
+ * been inserted into the document is expected to trigger a new
+ * load, and that configuration will be tested.
+ */
+
+/**
+ * Options for this specific configuration of an element test.
+ *
+ * @typedef {object} ElementTestOptions
+ * @property {string} origin
+ * The origin with which the content is expected to load. This
+ * may be either "page" or "extension". The actual load of the
+ * URL will be tested against the computed origin strings for
+ * those two contexts.
+ * @property {string} source
+ * An arbitrary string which uniquely identifies the source of
+ * the load. For instance, each of these should have separate
+ * origin strings:
+ *
+ * - An element present in the initial page HTML.
+ * - An element injected by a page script belonging to web
+ * content.
+ * - An element injected by an extension content script.
+ */
+
+/**
+ * Data describing a test element, which can be used to create a
+ * corresponding DOM tree.
+ *
+ * @typedef {object} ElementData
+ * @property {string} tagName
+ * The tag name for the element.
+ * @property {object} attrs
+ * A property containing key-value pairs for each of the
+ * attribute's elements.
+ * @property {Array<ElementData>} children
+ * A possibly empty array of element data for child elements.
+ */
+
+/**
+ * Returns data necessary to create test elements for the given test,
+ * with the given options.
+ *
+ * @param {ElementTestCase} test
+ * An object describing the elements to create for a specific
+ * test. This element will be created under various
+ * circumstances, as described by `opts`.
+ * @param {ElementTestOptions} opts
+ * Options for this specific configuration of the test.
+ * @returns {ElementData}
+ */
+function getElementData(test, opts) {
+ let baseURL = typeof BASE_URL !== "undefined" ? BASE_URL : location.href;
+
+ let {srcAttr, src} = test;
+
+ // Absolutify the URL, so it passes sanity checks that ignore
+ // triggering principals for relative URLs.
+ src = new URL(src + `?origin=${encodeURIComponent(opts.origin)}&source=${encodeURIComponent(opts.source)}`,
+ baseURL).href;
+
+ let haveSrc = false;
+ function rec(element) {
+ let [tagName, attrs, ...children] = element;
+
+ if (children.length) {
+ children = children.map(rec);
+ } else if (!haveSrc) {
+ attrs = Object.assign({[srcAttr]: src}, attrs);
+ haveSrc = true;
+ }
+
+ return {tagName, attrs, children};
+ }
+ return rec(test.element);
+}
+
+/**
+ * The result type of the {@see createElement} function.
+ *
+ * @typedef {object} CreateElementResult
+ * @property {Element} elem
+ * The root element of the created DOM tree.
+ * @property {Element} srcElem
+ * The element in the tree to which the source attribute must be
+ * added.
+ * @property {string} src
+ * The value of the source element.
+ */
+
+/**
+ * Creates a DOM tree for a given test, in a given configuration, as
+ * understood by {@see getElementData}, but without the `test.srcAttr`
+ * attribute having been set. The caller must set the value of that
+ * attribute to the returned `src` value.
+ *
+ * There are many different ways most source values can be set
+ * (DOM attribute, DOM property, ...) and many different contexts
+ * (content script verses page script). Each test should be run with as
+ * many variants of these as possible.
+ *
+ * @param {ElementTestCase} test
+ * A test object, as passed to {@see getElementData}.
+ * @param {ElementTestOptions} opts
+ * An options object, as passed to {@see getElementData}.
+ * @returns {CreateElementResult}
+ */
+function createElement(test, opts) {
+ let srcElem;
+ let src;
+
+ function rec({tagName, attrs, children}) {
+ let elem = document.createElement(tagName);
+
+ for (let [key, val] of Object.entries(attrs)) {
+ if (key === test.srcAttr) {
+ srcElem = elem;
+ src = val;
+ } else {
+ elem.setAttribute(key, val);
+ }
+ }
+ for (let child of children) {
+ elem.appendChild(rec(child));
+ }
+ return elem;
+ }
+ let elem = rec(getElementData(test, opts));
+
+ return {elem, srcElem, src};
+}
+
+/**
+ * Converts the given test data, as accepted by {@see getElementData},
+ * to an HTML representation.
+ *
+ * @param {ElementTestCase} test
+ * A test object, as passed to {@see getElementData}.
+ * @param {ElementTestOptions} opts
+ * An options object, as passed to {@see getElementData}.
+ * @returns {string}
+ */
+function toHTML(test, opts) {
+ function rec({tagName, attrs, children}) {
+ let html = [`<${tagName}`];
+ for (let [key, val] of Object.entries(attrs)) {
+ html.push(escaped` ${key}="${val}"`);
+ }
+
+ html.push(">");
+ if (!AUTOCLOSE_TAGS.has(tagName)) {
+ for (let child of children) {
+ html.push(rec(child));
+ }
+
+ html.push(`</${tagName}>`);
+ }
+ return html.join("");
+ }
+ return rec(getElementData(test, opts));
+}
+
+/**
+ * A function which will be stringified, and run both as a page script
+ * and an extension content script, to test element injection under
+ * various configurations.
+ *
+ * @param {Array<ElementTestCase>} tests
+ * A list of test objects, as understood by {@see getElementData}.
+ * @param {ElementTestOptions} baseOpts
+ * A base options object, as understood by {@see getElementData},
+ * which represents the default values for injections under this
+ * context.
+ */
+function injectElements(tests, baseOpts) {
+ window.addEventListener("load", () => {
+ let overrideOpts = opts => Object.assign({}, baseOpts, opts);
+ let opts = baseOpts;
+
+ // Build the full element with setAttr, then inject.
+ for (let test of tests) {
+ let {elem, srcElem, src} = createElement(test, opts);
+ srcElem.setAttribute(test.srcAttr, src);
+ document.body.appendChild(elem);
+ }
+
+ // Build the full element with a property setter.
+ opts = overrideOpts({source: `${baseOpts.source}-prop`});
+ for (let test of tests) {
+ let {elem, srcElem, src} = createElement(test, opts);
+ srcElem[test.srcAttr] = src;
+ document.body.appendChild(elem);
+ }
+
+ // Build the element without the source attribute, inject, then set
+ // it.
+ opts = overrideOpts({source: `${baseOpts.source}-attr-after-inject`});
+ for (let test of tests) {
+ let {elem, srcElem, src} = createElement(test, opts);
+ document.body.appendChild(elem);
+ srcElem.setAttribute(test.srcAttr, src);
+ }
+
+ // Build the element without the source attribute, inject, then set
+ // the corresponding property.
+ opts = overrideOpts({source: `${baseOpts.source}-prop-after-inject`});
+ for (let test of tests) {
+ let {elem, srcElem, src} = createElement(test, opts);
+ document.body.appendChild(elem);
+ srcElem[test.srcAttr] = src;
+ }
+
+ // Build the element with a relative, rather than absolute, URL, and
+ // make sure it always has the page origin.
+ opts = overrideOpts({source: `${baseOpts.source}-relative-url`,
+ origin: "page"});
+ for (let test of tests) {
+ let {elem, srcElem, src} = createElement(test, opts);
+ // Note: This assumes that the content page and the src URL are
+ // always at the server root. If that changes, the test will
+ // timeout waiting for matching requests.
+ src = src.replace(/.*\//, "");
+ srcElem.setAttribute(test.srcAttr, src);
+ document.body.appendChild(elem);
+ }
+
+ // If we're in an extension content script, do some additional checks.
+ if (typeof browser !== "undefined") {
+ // Build the element without the source attribute, inject, then
+ // have content set it.
+ opts = overrideOpts({source: `${baseOpts.source}-content-attr-after-inject`,
+ origin: "page"});
+
+ for (let test of tests) {
+ let {elem, srcElem, src} = createElement(test, opts);
+ document.body.appendChild(elem);
+ window.wrappedJSObject.elem = srcElem;
+ window.wrappedJSObject.eval(`elem.setAttribute(${uneval(test.srcAttr)}, ${uneval(src)})`);
+ }
+
+ // Build the full element, then let content inject.
+ opts = overrideOpts({source: `${baseOpts.source}-content-inject-after-attr`});
+ for (let test of tests) {
+ let {elem, srcElem, src} = createElement(test, opts);
+ srcElem.setAttribute(test.srcAttr, src);
+ window.wrappedJSObject.elem = elem;
+ window.wrappedJSObject.eval(`document.body.appendChild(elem)`);
+ }
+
+ // Build the element without the source attribute, let content set
+ // it, then inject.
+ opts = overrideOpts({source: `${baseOpts.source}-inject-after-content-attr`,
+ origin: "page"});
+
+ for (let test of tests) {
+ let {elem, srcElem, src} = createElement(test, opts);
+ window.wrappedJSObject.elem = srcElem;
+ window.wrappedJSObject.eval(`elem.setAttribute(${uneval(test.srcAttr)}, ${uneval(src)})`);
+ document.body.appendChild(elem);
+ }
+
+ // Build the element with a dummy source attribute, inject, then
+ // let content change it.
+ opts = overrideOpts({source: `${baseOpts.source}-content-change-after-inject`,
+ origin: "page"});
+
+ for (let test of tests) {
+ let {elem, srcElem, src} = createElement(test, opts);
+ srcElem.setAttribute(test.srcAttr, "meh.txt");
+ document.body.appendChild(elem);
+ window.wrappedJSObject.elem = srcElem;
+ window.wrappedJSObject.eval(`elem.setAttribute(${uneval(test.srcAttr)}, ${uneval(src)})`);
+ }
+ }
+ }, {once: true});
+}
+
+/**
+ * Stringifies the {@see injectElements} function for use as a page or
+ * content script.
+ *
+ * @param {Array<ElementTestCase>} tests
+ * A list of test objects, as understood by {@see getElementData}.
+ * @param {ElementTestOptions} opts
+ * A base options object, as understood by {@see getElementData},
+ * which represents the default values for injections under this
+ * context.
+ * @returns {string}
+ */
+function getInjectionScript(tests, opts) {
+ return `
+ ${getElementData}
+ ${createElement}
+ (${injectElements})(${JSON.stringify(tests)},
+ ${JSON.stringify(opts)});
+ `;
+}
+
+/**
+ * Awaits the content loads for each of the given tests, with each of
+ * the given sources, and checks that their origin strings are as
+ * expected.
+ *
+ * @param {Array<ElementTestCase>} tests
+ * A list of tests, as understood by {@see getElementData}.
+ * @param {Object<string, object>} sources
+ * A set of sources for which each of the above tests is expected
+ * to generate one request, if each of the properties in the
+ * value object matches the value of the same property in the
+ * test object.
+ * @param {object<string, string>} origins
+ * A mapping of origin parameters as they appear in URL query
+ * strings to the origin strings returned by corresponding
+ * principals. These values are used to test requests against
+ * their expected origins.
+ * @returns {Promise}
+ * A promise which resolves when all requests have been
+ * processed.
+ */
+function awaitLoads(tests, sources, origins) {
+ let expectedURLs = new Set();
+
+ for (let test of tests) {
+ for (let [source, attrs] of Object.entries(sources)) {
+ if (Object.keys(attrs).every(attr => attrs[attr] === test[attr])) {
+ let urlPrefix = `${BASE_URL}/${test.src}?source=${source}`;
+ expectedURLs.add(urlPrefix);
+ }
+ }
+ }
+
+ return new Promise(resolve => {
+ let observer = (channel, topic, data) => {
+ channel.QueryInterface(Ci.nsIChannel);
+
+ let url = new URL(channel.URI.spec);
+ let origin = url.searchParams.get("origin");
+ url.searchParams.delete("origin");
+
+ if (expectedURLs.has(url.href)) {
+ expectedURLs.delete(url.href);
+
+ equal(channel.loadInfo.triggeringPrincipal.origin,
+ origins[origin],
+ `Got expected origin for URL ${channel.URI.spec}`);
+
+ if (!expectedURLs.size) {
+ Services.obs.removeObserver(observer, "http-on-modify-request");
+ resolve();
+ }
+ }
+ };
+ Services.obs.addObserver(observer, "http-on-modify-request");
+ });
+}
+
+add_task(async function test_contentscript_triggeringPrincipals() {
+ /**
+ * A list of tests to run in each context, as understood by
+ * {@see getElementData}.
+ */
+ const TESTS = [
+ {
+ element: ["img", {}],
+ src: "img.png",
+ },
+ ];
+
+ /**
+ * A set of sources for which each of the above tests is expected to
+ * generate one request, if each of the properties in the value object
+ * matches the value of the same property in the test object.
+ */
+ const SOURCES = {
+ "contentScript": {},
+ "contentScript-attr-after-inject": {liveSrc: true},
+ "contentScript-content-attr-after-inject": {liveSrc: true},
+ "contentScript-content-change-after-inject": {liveSrc: true},
+ "contentScript-content-inject-after-attr": {},
+ "contentScript-inject-after-content-attr": {},
+ "contentScript-prop": {},
+ "contentScript-prop-after-inject": {},
+ "pageHTML": {},
+ "pageScript": {},
+ "pageScript-attr-after-inject": {},
+ "pageScript-prop": {},
+ "pageScript-prop-after-inject": {},
+ };
+
+ for (let test of TESTS) {
+ if (!test.srcAttr) {
+ test.srcAttr = "src";
+ }
+ if (!("liveSrc" in test)) {
+ test.liveSrc = true;
+ }
+ }
+
+
+ registerStaticPage("/page.html", `<!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title></title>
+ <script>
+ ${getInjectionScript(TESTS, {source: "pageScript", origin: "page"})}
+ </script>
+ </head>
+ <body>
+ ${TESTS.map(test => toHTML(test, {source: "pageHTML", origin: "page"})).join("\n ")}
+ </body>
+ </html>`);
+
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [{
+ "matches": ["http://*/page.html"],
+ "run_at": "document_start",
+ "js": ["content_script.js"],
+ }],
+ },
+
+ files: {
+ "content_script.js": getInjectionScript(TESTS, {source: "contentScript", origin: "extension"}),
+ },
+ });
+
+ await extension.startup();
+
+ const pageURL = `${BASE_URL}/page.html`;
+ const pageURI = Services.io.newURI(pageURL);
+
+ let origins = {
+ page: Services.scriptSecurityManager.createCodebasePrincipal(pageURI, {}).origin,
+ extension: Cu.getObjectPrincipal(Cu.Sandbox([extension.extension.principal, pageURL])).origin,
+ };
+ let finished = awaitLoads(TESTS, SOURCES, origins);
+
+ let contentPage = await ExtensionTestUtils.loadContentPage(pageURL);
+
+ await finished;
+
+ await extension.unload();
+ await contentPage.close();
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-content.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-content.ini
@@ -1,6 +1,7 @@
[test_ext_i18n.js]
skip-if = os == "android" || (os == "win" && debug)
[test_ext_i18n_css.js]
[test_ext_contentscript.js]
[test_ext_contentscript_scriptCreated.js]
+[test_ext_contentscript_triggeringPrincipal.js]
[test_ext_contentscript_xrays.js]