Skip to content
Permalink
Browse files
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
  • Loading branch information
arthuredelstein committed Jan 8, 2018
1 parent dca73c1 commit c79f2a21838c443ca10b0b0cd874f7e896fd5de0
@@ -5601,7 +5601,7 @@ function handleLinkClick(event, href, linkNode) {

if (where == "save") {
saveURL(href, linkNode ? gatherTextUnder(linkNode) : "", null, true,
true, doc.documentURIObject, doc);
true, doc.documentURIObject, doc, doc.nodePrincipal);
event.preventDefault();
return true;
}
@@ -1185,9 +1185,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);
},
@@ -1258,7 +1260,7 @@ nsContextMenu.prototype = {
// Helper function to wait for appropriate MIME-type headers and
// then prompt the user with a file picker
saveHelper: function(linkURL, linkText, dialogTitle, bypassCache, doc, docURI,
windowID, linkDownload) {
windowID, linkDownload, isContentWindowPrivate) {
// canonical def in nsURILoader.h
const NS_ERROR_SAVE_LINK_AS_TIMEOUT = 0x805d0020;

@@ -1317,7 +1319,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);
doc, isContentWindowPrivate);
}
if (this.extListener)
this.extListener.onStopRequest(aRequest, aContext, aStatusCode);
@@ -1358,16 +1360,16 @@ nsContextMenu.prototype = {
}
}

const principal = Services.scriptSecurityManager.createCodebasePrincipal(
makeURI(linkURL), this.principal.originAttributes);

// setting up a new channel for 'right click - save link as ...'
// ideally we should use:
// * doc - as the loadingNode, and/or
// * this.principal - as the loadingPrincipal
// for now lets use systemPrincipal to bypass mixedContentBlocker
// checks after redirects, see bug: 1136055
var channel = NetUtil.newChannel({
uri: makeURI(linkURL),
loadUsingSystemPrincipal: true
});
uri: makeURI(linkURL),
loadingPrincipal: principal,
securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER
});

if (linkDownload)
channel.contentDispositionFilename = linkDownload;
@@ -1410,7 +1412,8 @@ nsContextMenu.prototype = {
this.saveHelper(this.linkURL, this.linkTextStr, null, true, this.ownerDoc,
gContextMenuContentData.documentURIObject,
this.frameOuterWindowID,
this.linkDownload);
this.linkDownload,
PrivateBrowsingUtils.isBrowserPrivate(this.browser));
},

// Backwards-compatibility wrapper
@@ -1424,25 +1427,31 @@ nsContextMenu.prototype = {
let doc = this.ownerDoc;
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);
var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle";
this.saveHelper(this.mediaURL, null, dialogTitle, false, doc, referrerURI,
this.frameOuterWindowID, "");
this.frameOuterWindowID, "", isPrivate);
}
},

@@ -743,15 +743,16 @@ 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) {
if (aDirectory) {
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++) {
@@ -985,6 +985,18 @@ nsWebBrowser::SetProgressListener(nsIWebProgressListener* aProgressListener)
return NS_OK;
}

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,
@@ -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. */
@@ -111,6 +112,8 @@ interface nsIWebBrowserPersist : nsICancelable
*/
attribute nsIWebProgressListener progressListener;

attribute nsIPrincipal loadingPrincipal;

/**
* Save the specified URI to file.
*
@@ -274,6 +274,7 @@ const char *kWebBrowserPersistStringBundle =
nsWebBrowserPersist::nsWebBrowserPersist() :
mCurrentDataPathIsRelative(false),
mCurrentThingsToPersist(0),
mLoadingPrincipal(nsContentUtils::GetSystemPrincipal()),
mFirstAndOnlyUse(true),
mSavingDocument(false),
mCancel(false),
@@ -410,6 +411,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,
@@ -1352,7 +1366,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, // aLoadGroup
@@ -2696,7 +2710,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);
@@ -146,6 +146,8 @@ class nsWebBrowserPersist final : public nsIInterfaceRequestor,
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
@@ -118,8 +118,6 @@ GetLoadContextInfo(nsIChannel * aChannel)
{
nsresult rv;

DebugOnly<bool> pb = NS_UsePrivateBrowsing(aChannel);

bool anon = false;
nsLoadFlags loadFlags;
rv = aChannel->GetLoadFlags(&loadFlags);
@@ -129,7 +127,21 @@ GetLoadContextInfo(nsIChannel * aChannel)

NeckoOriginAttributes 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);
}
@@ -86,15 +86,15 @@ 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
@@ -137,7 +137,7 @@ const nsISupportsCString = Components.interfaces.nsISupportsCString;
*/
function saveImageURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache,
aSkipPrompt, aReferrer, aDoc, aContentType, aContentDisp,
aIsContentWindowPrivate)
aIsContentWindowPrivate, aContentPrincipal)
{
forbidCPOW(aURL, "saveImageURL", "aURL");
forbidCPOW(aReferrer, "saveImageURL", "aReferrer");
@@ -182,7 +182,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
@@ -199,7 +200,7 @@ function saveBrowser(aBrowser, aSkipPrompt, aOuterWindowID=0)
let stack = Components.stack.caller;
persistable.startPersistence(aOuterWindowID, {
onDocumentReady: function (document) {
saveDocument(document, aSkipPrompt);
saveDocument(document, aSkipPrompt, aBrowser.contentPrincipal);
},
onError: function (status) {
throw new Components.Exception("saveBrowser failed asynchronously in startPersistence",
@@ -215,7 +216,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)
{
const Ci = Components.interfaces;

@@ -273,7 +276,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) {
@@ -384,11 +387,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 loading 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");
@@ -460,6 +465,7 @@ function internalSave(aURL, aDocument, aDefaultFileName, aContentDisposition,
let nonCPOWDocument =
aDocument && !Components.utils.isCrossProcessWrapper(aDocument);


let isPrivate = aIsContentWindowPrivate;
if (isPrivate === undefined) {
isPrivate = aInitiatingDocument instanceof Components.interfaces.nsIDOMDocument
@@ -477,6 +483,7 @@ function internalSave(aURL, aDocument, aDefaultFileName, aContentDisposition,
sourcePostData : nonCPOWDocument ? getPostData(aDocument) : null,
bypassCache : aShouldBypassCache,
isPrivate : isPrivate,
loadingPrincipal : aContentPrincipal,
};

// Start the actual save process
@@ -513,11 +520,16 @@ 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 (persistArgs.sourceURI.scheme !== "file") {
persist.loadingPrincipal = persistArgs.loadingPrincipal;
}
// Calculate persist flags.
const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
const flags = nsIWBP.PERSIST_FLAGS_REPLACE_EXISTING_FILES |

0 comments on commit c79f2a2

Please sign in to comment.