Bug 806037 - use nsIPrincipal for origin checks. r=gavin

This commit is contained in:
Mark Hammond 2012-12-27 18:28:49 +11:00
parent 3f3cb0b9df
commit 20dbf380a1
8 changed files with 239 additions and 117 deletions

View File

@ -70,12 +70,11 @@ function injectController(doc, topic, data) {
// Loads mozSocial support functions associated with provider into targetWindow
function attachToWindow(provider, targetWindow) {
// If the loaded document isn't from the provider's origin, don't attach
// the mozSocial API.
let origin = provider.origin;
// If the loaded document isn't from the provider's origin (or a protocol
// that inherits the principal), don't attach the mozSocial API.
let targetDocURI = targetWindow.document.documentURIObject;
if (provider.origin != targetDocURI.prePath) {
let msg = "MozSocialAPI: not attaching mozSocial API for " + origin +
if (!provider.isSameOrigin(targetDocURI)) {
let msg = "MozSocialAPI: not attaching mozSocial API for " + provider.origin +
" to " + targetDocURI.spec + " since origins differ."
Services.console.logStringMessage(msg);
return;
@ -125,10 +124,9 @@ function attachToWindow(provider, targetWindow) {
if (!chromeWindow.SocialFlyout)
return;
let url = targetWindow.document.documentURIObject.resolve(toURL);
let fullURL = ensureProviderOrigin(provider, url);
if (!fullURL)
if (!provider.isSameOrigin(url))
return;
chromeWindow.SocialFlyout.open(fullURL, offset, callback);
chromeWindow.SocialFlyout.open(url, offset, callback);
}
},
closePanel: {
@ -229,26 +227,6 @@ function getChromeWindow(contentWin) {
.getInterface(Ci.nsIDOMWindow);
}
function ensureProviderOrigin(provider, url) {
// resolve partial URLs and check prePath matches
let uri;
let fullURL;
try {
fullURL = Services.io.newURI(provider.origin, null, null).resolve(url);
uri = Services.io.newURI(fullURL, null, null);
} catch (ex) {
Cu.reportError("mozSocial: failed to resolve window URL: " + url + "; " + ex);
return null;
}
if (provider.origin != uri.prePath) {
Cu.reportError("mozSocial: unable to load new location, " +
provider.origin + " != " + uri.prePath);
return null;
}
return fullURL;
}
function isWindowGoodForChats(win) {
return win.SocialChatBar && win.SocialChatBar.isAvailable;
}
@ -289,10 +267,10 @@ this.openChatWindow =
chromeWindow = findChromeWindowForChats(chromeWindow);
if (!chromeWindow)
return;
let fullURL = ensureProviderOrigin(provider, url);
if (!fullURL)
let fullURI = provider.resolveUri(url);
if (!provider.isSameOrigin(fullURI))
return;
chromeWindow.SocialChatBar.openChat(provider, fullURL, callback, mode);
chromeWindow.SocialChatBar.openChat(provider, fullURI.spec, callback, mode);
// getAttention is ignored if the target window is already foreground, so
// we can call it unconditionally.
chromeWindow.getAttention();

View File

@ -246,6 +246,8 @@ function SocialProvider(input) {
this.workerURL = input.workerURL;
this.sidebarURL = input.sidebarURL;
this.origin = input.origin;
let originUri = Services.io.newURI(input.origin, null, null);
this.principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(originUri);
this.ambientNotificationIcons = {};
this.errorState = null;
this._active = ActiveProviders.has(this.origin);
@ -336,14 +338,16 @@ SocialProvider.prototype = {
reportError('images["' + sub + '"] is missing or not a non-empty string');
return;
}
// resolve potentially relative URLs then check the scheme is acceptable.
url = Services.io.newURI(this.origin, null, null).resolve(url);
let uri = Services.io.newURI(url, null, null);
if (!uri.schemeIs("http") && !uri.schemeIs("https") && !uri.schemeIs("data")) {
reportError('images["' + sub + '"] does not have a valid scheme');
// resolve potentially relative URLs but there is no same-origin check
// for images to help providers utilize content delivery networks...
// Also note no scheme checks are necessary - even a javascript: URL
// is safe as gecko evaluates them in a sandbox.
let imgUri = this.resolveUri(url);
if (!imgUri) {
reportError('images["' + sub + '"] is an invalid URL');
return;
}
promptImages[sub] = url;
promptImages[sub] = imgUri.spec;
}
for (let sub of ["shareTooltip", "unshareTooltip",
"sharedLabel", "unsharedLabel", "unshareLabel",
@ -451,5 +455,52 @@ SocialProvider.prototype = {
return null;
return getFrameWorkerHandle(this.workerURL, window,
"SocialProvider:" + this.origin, this.origin).port;
},
/**
* Checks if a given URI is of the same origin as the provider.
*
* Returns true or false.
*
* @param {URI or string} uri
*/
isSameOrigin: function isSameOrigin(uri, allowIfInheritsPrincipal) {
if (!uri)
return false;
if (typeof uri == "string") {
try {
uri = Services.io.newURI(uri, null, null);
} catch (ex) {
// an invalid URL can't be loaded!
return false;
}
}
try {
this.principal.checkMayLoad(
uri, // the thing to check.
false, // reportError - we do our own reporting when necessary.
allowIfInheritsPrincipal
);
return true;
} catch (ex) {
return false;
}
},
/**
* Resolve partial URLs for a provider.
*
* Returns nsIURI object or null on failure
*
* @param {string} url
*/
resolveUri: function resolveUri(url) {
try {
let fullURL = this.principal.URI.resolve(url);
return Services.io.newURI(fullURL, null, null);
} catch (ex) {
Cu.reportError("mozSocial: failed to resolve window URL: " + url + "; " + ex);
return null;
}
}
}

View File

@ -102,19 +102,18 @@ WorkerAPI.prototype = {
case "link":
// if there is a url, make it open a tab
if (actionArgs.toURL) {
try {
let pUri = Services.io.newURI(provider.origin, null, null);
let nUri = Services.io.newURI(pUri.resolve(actionArgs.toURL),
null, null);
// fixup
if (nUri.scheme != pUri.scheme)
nUri.scheme = pUri.scheme;
if (nUri.prePath == provider.origin) {
let xulWindow = Services.wm.getMostRecentWindow("navigator:browser");
xulWindow.openUILinkIn(nUri.spec, "tab");
}
} catch(e) {
Cu.reportError("social.notification-create error: "+e);
let uriToOpen = provider.resolveUri(actionArgs.toURL);
// Bug 815970 - facebook gives us http:// links even though
// the origin is https:// - so we perform a fixup here.
let pUri = Services.io.newURI(provider.origin, null, null);
if (uriToOpen.scheme != pUri.scheme)
uriToOpen.scheme = pUri.scheme;
if (provider.isSameOrigin(uriToOpen)) {
let xulWindow = Services.wm.getMostRecentWindow("navigator:browser");
xulWindow.openUILinkIn(uriToOpen.spec, "tab");
} else {
Cu.reportError("Not opening notification link " + actionArgs.toURL
+ " as not in provider origin");
}
}
break;

View File

@ -6,70 +6,6 @@ const TEST_PROVIDER_ORIGIN = 'http://example.com';
Cu.import("resource://gre/modules/Services.jsm");
// A mock notifications server. Based on:
// dom/tests/mochitest/notification/notification_common.js
const FAKE_CID = Cc["@mozilla.org/uuid-generator;1"].
getService(Ci.nsIUUIDGenerator).generateUUID();
const ALERTS_SERVICE_CONTRACT_ID = "@mozilla.org/alerts-service;1";
const ALERTS_SERVICE_CID = Components.ID(Cc[ALERTS_SERVICE_CONTRACT_ID].number);
function MockAlertsService() {}
MockAlertsService.prototype = {
showAlertNotification: function(imageUrl, title, text, textClickable,
cookie, alertListener, name) {
let obData = JSON.stringify({
imageUrl: imageUrl,
title: title,
text:text,
textClickable: textClickable,
cookie: cookie,
name: name
});
Services.obs.notifyObservers(null, "social-test:notification-alert", obData);
if (textClickable) {
// probably should do this async....
alertListener.observe(null, "alertclickcallback", cookie);
}
alertListener.observe(null, "alertfinished", cookie);
},
QueryInterface: function(aIID) {
if (aIID.equals(Ci.nsISupports) ||
aIID.equals(Ci.nsIAlertsService))
return this;
throw Cr.NS_ERROR_NO_INTERFACE;
}
};
var factory = {
createInstance: function(aOuter, aIID) {
if (aOuter != null)
throw Cr.NS_ERROR_NO_AGGREGATION;
return new MockAlertsService().QueryInterface(aIID);
}
};
function replacePromptService() {
Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
.registerFactory(FAKE_CID, "",
ALERTS_SERVICE_CONTRACT_ID,
factory)
}
function restorePromptService() {
Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
.registerFactory(ALERTS_SERVICE_CID, "",
ALERTS_SERVICE_CONTRACT_ID,
null);
}
// end of alerts service mock.
function ensureProvider(workerFunction, cb) {
let manifest = {
origin: TEST_PROVIDER_ORIGIN,
@ -90,8 +26,8 @@ function test() {
let cbPostTest = function(cb) {
SocialService.removeProvider(TEST_PROVIDER_ORIGIN, function() {cb()});
};
replacePromptService();
registerCleanupFunction(restorePromptService);
replaceAlertsService();
registerCleanupFunction(restoreAlertsService);
runTests(tests, undefined, cbPostTest);
}

View File

@ -7,6 +7,9 @@ let provider;
function test() {
waitForExplicitFinish();
replaceAlertsService();
registerCleanupFunction(restoreAlertsService);
let manifest = {
origin: 'http://example.com',
name: "Example Provider",
@ -144,5 +147,62 @@ let tests = {
}
}
port.postMessage({topic: "test-initialization"});
}
},
testNotificationLinks: function(next) {
let port = provider.getWorkerPort();
let data = {
id: 'an id',
body: 'the text',
action: 'link',
actionArgs: {} // will get a toURL elt during the tests...
}
let testArgs = [
// toURL, expectedLocation, expectedWhere]
["http://example.com", "http://example.com/", "tab"],
// bug 815970 - test that a mis-matched scheme gets patched up.
["https://example.com", "http://example.com/", "tab"],
// check an off-origin link is not opened.
["https://mochitest:8888/", null, null]
];
// we monkey-patch openUILinkIn
let oldopenUILinkIn = window.openUILinkIn;
registerCleanupFunction(function () {
// restore the monkey-patch
window.openUILinkIn = oldopenUILinkIn;
});
let openLocation;
let openWhere;
window.openUILinkIn = function(location, where) {
openLocation = location;
openWhere = where;
}
// the testing framework.
let toURL, expectedLocation, expectedWhere;
function nextTest() {
if (testArgs.length == 0) {
port.close();
next(); // all out of tests!
return;
}
openLocation = openWhere = null;
[toURL, expectedLocation, expectedWhere] = testArgs.shift();
data.actionArgs.toURL = toURL;
port.postMessage({topic: 'test-notification-create', data: data});
};
port.onmessage = function(evt) {
if (evt.data.topic == "did-notification-create") {
is(openLocation, expectedLocation, "url actually opened was " + openLocation);
is(openWhere, expectedWhere, "the url was opened in a " + expectedWhere);
nextTest();
}
}
// and kick off the tests.
port.postMessage({topic: "test-initialization"});
nextTest();
},
};

View File

@ -59,3 +59,66 @@ function runTests(tests, cbPreTest, cbPostTest, cbFinish) {
}
runNextTest();
}
// A mock notifications server. Based on:
// dom/tests/mochitest/notification/notification_common.js
const FAKE_CID = Cc["@mozilla.org/uuid-generator;1"].
getService(Ci.nsIUUIDGenerator).generateUUID();
const ALERTS_SERVICE_CONTRACT_ID = "@mozilla.org/alerts-service;1";
const ALERTS_SERVICE_CID = Components.ID(Cc[ALERTS_SERVICE_CONTRACT_ID].number);
function MockAlertsService() {}
MockAlertsService.prototype = {
showAlertNotification: function(imageUrl, title, text, textClickable,
cookie, alertListener, name) {
let obData = JSON.stringify({
imageUrl: imageUrl,
title: title,
text:text,
textClickable: textClickable,
cookie: cookie,
name: name
});
Services.obs.notifyObservers(null, "social-test:notification-alert", obData);
if (textClickable) {
// probably should do this async....
alertListener.observe(null, "alertclickcallback", cookie);
}
alertListener.observe(null, "alertfinished", cookie);
},
QueryInterface: function(aIID) {
if (aIID.equals(Ci.nsISupports) ||
aIID.equals(Ci.nsIAlertsService))
return this;
throw Cr.NS_ERROR_NO_INTERFACE;
}
};
var factory = {
createInstance: function(aOuter, aIID) {
if (aOuter != null)
throw Cr.NS_ERROR_NO_AGGREGATION;
return new MockAlertsService().QueryInterface(aIID);
}
};
function replaceAlertsService() {
Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
.registerFactory(FAKE_CID, "",
ALERTS_SERVICE_CONTRACT_ID,
factory)
}
function restoreAlertsService() {
Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
.registerFactory(ALERTS_SERVICE_CID, "",
ALERTS_SERVICE_CONTRACT_ID,
null);
}
// end of alerts service mock.

View File

@ -42,6 +42,11 @@ onconnect = function(e) {
// start
apiPort.postMessage({topic: 'social.reload-worker'});
break;
case "test-notification-create":
apiPort.postMessage({topic: 'social.notification-create',
data: data});
testerPort.postMessage({topic: 'did-notification-create'});
break;
}
}
// used for "test-reload-worker"

View File

@ -32,6 +32,8 @@ function run_test() {
runner.appendIterator(testGetProviderList(manifests, next));
runner.appendIterator(testEnabled(manifests, next));
runner.appendIterator(testAddRemoveProvider(manifests, next));
runner.appendIterator(testIsSameOrigin(manifests, next));
runner.appendIterator(testResolveUri (manifests, next));
runner.next();
}
@ -136,3 +138,31 @@ function testAddRemoveProvider(manifests, next) {
newProvider = yield SocialService.getProvider(newProvider.origin, next);
do_check_true(!newProvider);
}
function testIsSameOrigin(manifests, next) {
let providers = yield SocialService.getProviderList(next);
let provider = providers[0];
// provider.origin is a string.
do_check_true(provider.isSameOrigin(provider.origin));
do_check_true(provider.isSameOrigin(Services.io.newURI(provider.origin, null, null)));
do_check_true(provider.isSameOrigin(provider.origin + "/some-sub-page"));
do_check_true(provider.isSameOrigin(Services.io.newURI(provider.origin + "/some-sub-page", null, null)));
do_check_false(provider.isSameOrigin("http://something.com"));
do_check_false(provider.isSameOrigin(Services.io.newURI("http://something.com", null, null)));
do_check_false(provider.isSameOrigin("data:text/html,<p>hi"));
do_check_true(provider.isSameOrigin("data:text/html,<p>hi", true));
do_check_false(provider.isSameOrigin(Services.io.newURI("data:text/html,<p>hi", null, null)));
do_check_true(provider.isSameOrigin(Services.io.newURI("data:text/html,<p>hi", null, null), true));
// we explicitly handle null and return false
do_check_false(provider.isSameOrigin(null));
}
function testResolveUri(manifests, next) {
let providers = yield SocialService.getProviderList(next);
let provider = providers[0];
do_check_eq(provider.resolveUri(provider.origin).spec, provider.origin + "/");
do_check_eq(provider.resolveUri("foo.html").spec, provider.origin + "/foo.html");
do_check_eq(provider.resolveUri("/foo.html").spec, provider.origin + "/foo.html");
do_check_eq(provider.resolveUri("http://somewhereelse.com/foo.html").spec, "http://somewhereelse.com/foo.html");
do_check_eq(provider.resolveUri("data:text/html,<p>hi").spec, "data:text/html,<p>hi");
}