Out with the old, in with the goo(gle)

Some time ago I reworked my home page to feature content from various other sites I post to (blogs, flickr, delicious) by using some JSON tricks to pull in their feeds. I blogged about how to do this with Feedburner’s JSON API, so that my actual page was just static HTML and all the work was done client-side.

Last week I decided to revisit this using Google’s new AJAX feeds API. Feedburner‘s API never seemed to be well supported (it came out of a hackathon) and it forced me to serialize my requests. In the process I neatened up a bunch of the code.

Google’s API is pretty straight-forward. It uses a loader model that is similar to Dojo‘s dojo.require, so you load the main Google library:

<script src="http://www.google.com/jsapi?key=YOURAPIKEY"
  type="text/javascript"></script>

and then ask it to load their feed library:

google.load('feeds', '1');

They have a handy way of setting a callback to be called when the libraries are loaded:

google.setOnLoadCallback(function () { /* just add code */ });

By putting all three of these together we have a straight-forward way to execute code at the right time.

I refactored the code that inserts the feed data into the page a lot. I fleshed out the concept of input filters from simply filtering the title to filtering the whole item objects. This allows for a more flexible transformation from the information that is presented in the RSS feeds to information that I want to present to visitors to my page. In practice I only used it to remove my name from Twitter updates. Instead of hard-coding the DOM node creation like I did in the previous version of the code I moved to a theme model. The theme function takes a feed entry and returns a DOM node to append to the target DOM node.

The flexibility of Google’s API let me abandon my separate code path for displaying my Flickr images. Previously I used Flickr’s own JSON feed API but since Google’s feed API supports returning RSS extensions I used the Flickr’s MediaRSS compliant feeds to show thumbnails and links. They even provide a handy cross-browser implementation of getElementsByTagNameNS (google.feeds.getElementsByTagNameNS) for us to use.

I’m tempted to write a client-side implementation of Jeremy Keith‘s lifestream thing using this API.

Take a look at the code running on my home page or check out the script.

Insecurity is Ruby on Rails Best Practice

Update: This post is really really out of date. Please disregard most of what’s written here.

Ruby on Rails by default encourages developers to develop insecure web applications. While it’s certainly possible to develop secure sites using the Rails framework you need to be aware of the issues at hand and many technologies that make Rails a powerful easy to use platform will work against you.

Cross Site Request Forgery
CSRF is the new bad guy in web application security. Everyone has worked out how to protect their SQL database from malicious input, and RoR saves you from ever having to worry about this. Cross site scripting attacks are dying and the web community even managed to nip most JSON data leaks in the bud.

Cross Site Request Forgery is very simple. A malicious site asks the user’s browser to carry out an action on a site that the user has an active session on and the victim site carries out that action believing that the user intended that action to occur. In other words the problem arises when a web application relies purely on session cookies to authenticate requests.

Let’s look at a simple example. I want my site to add me to your 37Signals Highrise contact list.

37 Signals Highrise
Working out how to do this is pretty simple. I go to Highrise and take a look at the “Add a person” form in Firebug or using View Source. It’s a fairly straightforward form that submits to http://ianloic.highrisehq.com/people and has a bunch of fields. I can easily recreate that form on my own server. Since Highrise uses a different domain for each user I ask the user to enter their domain name. I use the same username I use everywhere else so it should be pretty easy to fool my user into giving me their Highrise domain name. Most other sites do not have unique per-user URLs.

The page is here and the source is here. Provided you’re logged in and you enter your highrise domain correctly clicking “Add me” will add my name to your contacts list because 37Signals’ servers have no idea that the request is not coming from their application.

It’s fairly straight-forward to modify a form like this to automatically submit on page load. It’s pretty easy to put it in an hidden IFRAME so the user doesn’t even know what’s going on.

By using a unique per-user URL the guys at 37 signals have made this exploit non-trivial. This is not the default behavior from the Ruby on Rails framework. By default action URLs are very predictable. For example if we take a look at the social bookmarking site Magnolia we see predictable URLs all over the place.

