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 can?
and 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 401 UNAUTHORIZED
.
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
The 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 can?
/ cannot?
syntax as mentioned above. In this case, you have to be an administrator user to create new articles, but anyone can view them.
Failing Deadly
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:
load_and_authorize_resource :user
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
Simple, right? 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 authorize
call?
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.
Wrap-Up
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!