Web programming has been a pretty big deal for over 10 years now, but in some ways the tools web developers use haven't really progressed that much, particularly when it comes to security. For instance, CSRF and clickjacking are instances of the Confused Deputy problem, a security problem known since at least 1988.
Since these are both instances of the same underlying problem, in principle they should have the same or very similar solutions. However, the current solutions to these pervasive vulnerabilities are designed to solve only the specific problem at hand, and not the general problem of Confused Deputies. This means that if another HTML or Flash enhancement comes along that introduces another Confused Deputy, these solutions will not necessarily prevent exploitation of that vulnerability.
However, if we solve the underlying Confused Deputy problem, then that solution will address all present and future Confused Deputies, assuming any new features don't violate the constraints the solution specifies. Fortunately, the solution to the Confused Deputy has been known since it was first recognized: capabilities.
Capabilities
Capabilities are in fact a very simple and general framework for representing and reasoning about the flow of authority in a program. You've used capabilities many times without even knowing it. For instance, if you've ever received an activation e-mail with a long, unguessable URL, or if you've ever received a link to a Google Docs document.
There was no login requirement to load these links, no account information you had to input or password you had to provide. These types of links just bring you to the resource you want to access and whatever loads in your browser lets you know what you can do. Sometimes you can only read that resource, and sometimes there are some fields via which you can update the resource you're viewing, or other possibly update other resources. These types of URLs are capabilities.
The Confused Deputy problem inherently relies on references to resources being forgeable, like a file path, and the permissions for that resource being separate from the reference itself. The latter criterion is called "ambient authority" in capability parlance.
Capabilities solve the Confused Deputy because references are inherently unforgeable and include the permissions on that object. So if you have a capability, you have necessary and sufficient permission to operate on that object, period, full stop. I won't go into capabilities and Confused Deputies any further since there are plenty of online references describing them, but if there's enough interest I will write something up for a future post.
The quintessential capability web framework is Waterken, and its capabilities are called "web-keys".
Capabilities are not a panacea though. In the past, I found the Waterken programming model somewhat unintuitive. In my experience, most programming proceeds by designing a model that reflects the problem domain, which you then compose with models that cover other, often orthogonal, problem domains.
Example problem domains are data persistence, web request handling, and the usual models a programmer might have to write himself, like an order processing system. Most of these models are orthogonal, so each problem a developer needs to tackle can often be tackled independently, with a little glue to compose them into a final program.
Part of the problem with Waterken-style capabilities is that the data model of a program is more tightly coupled to the authorities as viewed by the user. These often take the form of capability patterns like "facets", which are used for attenuation, revocation and delegation of authority.
However, in traditional programming, security is most often modelled separately like every other concern, and evolves independently. This orthogonal thinking doesn't work with capabilities as they are currently provided. In Waterken, you have to compose the capability model and your data model in your head before you can even write any code, and this is why it's a little more difficult than necessary in my opinion. Combined with the fact that Waterken already has a built-in notion of persistence, and it's not exactly a drop-in solution.
However, it's undeniable that capabilities provide a natural framework for preventing Confused Deputies, so a good web framework will probably look a bit like capabilities.
Continuations
In my opinion, the biggest advance on the server-side was the development of continuations to model web interactions. If used correctly, they provide a natural framework for specifying and reasoning about the state needed by a request, and how that request can flow into other requests.
Every web program is unequivocally continuation-based, simply due to the stateless nature of HTTP and the various browser features, eg. back buttons. However, there have been many extensions to HTTP to try and mimic statefulness. Most of these extensions are still expressible as abstractions on top of continuations, although when these abstractions are used to carry credentials of various types, as is often done with cookies, a web program is susceptible to Confused Deputies [1].
Continuations are particularly advantageous in a statically typed language. A web framework in a typed language can define continuation abstractions specifying the static types of the parameters the continuation needs to satisfy a request. The framework itself can then guarantee that any requests for that resource have precisely the right types, and even the right names. This eliminates quite a bit of duplicate sanity checking code.
A good web framework will make the continuations inherent to web development somewhat obvious to maximize simplicity and flexibility.
Summary of Problems
Fundamentally, the problems with typical web frameworks are simple:
- Pervasive statefulness, ie. sessions and cookies.
- No standard, reliable way to protect parameters from tampering.
- Authorization context is ambient, leading to confused deputies like CSRF and clickjacking.
- No way to know at a glance which parameters require sanity checking, and which can be relied on.
Clavis
Taking the above lessons to heart, I present here a simplistic web security microframework. It trivially integrates with ASP.NET, but does not inherently depend on the page model. System.Web.Page already naturally satisfies Clavis' requirements though, so you can get right to developing your program.
What Clavis provides, addressing some of the failings of typical approaches:
- A session-free, ambient authority-free state model via continuations.
- A standard parameter protection semantics that prevents tampering, ie. parameters are unforgeable by default.
- Authorization context is explicit as a parameter. Combined with unforgeability, this automatically prevents Confused Deputies like CSRF and clickjacking.
- A parameter's forgeability is specified in the type and enforced by the compiler, so the need for sanity checking is obvious at a glance.
State Model - Continuations
The core concept is that of a set of interfaces defining continuations with various parameters required to load the page:
public interface IContinuation : ... { } public interface IContinuation<T> : ... { } public interface IContinuation<T0, T1> : ... { } public interface IContinuation<T0, T1, T2> : ... { } public interface IContinuation<T0, T1, T2, T3> : ... { } ...
A page class you define will then simply declare that it implements a certain continuation interface, and specify the types of the parameters the page requires to load:
public class Foo : System.Web.Page, IContinuation<User, SalesOrder> { ... }
The requirements on IContinuation are already satisfied by System.Web.Page, so there is nothing to implement. A page can also implement multiple continuation types, However, implementing the IContinuation interface gets you access to extension methods that allow you to name and parse parameters:
public class Foo : System.Web.Page, IContinuation<User, SalesOrder> { User user; SalesOrder order; protected void Page_Load(object sender, EventArgs e) { if (!this.TryParse0(out user, userId => LoadUser(userId))) throw ArgumentException("Invalid request!"); if (!this.TryParse1(out order, orderId => LoadOrder(orderId))) throw ArgumentException("Invalid request!"); } }
You can generate URLs from a continuation based on the page's fully qualified type name:
var link = Continuation.ToUrl<Foo, User, SalesOrder>( user.AsParam(), order.AsParam());
If you just want to redirect to a continuation:
Continuation.Display<Foo, User, SalesOrder>( user.AsParam(), order.AsParam());
For simplicity, Clavis requires that the fully qualified names of the continuations map to the URL path. So a continuation class Company.Warehouse.Foo, will map to ~/Company/Warehouse/Foo/. If the continuation is a System.Web.Page, you can simply place it under that directory and name it Default.aspx, and IIS will load it.
You can also customize the name used for a parameter if you really want to, although you must then keep the names used when generating the URL and the name used when parsing the parameter in sync.
Unforgeable Parameters
Each of a continuation's arguments are unforgeable, meaning that a URL constructed from a program is guaranteed to be free from tampering. A URL constructed for Foo above, might look something like this:
http://host.com/Foo/?!User=123&!SalesOrder=456&clavis=6dc8ca2b
Every query parameter prefixed with ! is guaranteed to be tamper proof, which means they can be used as unforgeable references. The unforgeability is guaranteed by the "clavis" query parameter, which is an HMAC of the URL path and the parameters prefixed by !.
By default, continuation arguments generate tamper-proof query parameters, with no further work on the developer's part. The developer simply needs to provide the functions to convert a type to and from a string that designates that type, ie. a UserId, a SalesOrderId, etc.
The query parameters are named according to the type of object that parameter is supposed to represent as specified in the IContinuation type definition. You should avoid using "int" or "string" since those aren't very meaningful. Foo from above generated "User" and "SalesOrder" parameters, but if I were instead write a class like this:
public class Meaningless : System.Web.Page, IContinuation<int, string> { ... }
you'd end up with a URL that isn't very meaningful:
http://host.com/Meaningless/?!int=123&!string=456&clavis=0a1a15d0
So instead of specifying that your continuation accepts integers or strings that represent an object in your data model, just specify the data model objects in your continuation as I did with Foo above.
Generating a URL for Foo from above is probably a little more complicated than I showed before, since you will often need to specify a string conversion:
var link = Continuation.ToUrl<Foo, User, SalesOrder>( user.AsParam(u => u.UserId), order.AsParam(o => o.OrderId));
Loading the parameter on the receiving end depends on the position in the continuation argument list. The first parameter is loaded via TryParse0, the second via TryParse1, and so on.
In general, the AsParam() extension methods are used to generate URL parameters with string conversions, and TryParseX() extension methods are to load data model objects from the continuation arguments via conversion from string. You could also add your own extension methods for AsParam() so you don't have to specify the string conversion every time. It's not quite so easy for TryParseX since you need to provide an overload for each possible argument index.
Explicit Authorization
Since all state is intended to be passed via continuation arguments, this includes any user identification or other authorization information, like a UserId:
http://host.com/Foo/?!User=123&!SalesOrder=456&clavis=6dc8ca2b
Because the User parameter is unforgeable, this means the explicit authorization information needed to operate on SalesOrder 456 is guaranteed to be reliable. No attacker will be able to generate the correct "clavis" parameter to forge such a request.
With plain continuations and unforgeable references discussed so far, all URLs are straightforward capabilities. This means if someone ever accidentally finds a legitimate URL, they can access the content at that URL.
Consider being on the above page for Foo, and it contains an external link. If you click that link, the server receiving the request will get the above URL in its entirety, as do any HTTP caches along the way if the connection isn't encrypted. It's a hideous security leak. There are plenty of techniques to mitigate this problem, but it's a general symptom of capabilities. For instance, capability URLs are also susceptible to shoulder-surfing attacks.
Clavis addresses this by providing a mechanism for tying its intrinsic capability URLs to "sessions". A session is basically just like an authentication cookie you would use on a typical website, except it need not carry any meaningful credentials. It's merely another random token that is included in the MAC so resulting URL is unique and tied to the lifetime of that token, and all such capability "leaks" are automatically plugged so long as the token is not also leaked.
The URL generated for a session-limited continuation will look like:
http://host.com/Foo/?!User=123&!SalesOrder=456&clavis-auth=faee8ce2
Notice that the "clavis" parameter is now called "clavis-auth", so Clavis knows that this request should include a session token in the MAC.
Clavis is agnostic about how this token is generated and stored on the server. You don't even have to store it at all, technically. The token can technically be anything, and you can even just use ASP.NET's FormsAuthentication cookies, or the ASP.NET session id for the token. This configuration happens on application load:
Continuation.Init(Convert.FromBase64String(clavisKey), () => { var x = HttpContext.Current.Session; return x == null ? "" : x.SessionID; });
"clavisKey" in the above is a 64-bit random byte array used as the private key for generating the MAC. Storing it as a parameter in the web.config file would do for most purposes. If you ever change this key, all previous URLs MAC'd with that key will break, but sometimes that's what you need. For instance, if this key is ever disclosed, you want to change it immediately to eliminate the threat.
Sanity Checked Parameters
By default, continuation arguments are unforgeable, so they don't require any sanity checking. This is unnecessarily restrictive however, since there are many HTTP patterns where we want the client to provide or modify some URL parameters. Ideally, we could easily identify such parameters so we can tell at a glance which ones need sanity checking.
In Clavis, this is achieved via Data<T>. A continuation whose argument type is wrapped in Data<T> will generate a forgeable query parameter, not an unforgeable one:
public class Foo2 : System.Web.Page, IContinuation<User, Data<SalesOrder>> { ... }
This indicates that the second argument of type SalesOrder is pure data, and not unforgeable, and so it requires strict sanity checking before being used. Foo2 will generate URL that looks like:
http://host.com/Foo2/!User=123&SalesOrder=456&clavis=8def944e
The SalesOrder parameter is no longer prefixed by !, and so it is not included in the MAC that prevents tampering. This is mostly useful in GET submissions like search forms, and allows a developer to see at a glance which arguments require sanity checking.
The Data<T> type itself is fairly straightforward wrapper:
public struct Data<T> : IRef<T>, IPureData, IEquatable<Data<T>> { ////// The encapsulated value. /// public T Value { get; set; } ... }
Using Clavis
You can obtain the Clavis binaries here. The Clavis API docs are here.
Simply link the Clavis and Sasa assemblies into your project, and put "using Clavis;" at the top of every file that's implementing or using continuations. I'm slowly migrating an old ASP.NET app to Clavis, demonstrating that it can be used incrementally in existing programs.
A few simple rules when using Clavis will maintain strong security at this layer of the stack:
- All state should preferably be transmitted via continuation parameters, ie. no use of the session or cookies. If you're careful, you can store credentials in the session token used by Clavis and remain safe, but only do this if you understand the serious dangers.
- Forgeable parameters should be used with extreme care, ie. Data<T>. In particular, don't use them to carry the authorization context or you are vulnerable to Confused Deputies, ie. a userId used to make access decisions should never be specified via Data<User>. Sanitize these parameters thoroughly.
- Use frameworks like Linq2Sql to ensure you're free of code injection vulnerabilities.
If you never use cookies or session state, your program will also automatically be RESTful. You still need to exercise care when sending content to clients to prevent XSS, but this happens at another layer that Clavis can't really help with.
Future Work
Clavis is a fairly simplistic approach to solving the issues I raised in this post, but this simplicity makes it reliable and easy to integrate with the existing ASP.NET stack. There are a few small limitations to Clavis as currently devised, some of which will be addressed shortly:
- The way type names are mapped to URLs might be changed to exploit ASP.NET's new and more flexible URL routing framework.
- There are only continuation types for up to 4 parameters. I intend to at least double that.
- Clavis currently depends minimally on the last stable release of Sasa. I will either eliminate this dependency, or update it when the new Sasa version is released shortly.
I welcome any comments or suggestions.
Footnotes
[1] Credentials in cookies are bundled separately from the URL to the resource being operated on, and URLs are typically forgeable -- classic Confused Deputy.
Comments