Frame-Busting Gadgets

In the questions after my @media ajax talk, Simon Willison asked about frame busting. If gadgets sit inside iframes, what’s to stop them from busting the frame, i.e. replacing the container with another website. I notice he made a similar comment when OpenSocial came out. If a gadget can cause iGoogle to go away in place of another website, it could for example launch a phishing attack by presenting a mock iGoogle login page (“iGoogle has timed out. Please re-enter your username and password.”).

At a technical level, there is actually nothing to prevent this. There’s no magic in the container. IFrames are about as good as it gets in modern browser for taming the gadgets, but there are still problems with that model, and one of those is the ability for iframes to bust out. I decided to create a demo of this. The gadget is here. You can drop it into iGoogle and see for yourself. Video below:

[kml_flashembed movie=”http://picupper.com/video/framebust.swf” height=”700″ width=”400″ /]

(Parenthetically, the video is vertical. Why are videos always horizontal on the internets? It’s a computer, not a TV! It should be just as easy to embed an dodecahedron video if that’s what I fancy at the time.)

At this stage, there’s no technical way around this. One possibility would be for the container to do something in the rarely-used onexit() method, which runs just before the new page is loaded. However, as I learned with WebWait, which is also vulnerable to frame-busting, there’s nothing you can really do at that stage, and you can’t determine if the exit is happening because of a frame-busting event as opposed to the user just typing in a new URL. I suppose onexit() could still be used to log info to the server – if you knew all the times the container exits and which gadgets were present at that time, you might find certain (malicious) gadgets were present more times than you’d expect if they were present randomly. Although, I’m not sure about how reliable it is to make an Ajax call in the onexit(). (In this case, though, it would only need to work some of the time.)

In the future, the technical solution is Caja, if it proves to be production-ready. Caja ensures a safe subset of Javascript, so a container like iGoogle would only ever serve a gadget if it was verified to be safe.

Until then, we have to rely on “social” mechanisms. i.e. user comments in the catalogue and manual checking by container staff. A while ago, iGoogle made it harder to add a gadget directly by URL – you basically have to use the catalogue unless you’re a developer. So the catalogue is effectively acting as a whitelist – once found to be malicious, a gadget could be removed. This isn’t simple though – gadgets are dynamic and the catalogue is effectively just a list of URLs. A malicious author could have their gadget added to the catalogue and then change it, so the gadgets need constant review – a highly ineffective process.

I also mentioned in the presentation that “html” type gadgets are more recommended than “url” gadgets. This is partly because with html gadgets, the container at least has some idea of what’s happening inside the gadget – though not entirely, since the code can still reference external scripts and services. With “url” gadgets, what happens is entirely controlled by an external server.

Google Gadget API for Ajax Developers

I just made my first Google Gadget – a Digg roundup tool. Here are some quick notes on making a Google Gadget for Ajax developers. I assume you know Ajax and also that you’ve played around with iGoogle as a user.

What’s a Gadget?

  • The gadget is an XML file sitting on your server. In my case, http://ajaxify.com/run/widgets/google/diggroundup.xml. It will get cached, so effectively it must be a static file.
  • The user adds your gadget to their igoogle portal, or codes it into their own website, by specifying this URL (it may be done indirectly – via the gadget registry. You’ll appear in the registry if you’ve submitted your gadget to igoogle.)
  • The gadget is rendered as an iframe, so you have all the usual security constraints which stop you borking the portal and other gadgets. This also means you can’t communicate with other Gadgets other than via than remote calls to a common third-party server (or has anyone tried hooking them together using the iframe-fragment identifier hack? ).

The Gadget Content

  • The XML file contains three main sections: module settings, user preferences, and the iframe content.
  • Module settings (<ModulePrefs>) are the usual demographic stuff – module name, your email, blood type, etc. Mostly optional.
  • User preferences (<UserPref>) are things users can edit by hitting the Edit button. Note that these aren’t dynamic app variables like a user’s search query – they are longer-term preferences like background colour and number of results to show. Each time the user changes then, the entire gadget is reloaded, so you can assume in initialisation that the preferences have already been established, and you don’t need to bother checking if they’re changed. The nice thing about preferences is they’re declarative, so you don’t have to manage input and persistence. You just say “I need a number called resultAmount from the user” (<UserPref name="resultAmount" display_name="No. of Results" default_value="5" datatype="string" ></UserPref>). Later on, you’ll be able to interrogate a prefs object to get back the data.
  • The content. The content is pretty much the “body” part of an IFrame. ie. your html and … you’ll never expect this … the javascript. You probably expected the JS to be in a separate section of the XML file, but you simply include it using an inline script tag (<script tag="text/javascript">the script...</script>, wherever you fancy. The same goes for CSS – just include a <style type="text/css"> section at the start of your script or inline it. So much for unobtrusive Javascript/CSS, but you can and should still separate things out within your page content.

User Prefs example

This is what the user prefs looks like:

It comes from this definition:

  1. <UserPref name="bgColor" display_name="Background Color:" default_value="White" datatype="enum" >
  2.         <EnumValue value="White" />
  3.         <EnumValue value="Pink" />
  4.         <EnumValue value="Aqua" />
  5.         <EnumValue value="Lime" />
  6.         <EnumValue value="Yellow" />·
  7.         <EnumValue value="Orange" />
  8.         <EnumValue value="Black" />
  9.     </UserPref>
  10.    <UserPref name="container" display_name="Story Type:" default_value="" datatype="enum" >
  11.         <EnumValue value="" display_value="All" />
  12.         <EnumValue value="technology" display_value="Technology" />
  13.         <EnumValue value="science" display_value="Science" />
  14.         <EnumValue value="world_business" display_value="World Business" />
  15.         <EnumValue value="sports" display_value="Sports" />
  16.         <EnumValue value="entertainment" display_value="Entertainment" />·
  17.         <EnumValue value="gaming" display_value="Gaming" />
  18.     </UserPref>
  19.    <UserPref name="storyAmount" display_name="No. of Stories:" default_value="5" datatype="string" >
  20.    </UserPref>
  21.    <UserPref name="popularOnly" display_name="Popular Only?" default_value="true" datatype="bool" >
  22.    </UserPref>

and is accessed like this:

javascript

  1. var prefs = new _IG_Prefs(__MODULE_ID__);
  2. message.style.backgroundColor = prefs.getString("bgColor");

Remote Content

  • Your gadget probably takes remote content. Remember it’s a static XML file, so you can’t output such content on the fly – you have to use some kind of web remoting. Because of cross-domain restrictions, you will want to use a Cross-Domain Proxy. (I experimented with direct On-Demand Javascript, but no cigar.) Fortunately, Google provides a nice API for proxying, which also caches the content you’re grabbing. The main call is _IG_FetchContent(url, callbackFunc). The callback function is given the contents of URL. As mentioned in my previous post, this might cause some problems if you need to refresh the cache more than once an hour.

Development Tips

  • igoogle can give some pretty poor error messages when you’re just starting up. If there’s nothing at the URL you specify, you will get a weird XML parsing error rather than a good old 404 warning. I also found if you have certain errors such as a typo in your XML file, igoogle says the gadget doesn’t exist at that URL, even though in this case, it does! For those reasons, be sure to start with Google’s Hello World and work from there!
  • Develop on your server directly. (If the gadget is live, you might make a test copy of it for debugging.) You don’t want to be developing on your local box and continuously uploading/checking it in, because igoogle needs to fetch it from a URL each time you want to test it.
  • The secret sauce is this: The Developer Gadget. This is a special gadget provided by igoogle that, if you include it on your portal, will let you suppress caching of your app, which you absolutely have to do. It also makes it a bit easier to add a new gadget and view the gadget source code.
  • Speaking of gadget source code, you can view the entire gadget XML for any gadget you come across, so take advantage of that and avoid reinventing the wheel.

The Developer Gadget looks like this: