20061014

A level of indirection

If you're a webapp developer, you've probably heard the term "a level of indirection". But it's really shocking to me how many applications actually fail to use it.

A level of indirection is one of the most basic, yet critical, markers of securing a web application. The way it works is for the user not to be in control of data they should not be in control of.

In this post, I'll give two examples of a level of indirection. First, I'll show you how you can use a level of indirection to prevent non-authenticated sites that have jump pages (the warning that says you're leaving this site) from becoming jump sites to arbitrary sites. In the second example, I'll show you how to prevent horizontal privilege escalation while actually simplifying the security checks involved in the workflow.

A level of indirection in jump pages
We've all seen those pages when you're on a website, it gives you a link to a news story or something, and you click a link that says "Warning, you're about to leave our site. We're not responsible for anything you read from here on out, even though we sent you there." Unfortunately, the way those are typically implemented is by creating a link that looks something like:

http://.../jumppage?url=http%3A%2F%2Fwww.google.com%2F

The url is then used in a meta tag to redirect the user after the warning page is displayed. The problem with these sorts of URL's is that the user is in control of the site they get jumped to. These types of URL's are frequently used in phishing attacks - the attacker sends the person to goodcompany.com's jump page, with a jump URL back to their own site. And it can be used for just plain brand confusion or degradation - "Hey - goodcompany.com has a link to badcompany.com".

The way to apply a level of indirection to this is to have a finite set of URL's the site will allow a visitor to jump to. Then, to render the jump page, you look up a url by index or key, rather than send the url directly. So if you want to be able to send user's to Google or Yahoo, your table would just have two entries:

1 http://www.google.com
2 http://www.yahoo.com


Then, the URL to the jump page says something like:
http://.../jumppage?urlindex=1
And to render the jump page, you look up the index the user sent in. If the user sent in an index for a site not in the list, you log the event and tell them no such page, or possibly blue screen their browser, or empty their account or something. (Unless there's the slightest possibility that your developers made a typo and put in a reference that doesn't exist).

This does have a cost, however. In a large development environment, all your developers have to know to register new URL's in the system and to use those ID's. This is only slightly more overhead, because they already know they have to use the jump page to begin with, right? And if you use some custom tags or library methods, you can still work them out to where this whole process is transparent to the developer. Like:

<myco:jump url="http://www.google.com">Go to Google<myco:jump>

The implementation for the tag in your upgraded model, looks up the URL, and renders the correct urlindex when it makes the A tags.

A level of indirection in permission checking
This solution is a little more expensive in computation and developer headache than the first, but it does simplify some permission checking later, and gives you a whole bunch of benefits. Take a CD catalog site for example, where users can keep a catalog of their own CD collection. The user is shown a list of their CD's, and each row in that table shows a link to edit or delete the CD.

Miles DavisKind of BlueEdit/Delete
The BeatlesWhite AlbumEdit/Delete
Jay-ZBlack AlbumEdit/Delete

The edit and delete links both look about the same - they end in something like id=3847, where the number is a unique identifier that identifies that exact CD, most often the primary key value to identify that record in the database. When a user clicks the edit link, they go to the edit page with that CD selected. I'll show you the solution to the problems before I show you the problems with it....

The way to add a level of indirection to this is to add stuff to the session that indicates what CD's are theirs. The indexes are independent of the actual primary key values - they just point to them. So if the three albums above are identified in the database by indexes 3847, 3902, and 9582, then you'd put something in session scope that looks like this:

0 3847
1 3902
2 9582

And the links for edit and delete would include the index, the receiving page looks up the element in the session by index, and then uses the primary key from there. To be truly effective, you need to make sure that on the edit page, for example, the primary key is kept in the session as well so that you don't have hidden form tags with the primary key so that a user can render the edit screen for their own CD, and then edit somebody else's.

The benefits of this are many:
  • Reduces the possibility of SQL injection because if the user includes SQL in the id index, they'll just get a number format exception or array index out of bounds
  • Reduces the number of times permission checks have to be performed. Ordinarily, when I render the list of items you have permission to, I have to find those. Then the edit page has to make sure you have permission to edit the item I'm rendering to you, and finally, the edit submit has to make sure you have permission to edit the item in question. With this approach, all of those checks are guranteed, because I wouldn't have put it in your session if you didn't have access to it. All of this results in a lower chance for horizontal privilege escalation.
  • Enforces a workflow. Now, I can't go to the edit page without first seeing the item I want to edit in a list. Or I can't submit an edit without having seen the form to edit the item.
There are a few of downsides to this:
  • You need to apply another flow enforcement on like delete links. If the user sends deleteid=0, and the delete function does the deletion of the first item in the session, then sends the user back to the rendering page, if they refresh, they keep deleting the first item in their collection, thereby reducing their collection over and over again - probably not what they want.
  • User's can't bookmark a link directly to a particular item anymore. If there's a view details page, the user must see the item in the list to get the right index to it, so they can't keep a link that includes the primary key.
  • You're keeping more information in the session now. If memory is a big concern, you may have to be clever about how you deal with this.

0 comments: