commit c2b7e89ecaf3ea6a14a650a0a71c6dc3ebb02c50 Author: Arthur Edelstein arthuredelstein@gmail.com Date: Tue May 30 10:25:34 2017 -0700
Bug 22343: Make 'Save Page As' obey first-party isolation
Fixes: * File menu: - Save Page As * Context menu in content pages: - Save Page As - Save Image As - Save Video As - Save Link As - Save Frame As * Page Info "Media" Panel: - Save As --- browser/base/content/browser.js | 2 +- browser/base/content/nsContextMenu.js | 39 ++++++++++++++++++-------- browser/base/content/pageinfo/pageInfo.js | 5 ++-- browser/base/content/utilityOverlay.js | 6 ++-- dom/webbrowserpersist/nsIWebBrowserPersist.idl | 9 +++++- dom/webbrowserpersist/nsWebBrowserPersist.cpp | 18 ++++++++++-- dom/webbrowserpersist/nsWebBrowserPersist.h | 2 ++ mobile/android/chrome/content/browser.js | 6 ++-- netwerk/base/LoadContextInfo.cpp | 18 ++++++++++-- toolkit/components/browser/nsWebBrowser.cpp | 12 ++++++++ toolkit/content/contentAreaUtils.js | 32 +++++++++++++++------ 11 files changed, 115 insertions(+), 34 deletions(-)
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 0fc6a72daf66..5eed34e08086 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -6175,7 +6175,7 @@ function handleLinkClick(event, href, linkNode) {
if (where == "save") { saveURL(href, linkNode ? gatherTextUnder(linkNode) : "", null, true, - true, doc.documentURIObject, doc); + true, doc.documentURIObject, doc, undefined, doc.nodePrincipal); event.preventDefault(); return true; } diff --git a/browser/base/content/nsContextMenu.js b/browser/base/content/nsContextMenu.js index 368d0475ac34..37ebde22ea07 100644 --- a/browser/base/content/nsContextMenu.js +++ b/browser/base/content/nsContextMenu.js @@ -989,9 +989,11 @@ nsContextMenu.prototype = { let onMessage = (message) => { mm.removeMessageListener("ContextMenu:SaveVideoFrameAsImage:Result", onMessage); let dataURL = message.data.dataURL; + const principal = Services.scriptSecurityManager.createCodebasePrincipal( + makeURI(dataURL), this.principal.originAttributes); saveImageURL(dataURL, name, "SaveImageTitle", true, false, document.documentURIObject, null, null, null, - isPrivate); + isPrivate, principal); }; mm.addMessageListener("ContextMenu:SaveVideoFrameAsImage:Result", onMessage); }, @@ -1063,7 +1065,7 @@ nsContextMenu.prototype = { // Helper function to wait for appropriate MIME-type headers and // then prompt the user with a file picker saveHelper(linkURL, linkText, dialogTitle, bypassCache, doc, docURI, - windowID, linkDownload, isContentWindowPrivate) { + windowID, linkDownload, isContentWindowPrivate, contentPrincipal) { // canonical def in nsURILoader.h const NS_ERROR_SAVE_LINK_AS_TIMEOUT = 0x805d0020;
@@ -1116,7 +1118,7 @@ nsContextMenu.prototype = { // do it the old fashioned way, which will pick the best filename // it can without waiting. saveURL(linkURL, linkText, dialogTitle, bypassCache, false, docURI, - doc, isContentWindowPrivate); + doc, isContentWindowPrivate, contentPrincipal); } if (this.extListener) this.extListener.onStopRequest(aRequest, aContext, aStatusCode); @@ -1156,10 +1158,13 @@ nsContextMenu.prototype = { } };
+ const principal = Services.scriptSecurityManager.createCodebasePrincipal( + makeURI(linkURL), this.principal.originAttributes); + // setting up a new channel for 'right click - save link as ...' var channel = NetUtil.newChannel({ uri: makeURI(linkURL), - loadingPrincipal: this.principal, + loadingPrincipal: principal, contentPolicyType: Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD, securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS, }); @@ -1201,14 +1206,17 @@ nsContextMenu.prototype = {
// Save URL of clicked-on link. saveLink() { - urlSecurityCheck(this.linkURL, this.principal); + const principal = Services.scriptSecurityManager.createCodebasePrincipal( + makeURI(this.linkURL), this.principal.originAttributes); + urlSecurityCheck(this.linkURL, principal);
let isContentWindowPrivate = this.isRemote ? this.ownerDoc.isPrivate : undefined; this.saveHelper(this.linkURL, this.linkTextStr, null, true, this.ownerDoc, gContextMenuContentData.documentURIObject, this.frameOuterWindowID, this.linkDownload, - isContentWindowPrivate); + isContentWindowPrivate, + principal); },
// Backwards-compatibility wrapper @@ -1223,23 +1231,32 @@ nsContextMenu.prototype = { let isContentWindowPrivate = this.isRemote ? this.ownerDoc.isPrivate : undefined; let referrerURI = gContextMenuContentData.documentURIObject; let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser); + let thisPrincipal = this.principal; if (this.onCanvas) { // Bypass cache, since it's a data: URL. this._canvasToBlobURL(this.target).then(function(blobURL) { + const principal = Services.scriptSecurityManager.createCodebasePrincipal( + makeURI(blobURL), thisPrincipal.originAttributes); saveImageURL(blobURL, "canvas.png", "SaveImageTitle", true, false, referrerURI, null, null, null, - isPrivate); + isPrivate, principal); }, Cu.reportError); } else if (this.onImage) { - urlSecurityCheck(this.mediaURL, this.principal); + const principal = Services.scriptSecurityManager.createCodebasePrincipal( + makeURI(this.mediaURL), thisPrincipal.originAttributes); + urlSecurityCheck(this.mediaURL, principal); saveImageURL(this.mediaURL, null, "SaveImageTitle", false, false, referrerURI, null, gContextMenuContentData.contentType, - gContextMenuContentData.contentDisposition, isPrivate); + gContextMenuContentData.contentDisposition, isPrivate, + principal); } else if (this.onVideo || this.onAudio) { - urlSecurityCheck(this.mediaURL, this.principal); + const principal = Services.scriptSecurityManager.createCodebasePrincipal( + makeURI(this.mediaURL), thisPrincipal.originAttributes); + urlSecurityCheck(this.mediaURL, principal); var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle"; this.saveHelper(this.mediaURL, null, dialogTitle, false, doc, referrerURI, - this.frameOuterWindowID, "", isContentWindowPrivate); + this.frameOuterWindowID, "", isContentWindowPrivate, + principal); } },
diff --git a/browser/base/content/pageinfo/pageInfo.js b/browser/base/content/pageinfo/pageInfo.js index 86f548c74494..efe24f7487d9 100644 --- a/browser/base/content/pageinfo/pageInfo.js +++ b/browser/base/content/pageinfo/pageInfo.js @@ -696,7 +696,7 @@ function saveMedia() { titleKey = "SaveAudioTitle";
saveURL(url, null, titleKey, false, false, makeURI(item.baseURI), - null, gDocInfo.isContentWindowPrivate); + null, gDocInfo.isContentWindowPrivate, gDocInfo.principal); } } else { selectSaveFolder(function(aDirectory) { @@ -704,7 +704,8 @@ function saveMedia() { var saveAnImage = function(aURIString, aChosenData, aBaseURI) { uniqueFile(aChosenData.file); internalSave(aURIString, null, null, null, null, false, "SaveImageTitle", - aChosenData, aBaseURI, null, false, null, gDocInfo.isContentWindowPrivate); + aChosenData, aBaseURI, null, false, null, gDocInfo.isContentWindowPrivate, + gDocInfo.principal); };
for (var i = 0; i < rowArray.length; i++) { diff --git a/browser/base/content/utilityOverlay.js b/browser/base/content/utilityOverlay.js index b73a01a0b0f3..4cd7d91e4e9a 100644 --- a/browser/base/content/utilityOverlay.js +++ b/browser/base/content/utilityOverlay.js @@ -258,14 +258,16 @@ function openLinkIn(url, where, params) {
// ContentClick.jsm passes isContentWindowPrivate for saveURL instead of passing a CPOW initiatingDoc if ("isContentWindowPrivate" in params) { - saveURL(url, null, null, true, true, aNoReferrer ? null : aReferrerURI, null, params.isContentWindowPrivate); + saveURL(url, null, null, true, true, aNoReferrer ? null : aReferrerURI, + null, params.isContentWindowPrivate, + aPrincipal || aTriggeringPrincipal); } else { if (!aInitiatingDoc) { Cu.reportError("openUILink/openLinkIn was called with " + "where == 'save' but without initiatingDoc. See bug 814264."); return; } - saveURL(url, null, null, true, true, aNoReferrer ? null : aReferrerURI, aInitiatingDoc); + saveURL(url, null, null, true, true, aNoReferrer ? null : aReferrerURI, aInitiatingDoc, params.isContentWindowPrivate, aPrincipal || aTriggeringPrincipal); } return; } diff --git a/dom/webbrowserpersist/nsIWebBrowserPersist.idl b/dom/webbrowserpersist/nsIWebBrowserPersist.idl index 8de84f5e2904..62ac1c1cd7bd 100644 --- a/dom/webbrowserpersist/nsIWebBrowserPersist.idl +++ b/dom/webbrowserpersist/nsIWebBrowserPersist.idl @@ -13,11 +13,12 @@ interface nsIWebProgressListener; interface nsIFile; interface nsIChannel; interface nsILoadContext; +interface nsIPrincipal;
/** * Interface for persisting DOM documents and URIs to local or remote storage. */ -[scriptable, uuid(8cd752a4-60b1-42c3-a819-65c7a1138a28)] +[scriptable, uuid(ccdbc750-be09-4f11-bb01-4e0a4db76c41)] interface nsIWebBrowserPersist : nsICancelable { /** No special persistence behaviour. */ @@ -112,6 +113,12 @@ interface nsIWebBrowserPersist : nsICancelable attribute nsIWebProgressListener progressListener;
/** + * This attribute can be used to set the loading principal + * of the document or URI to be persisted. + */ + attribute nsIPrincipal loadingPrincipal; + + /** * Save the specified URI to file. * * @param aURI URI to save to file. Some implementations of this interface diff --git a/dom/webbrowserpersist/nsWebBrowserPersist.cpp b/dom/webbrowserpersist/nsWebBrowserPersist.cpp index fd6d9d0d9315..4f49b63475f3 100644 --- a/dom/webbrowserpersist/nsWebBrowserPersist.cpp +++ b/dom/webbrowserpersist/nsWebBrowserPersist.cpp @@ -277,6 +277,7 @@ const char *kWebBrowserPersistStringBundle = nsWebBrowserPersist::nsWebBrowserPersist() : mCurrentDataPathIsRelative(false), mCurrentThingsToPersist(0), + mLoadingPrincipal(nsContentUtils::GetSystemPrincipal()), mFirstAndOnlyUse(true), mSavingDocument(false), mCancel(false), @@ -413,6 +414,19 @@ NS_IMETHODIMP nsWebBrowserPersist::SetProgressListener( return NS_OK; }
+NS_IMETHODIMP nsWebBrowserPersist::GetLoadingPrincipal(nsIPrincipal** loadingPrincipal) +{ + *loadingPrincipal = mLoadingPrincipal; + return NS_OK; +} + +NS_IMETHODIMP nsWebBrowserPersist::SetLoadingPrincipal(nsIPrincipal* loadingPrincipal) +{ + mLoadingPrincipal = loadingPrincipal ? loadingPrincipal : + nsContentUtils::GetSystemPrincipal(); + return NS_OK; +} + NS_IMETHODIMP nsWebBrowserPersist::SaveURI( nsIURI *aURI, nsISupports *aCacheKey, nsIURI *aReferrer, uint32_t aReferrerPolicy, @@ -1385,7 +1399,7 @@ nsresult nsWebBrowserPersist::SaveURIInternal( nsCOMPtr<nsIChannel> inputChannel; rv = NS_NewChannel(getter_AddRefs(inputChannel), aURI, - nsContentUtils::GetSystemPrincipal(), + mLoadingPrincipal, nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, nsIContentPolicy::TYPE_OTHER, nullptr, // aPerformanceStorage @@ -2756,7 +2770,7 @@ nsWebBrowserPersist::CreateChannelFromURI(nsIURI *aURI, nsIChannel **aChannel)
rv = NS_NewChannel(aChannel, aURI, - nsContentUtils::GetSystemPrincipal(), + mLoadingPrincipal, nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, nsIContentPolicy::TYPE_OTHER); NS_ENSURE_SUCCESS(rv, rv); diff --git a/dom/webbrowserpersist/nsWebBrowserPersist.h b/dom/webbrowserpersist/nsWebBrowserPersist.h index 17b570d783e7..f95300be12cb 100644 --- a/dom/webbrowserpersist/nsWebBrowserPersist.h +++ b/dom/webbrowserpersist/nsWebBrowserPersist.h @@ -147,6 +147,8 @@ private: nsCOMPtr<nsIMIMEService> mMIMEService; nsCOMPtr<nsIURI> mURI; nsCOMPtr<nsIWebProgressListener> mProgressListener; + nsCOMPtr<nsIPrincipal> mLoadingPrincipal; + /** * Progress listener for 64-bit values; this is the same object as * mProgressListener, but is a member to avoid having to qi it for each diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index d081fde9b20e..51fe1422acc5 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -886,10 +886,10 @@ var BrowserApp = { if (!permissionGranted) { return; } - + let doc = aTarget.ownerDocument; ContentAreaUtils.saveImageURL(aTarget.currentRequestFinalURI.spec, null, "SaveImageTitle", - false, true, aTarget.ownerDocument.documentURIObject, - aTarget.ownerDocument); + false, true, doc.documentURIObject, + null, null, null, doc.isPrivate, doc.nodePrincipal); }); });
diff --git a/netwerk/base/LoadContextInfo.cpp b/netwerk/base/LoadContextInfo.cpp index 79f870e8d20d..1218345b63ed 100644 --- a/netwerk/base/LoadContextInfo.cpp +++ b/netwerk/base/LoadContextInfo.cpp @@ -121,8 +121,6 @@ GetLoadContextInfo(nsIChannel * aChannel) { nsresult rv;
- DebugOnly<bool> pb = NS_UsePrivateBrowsing(aChannel); - bool anon = false; nsLoadFlags loadFlags; rv = aChannel->GetLoadFlags(&loadFlags); @@ -132,7 +130,21 @@ GetLoadContextInfo(nsIChannel * aChannel)
OriginAttributes oa; NS_GetOriginAttributes(aChannel, oa); - MOZ_ASSERT(pb == (oa.mPrivateBrowsingId > 0)); + +#ifdef DEBUG + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->GetLoadInfo(); + if (loadInfo) { + nsCOMPtr<nsIPrincipal> principal = loadInfo->LoadingPrincipal(); + if (principal) { + bool chrome; + principal->GetIsSystemPrincipal(&chrome); + if (!chrome) { + bool pb = NS_UsePrivateBrowsing(aChannel); + MOZ_ASSERT(pb == (oa.mPrivateBrowsingId > 0)); + } + } + } +#endif
return new LoadContextInfo(anon, oa); } diff --git a/toolkit/components/browser/nsWebBrowser.cpp b/toolkit/components/browser/nsWebBrowser.cpp index 40ac82210502..ff1e728243b4 100644 --- a/toolkit/components/browser/nsWebBrowser.cpp +++ b/toolkit/components/browser/nsWebBrowser.cpp @@ -997,6 +997,18 @@ nsWebBrowser::SetProgressListener(nsIWebProgressListener* aProgressListener) }
NS_IMETHODIMP +nsWebBrowser::GetLoadingPrincipal(nsIPrincipal** loadingPrincipal) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsWebBrowser::SetLoadingPrincipal(nsIPrincipal* loadingPrincipal) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsWebBrowser::SaveURI(nsIURI* aURI, nsISupports* aCacheKey, nsIURI* aReferrer, diff --git a/toolkit/content/contentAreaUtils.js b/toolkit/content/contentAreaUtils.js index 48cf448798a5..e213a0e4333e 100644 --- a/toolkit/content/contentAreaUtils.js +++ b/toolkit/content/contentAreaUtils.js @@ -62,14 +62,14 @@ function forbidCPOW(arg, func, argname) { // - A linked document using Alt-click Save Link As... // function saveURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache, - aSkipPrompt, aReferrer, aSourceDocument, aIsContentWindowPrivate) { + aSkipPrompt, aReferrer, aSourceDocument, aIsContentWindowPrivate, + aContentPrincipal) { forbidCPOW(aURL, "saveURL", "aURL"); forbidCPOW(aReferrer, "saveURL", "aReferrer"); // Allow aSourceDocument to be a CPOW. - internalSave(aURL, null, aFileName, null, null, aShouldBypassCache, aFilePickerTitleKey, null, aReferrer, aSourceDocument, - aSkipPrompt, null, aIsContentWindowPrivate); + aSkipPrompt, null, aIsContentWindowPrivate, aContentPrincipal); }
// Just like saveURL, but will get some info off the image before @@ -109,10 +109,12 @@ const nsISupportsCString = Ci.nsISupportsCString; * @param aIsContentWindowPrivate (bool) * Whether or not the containing window is in private browsing mode. * Does not need to be provided is aDoc is passed. + * @param aContentPrincipal [optional] + * The principal to be used for fetching and saving the target URL. */ function saveImageURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache, aSkipPrompt, aReferrer, aDoc, aContentType, aContentDisp, - aIsContentWindowPrivate) { + aIsContentWindowPrivate, aContentPrincipal) { forbidCPOW(aURL, "saveImageURL", "aURL"); forbidCPOW(aReferrer, "saveImageURL", "aReferrer");
@@ -156,7 +158,8 @@ function saveImageURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache,
internalSave(aURL, null, aFileName, aContentDisp, aContentType, aShouldBypassCache, aFilePickerTitleKey, null, aReferrer, - null, aSkipPrompt, null, aIsContentWindowPrivate); + null, aSkipPrompt, null, aIsContentWindowPrivate, + aContentPrincipal); }
// This is like saveDocument, but takes any browser/frame-like element @@ -170,7 +173,7 @@ function saveBrowser(aBrowser, aSkipPrompt, aOuterWindowID = 0) { let stack = Components.stack.caller; persistable.startPersistence(aOuterWindowID, { onDocumentReady(document) { - saveDocument(document, aSkipPrompt); + saveDocument(document, aSkipPrompt, aBrowser.contentPrincipal); }, onError(status) { throw new Components.Exception("saveBrowser failed asynchronously in startPersistence", @@ -186,7 +189,9 @@ function saveBrowser(aBrowser, aSkipPrompt, aOuterWindowID = 0) { // case "save as" modes that serialize the document's DOM are // unavailable. This is a temporary measure for the "Save Frame As" // command (bug 1141337) and pre-e10s add-ons. -function saveDocument(aDocument, aSkipPrompt) { +// +// aContentPrincipal is the principal for downloading and saving the document. +function saveDocument(aDocument, aSkipPrompt, aContentPrincipal) { if (!aDocument) throw "Must have a document when calling saveDocument";
@@ -241,7 +246,7 @@ function saveDocument(aDocument, aSkipPrompt) { internalSave(aDocument.documentURI, aDocument, null, contentDisposition, aDocument.contentType, false, null, null, aDocument.referrer ? makeURI(aDocument.referrer) : null, - aDocument, aSkipPrompt, cacheKey); + aDocument, aSkipPrompt, cacheKey, undefined, aContentPrincipal); }
function DownloadListener(win, transfer) { @@ -350,11 +355,13 @@ XPCOMUtils.defineConstant(this, "kSaveAsType_Text", kSaveAsType_Text); * This parameter is provided when the aInitiatingDocument is not a * real document object. Stores whether aInitiatingDocument.defaultView * was private or not. + * @param aContentPrincipal [optional] + * The principal to be used for fetching and saving the target URL. */ function internalSave(aURL, aDocument, aDefaultFileName, aContentDisposition, aContentType, aShouldBypassCache, aFilePickerTitleKey, aChosenData, aReferrer, aInitiatingDocument, aSkipPrompt, - aCacheKey, aIsContentWindowPrivate) { + aCacheKey, aIsContentWindowPrivate, aContentPrincipal) { forbidCPOW(aURL, "internalSave", "aURL"); forbidCPOW(aReferrer, "internalSave", "aReferrer"); forbidCPOW(aCacheKey, "internalSave", "aCacheKey"); @@ -423,6 +430,7 @@ function internalSave(aURL, aDocument, aDefaultFileName, aContentDisposition, let nonCPOWDocument = aDocument && !Cu.isCrossProcessWrapper(aDocument);
+ let isPrivate = aIsContentWindowPrivate; if (isPrivate === undefined) { isPrivate = aInitiatingDocument instanceof Ci.nsIDOMDocument @@ -440,6 +448,7 @@ function internalSave(aURL, aDocument, aDefaultFileName, aContentDisposition, sourcePostData: nonCPOWDocument ? getPostData(aDocument) : null, bypassCache: aShouldBypassCache, isPrivate, + loadingPrincipal: aContentPrincipal, };
// Start the actual save process @@ -476,10 +485,15 @@ function internalSave(aURL, aDocument, aDefaultFileName, aContentDisposition, * If true, the document will always be refetched from the server * @param persistArgs.isPrivate * Indicates whether this is taking place in a private browsing context. + * @param persistArgs.loadingPrincipal + * The principal assigned to the document being saved. */ function internalPersist(persistArgs) { var persist = makeWebBrowserPersist();
+ if (["http", "https", "ftp"].includes(persistArgs.sourceURI.scheme)) { + persist.loadingPrincipal = persistArgs.loadingPrincipal; + } // Calculate persist flags. const nsIWBP = Ci.nsIWebBrowserPersist; const flags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
tbb-commits@lists.torproject.org