Magnolia
As soon as you log in to Magnolia you are presented with a form for adding a bookmark by typing in its URL. This is a fantastic user experience. Unfortunately for Magnolia’s users anyone that submits this form on their behalf can add bookmarks and the form’s action (http://ma.gnolia.com/bookmarks/quicksave) is the same for all users. A trivial page that adds a bookmark to a visitor’s Magnolia account would look like this (try it):

<html>
    <head><title>Ma.gnolia</title></head>
    <body onload="document.getElementById('f').submit()">
        <form id="f" method="post"
                action="http://ma.gnolia.com/bookmarks/quicksave">
            <input type="text" value="http://ian.mckellar.org/"
                name="url" id="url" type="hidden"></input>
        </form>
    </body>
</html>

But it gets even worse. Since by default Rails allows GET as well as POST submissions you can call an action from an IMG tag (try it):

<img src="http://ma.gnolia.com/bookmarks/quicksave?url=http://ian.mckellar.org/">

Magnolia is not unique in the behavior. Unless a site’s developer has gone out of their way to prevent it, this class of attacks will affect every Rails site. Most of the popular sites I’ve looked at exhibit some vulnerabilities.

Other Rails sites I’ve looked at attempt to do their input validation in JavaScript rather than in Ruby which leaves them open to JavaScript injection and hence XSS attacks. This is a far more serious attack that I can cover separately if there is interest.

Solutions
Easy Solutions
There aren’t any good easy solutions to this. A first step is to do referrer checking on every request and block GET requests in form actions. Simply checking the domain on the referrer may not be enough security if there’s a chance that HTML could be posted somewhere in the domain by an attacker the application would be vulnerable again.

Better Solutions
Ideally we want a shared secret between the HTML that contains the form and the rails code in the action. We don’t want this to be accessible to third parties so serving as JavaScript isn’t an option. The way other platforms like Drupal achieve this is by inserting a hidden form field into every form that’s generated that contains a secret token, either unique to the current user’s current session or (for the more paranoid) also unique to the action. The action then has to check that the hidden token is correct before allowing processing to continue.

This is a pain in the arse to write by hand.

What is really required at the Rails level is a of form API that can generate and consume forms securely. In Drupal all form HTML is generated and parsed by the framework. This allows application developers to protect themselves from XSRF without even knowing they are.

There’s a plugin called Secure Action but I’m not sure how well it works. The dependence on a static shared salt rather than a randomly generated secret in the user’s session concerns me. The way it puts the signature in the URL makes me nervous too. It’s better than nothing though.

OpenID for the mathematically challenged

The other day I got the OpenID bee in my bonnet and grabbed James Walker‘s module and installed it on my server. Actually I grabbed it from CVS, and then discovered that the CVS version is half-ported to some new Drupal 6 form API, so I ended up using the DRUPAL-5 tag.

Anyway, I use Dreamhost which I love for many many reasons (primarilly it’s really cheap and seems to work really well). Unfortunately they don’t build their PHP with BCMath or even GMP, which means my PHP can’t do the hard math that’s required for crypto. Luckily there’s a mode of OpenID that doesn’t require any work on the relaying party side. So I made a small change that allows James’ module to work in this “dumb” mode.

Index: openid.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/openid/openid.install,v
retrieving revision 1.2
diff -u -p -r1.2 openid.install
--- openid.install      25 Mar 2007 06:38:00 -0000      1.2
+++ openid.install      16 May 2007 22:59:56 -0000
@@ -2,24 +2,6 @@

/**
- * OpenID module requires bcmath
- */
-function openid_requirements($phase) {
-  if ($phase == 'runtime') {
-    $requirements['bcmath']['title'] = t('BCMath');
-    if (function_exists('bcadd')) {
-      $requirements['bcmath']['severity'] = REQUIREMENT_OK;
-      $requirements['bcmath']['value'] = t('Enabled');
-    }
-    else {
-      $requirements['bcmath']['severity'] = REQUIREMENT_ERROR;
-      $requirements['bcmath']['description'] = t('OpenID needs the bcmath extension for encryption.');
-    }
-  }
-  return $requirements;
-}
-
-/**
* Implementation of hook_install
*/
function openid_install() {
Index: openid.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/openid/openid.module,v
retrieving revision 1.2
diff -u -p -r1.2 openid.module
--- openid.module       25 Mar 2007 06:38:00 -0000      1.2
+++ openid.module       16 May 2007 22:59:56 -0000
@@ -133,10 +133,14 @@ function openid_login_form_submit($formi

$idp_endpoint = $services[0]['uri'];
$_SESSION['openid_idp_endpoint'] = $idp_endpoint;
-  $assoc_handle = openid_association($claimed_id, $idp_endpoint);
-  if (empty($assoc_handle)) {
-    drupal_set_message(t('OpenID Association failed'), 'error');
-    return;
+
+  // if we have BCMath, we should use OpenID smart mode
+  if (function_exists('bcadd')) {
+      $assoc_handle = openid_association($claimed_id, $idp_endpoint);
+      if (empty($assoc_handle)) {
+        drupal_set_message(t('OpenID Association failed'), 'error');
+        return;
+      }
}

Also, I put the patch up on Drupal.org