This should really go without saying. However, I am constantly surprised at how many applications don’t follow this simple rule.
For those out of the loop, “fail-deadly” refers to a failure mode that is the exact opposite of “fail-safe”. A couple good examples of both systems are guns and locks. A gun that fails-safe, for instance, is rendered unfireable, whereas a fail-deadly gun would be primed to fire even if something was broken. Likewise, a fail-safe lock would remain locked even if something went wrong, whereas a fail-deadly lock would become unlocked. Of course, your perspective matters when looking at such things – “safe” for a lock that is securing an exit that needs to be used in an emergency might be to become unlocked.
A lock is a great analogy for what I’d like to talk about today because the point of this article is how I discovered a fail-deadly authorization system. In this case, I mean that a small developer error could lead to unintended access granted to a large number of people.
Let’s dig in and look at what a fail-deadly authorization system looks like at the code level.
Rails MVC Background
The application I’m going to be referring to is built in Rails which follows an MVC pattern.
request -> router -> controller -> view rendering ^ \ -> model < -- > database
Inside this application lives the 3rd-party authorization library – the CanCanCan gem. It’s a lightweight library that includes a DSL to allow a developer to define abilities through a
cannot? syntax. This also adds an extra authorization step to our MVC pattern above. Before anything else, the controller checks authorization and either allows the request to pass through or if authorization fails, redirects (usually to root) with
A simple controller and corresponding ability for
articles that includes CanCanCan authorization might look something like this:
# app/controllers/articles_controller.rb class ArticlesController < ApplicationController load_and_authorize_resource :article def new @article = Article.new end def create @article = Article.new(article_params) if @article.save redirect_to article_path(@article), notice: "Article created successfully" else render :new end end end # app/models/ability.rb class Ability def initialize(user) can :view, Article if user.admin? can :create, Article end end end
load_and_authorize_resource helper here is doing all the authorization work. It raises
CanCan::AccessDenied if the
authorize_resource call fails, and somewhere else (usually in the parent controller) this needs to be rescued such that a redirect away from the unauthorized path is performed.
The ability model here is defining the
cannot? syntax as mentioned above. In this case, you have to be an administrator user to create new articles, but anyone can view them.
Okay, we have our controller set up with the correct authorization and our ability model that defines permissions, so how does this fail deadly?
Well, this won’t. At least, not yet. Everything here is appropriately hooked up. (Herein also lies the problem with contrived examples like this – they’re too simple.)
We need to drastically up the complexity of our application by a few orders of magnitude. Imagine a situation with multiple teams of different backgrounds working on this application, with tens or hundreds of associated models, controllers and views. Imagine most of these also need to have specific authorization setups. This primes us for developer mistakes.
Now imagine that in one of these critical controllers, say for viewing private information on a user profile, some developer forgets a very important line:
What do you think would happen?
By omitting this line, every user with the ability to access your application now has the ability to view user profiles, even if you didn’t intend for them to.
CanCanCan also doesn’t offer us any protection from developer error in this case. This controller will work just fine as the developer continues to work on it and we won’t see any authorization errors. Even unit testing the controller won’t necessarily fail unless the developer writes exceptionally solid tests – in this case you’d need to test the condition that the user doesn’t have access and is redirected appropriately.
No errors are raised and monitoring is also unlikely to catch this unless you have a particularly robust setup.
This is what makes CanCanCan fail-deadly. In order to restrict access, you must remember to call the appropriate authorization helper or the action is effectively “unlocked” by default.
The Alternative Approach (Pundit)
Let’s look at a fail-safe approach as an alternative. Here I’d like to talk about Pundit, which is the gem that I’ve chosen to replace CanCanCan in this particular application.
It has a lot of advantages for my use case, but the relevant one for this issue is that it’s fail-safe through a controller callback called
verify_authorized. It even has a section in the README which I’m super thankful for.
In comparison to CanCanCan, Pundit has the concept of policies. You write a policy for a given model and tell it how to authorize your data, and then simply call the correct policy’s
authorize method. Aside from that, it is very similar to CanCanCan in that it will raise
Pundit::NotAuthorized just like
CanCan::AccessDenied, which can be rescued and redirected as before.
From the user profile example above, this might look like:
# app/policies/user_policy.rb class UserPolicy < ApplicationPolicy def show? user.admin? end end # app/controllers/users_controller.rb class UsersController < ApplicationController def show @user = authorize User.find(params[:id]) end end
show? in the policy matches up with
show in the controller, and Pundit knows exactly how to look up which policy to use to authorize your
User model. So what if we forgot that
Well, here’s the beauty of
verify_authorized. In the application controller, you can use an
after_action callback to ensure it wasn’t skipped:
# app/controllers/application_controller.rb class ApplicationController after_action :verify_authorized end
This will raise
Pundit::AuthorizationNotPerformed. It’s not as pretty as a nice redirect (redirecting in an
after_action typically results in a
AbstractController::DoubleRenderError due to the fact that the controller action has already completed rendering the view at this point), but it also means that even if a developer accidentally releases code with no authorization checks that it will simply lock everyone out of that feature.
This also drastically increases the visibility of the failure – unit tests should catch this mistake before it ever reaches production, even if the developer doesn’t initially notice. It’s also visible to monitoring tools. If nothing else, end users can now see it and report it to an engineer.
I hope this has shed some light on keeping critical applications safe, even when things go wrong. I also want to point out this is not a bash of CanCanCan – while I think it’s an inappropriate tool for our use case, it definitely has legitimate value if you understand its limitations.
If you liked this post, you may also like my last post Security and Swiss Cheese, so be sure to check it out.
That’s all for now. Thanks for reading!