DEV Community

Haseeb Annadamban
Haseeb Annadamban

Posted on • Edited on

Broken Access Control: What Is It and Why Does It Matter in your Rails application?

Access control is the backbone of web application security, ensuring that users only have access to the resources they are authorized to use. When this control mechanism is poorly implemented or overlooked, it can lead to disastrous consequences, leaving your application exposed to unauthorized access, data breaches, and even complete compromise. Understanding what is broken access control is essential for your application security. How serious is this?, Let’s find out.

What is access control

Access control is all about setting some rules for the requesting user can access a particular resource or action. Sometimes with additional decisions like how much we can allow to access. It is about restricting or allowing certain actions or resources based on user permissions and roles.

What is broken access control

Broken access control refers to either of not having expected level of access control or when an attacker is able to bypass the intended access control restrictions and gain unauthorized access to sensitive resources or actions.

This can happen due to various factors such as

  • Insufficient authorization checks
  • Lack of input validation
  • Poorly configured roles or permissions

The Basics of access control

I will discuss about the basics of access control first.

Authentication

Authentication is the first step in this. This process involves verifying the identity of a user. In Rails, authentication is often implemented using gems like Devise or Sorcery. Implementation of authentication mechanisms like password based authentication is beyond the scope of this.

Once a user is authenticated, they receive a session token or a cookie that allows them to be recognized as a valid user for subsequent requests. The most important thing from security perspective is, we should verify if the current user is authenticated for all sensitive resources. You can follow two approaches when using devise gem.

1. Authenticate when it is needed
we will use before_action :authenticate_user! wherever it is needed.
we will use before_action :authenticate_user! wherever it is needed.

class SensitiveController < ApplicationController
  before_action :authenticate_user!
end
Enter fullscreen mode Exit fullscreen mode

2. Authenticated by default

in this strategy we will define authenticate_user! on a base class,

class BaseAuthenticatedController < ApplicationController
  before_action :authenticate_user!
end
Enter fullscreen mode Exit fullscreen mode

And then all controllers that extends BaseAuthenticatedController (name it as per your requirement) will be authenticated by default:

class SensitiveController < BaseAuthenticatedController
  # No need to add before_action :authenticate_user! here
end
Enter fullscreen mode Exit fullscreen mode

For controllers with only public methods we will continue to extend ApplicationController:

class HomePageController < ApplicationController
end
Enter fullscreen mode Exit fullscreen mode

And we will skip authentication for public methods where BaseAuthenticatedController is used:

class SensitiveController < BaseAuthenticatedController
  skip_before_action :authenticate_user!, only: [:public_action]

  def sensitive_action
    # Authenticated as usual
  end

  def public_action
    # We explicitly skipped authentication for this endpoint.
  end
end
Enter fullscreen mode Exit fullscreen mode

Authorization

Authorization is the process of making sure the authenticated user has the necessary permissions to perform certain actions or access specific resources. In rails we have two equally famous gems for this at the time of writing this. CanCanCan and Pundit. I will mention either pundit or will write authorization code directly here for the sake of simplicity. Pundit gem focuses on providing explicit policy classes for each resource, making the authorization rules more organized and maintainable.

The basic guard against forgetting to add authorization in pundit is with verify_authorized. It will raise an error if authorization is not present for an action in the controller.

I would recommend to add it in BaseAuthenticatedController or ApplicationController to ensure we are always authorized.

class BaseAuthenticatedController < ApplicationController
  include Pundit::Authorization
  before_action :authenticate_user!
  after_action :verify_authorized
end
Enter fullscreen mode Exit fullscreen mode

Access control implementation

There are several ways to implement proper access control. Role based access control is considered the base of it all. I will explain about it and will mention some others.

Role Base Access Control (RBAC)

RBAC is a widely used access control model that focuses on organizing and managing user permissions based on their roles within an organization or system. In RBAC, users are assigned specific roles, and each role is associated with a set of permissions or privileges that determine what actions the user is allowed to perform on various resources.For example, you might have roles like "admin," "user," and "guest." Admins would have more privileges than regular users, who, in turn, would have more privileges than guests.

To implement RBAC

  1. You might add a role column as a string in your users table:
class AddRoleToUsers < ActiveRecord::Migration
  def change
    add_column :users, :role, :string, default: "user"
  end
end
Enter fullscreen mode Exit fullscreen mode
  1. In the policy, Check if a user has a specific role
class ArticlePolicy < ApplicationPolicy
  #
  # ....
  #
  def update?
    user.present? && (user.role == "admin" || user.role == "editor") # Only admins and editors can update articles
  end
end
Enter fullscreen mode Exit fullscreen mode
  1. Check this in your controller
class ArticlesController < BaseAuthenticatedController
  def update
    @article = Article.find(params[:id])
    authorize @article # This will make sure only the editors and admins are able to access the update
  end
end
Enter fullscreen mode Exit fullscreen mode

Others

Attribute-Based Access Control (ABAC)
ABAC is a more flexible access control model that takes into account various attributes of the user, the resource, and the environment to make access decisions. It uses a set of policies based on attributes such as user attributes (e.g., age, location, department), resource attributes (e.g., sensitivity, type), and environmental attributes (e.g., time of day, location). ABAC allows for more fine-grained and dynamic access control compared to RBAC.

Mandatory Access Control (MAC)
MAC is a strict access control model typically used in high-security environments such as government and military systems. Access decisions are based on sensitivity labels attached to users and resources. MAC enforces a hierarchical system of clearances and classifications, and it requires a centralized authority to manage access decisions.

Discretionary Access Control (DAC)
DAC is a more permissive access control model where resource owners have the discretion to control access to their resources. In DAC, access decisions are based on the identity of users and the permissions granted by the resource owner. It is commonly used in file systems and some traditional network access control scenarios.

Rule-Based Access Control (RBAC)

Rule based access control is a variation of RBAC, where permissions are defined using rules or policies. The rules can be more complex than simple role-to-permission mappings and can take various attributes and conditions into account. Access decisions are made based on rules defined by administrators or security experts. These rules can be more expressive and granular than typical RBAC models, providing greater control over access rights.

Hierarchical Role-Based Access Control (HRBAC)
HRBAC is an extension of RBAC that introduces role hierarchies. Roles are organized in a hierarchical structure, and permissions can be inherited from higher-level roles to lower-level roles. HRBAC simplifies the management of permissions and allows for a more flexible access control scheme.

History-Based Access Control (HBAC)
HBAC is an access control model that takes into account the historical behavior of users to make access decisions. It uses past user behavior patterns and access logs to determine access rights. HBAC is suitable for detecting anomalies and preventing insider threats.

Now that we have some strong basics, let’s proceed to discuss the vulnerabilities that can happen due to broken access control.

Insecure Direct Object Access (IDOR)

IDOR can be described as an attacker being able to bypass authorization and directly accesses resources in the system because user supplied arguments are used to give access to a particular resource or action and the attacker replaced them to some other value.

One reason for this is Not having an authorization at all on the server. It won’t help even if you have client side validation. Client side is not the right place to have the authorization mechanism. Consider this controller's update action which updates an article

class ArticlesController < ApplicationController
  before_action :authenticate_user!

  def update
    @post = Post.find(params[:id])
    @post.update(post_params)
    redirect_to @post
  end
end
Enter fullscreen mode Exit fullscreen mode

In this example, the update actions is vulnerable to IDOR. Although the authenticate_user! method ensures that a user is logged in, it doesn't verify that the logged-in user is the owner of the post they're trying to access.

This means that if an attacker can guess or otherwise discover the ID of a post that doesn't belong to them, they can simply add it in the URL path and edit any article.

PATCH /articles/:any_id
Enter fullscreen mode Exit fullscreen mode

This can be fixed by adding a PostPolicy and using that

class PostPolicy < ApplicationPolicy
  def update?
    record.user == user
  end
end
Enter fullscreen mode Exit fullscreen mode
class ArticlesController < ApplicationController
  before_action :authenticate_user!

  def update
    @post = Post.find(params[:id])
    authorize @post # Authorization here.
    @post.update(post_params)
    redirect_to @post
  end
end
Enter fullscreen mode Exit fullscreen mode

Second reason for IDOR is Inadequate Access Control. If an application doesn't implement access controls properly, a user might be able to escalate their privileges and access resources they're not supposed to. Usually happens due to bugs in code or developers not understanding or forgetting complex rules properly.

For example, Let's assume a forum software where A user can update if user is owner of the post or have more than 1000 reputation or is assigned an editor for a tag to edit a post in a forum but we have a bug in our code which instead of checking if user is editor for a tag, it checks if the user is added as an editor for any tags

class PostPolicy < ApplicationPolicy
  def update?
    user_is_owner? || user_has_high_reputation? || user_is_tag_editor?
  end

  private

  def user_is_owner?
    record.user == user
  end

  def user_has_high_reputation?
    user.reputation > 1000
  end

  def user_is_tag_editor?
    # Here it checks if the user is an editor for any tag. It is a bug
    user.editor_tags.any?
  end
end
Enter fullscreen mode Exit fullscreen mode

Having access control related tests are a great way to avoid this type of errors.

Privilege Escalation

Attackers could exploit weak access controls to elevate their privileges, gaining more access and control over a system than they should have. There are two types of privilege escalation.

Horizontal privilege escalation

This occurs when a user gains unauthorized access to another user's account of the same privilege level, often leading to unauthorized actions within the system.

For example in this comments controller we verify only admin users are able to access a post that is not a comment.

class PostsController < BaseAuthenticatedController
  before_action :set_post
  before_action :authorize_post_access

  def update
    if @post.update(post_params)
      redirect_to post_path(@post)
    else
      render :edit
    end
  end

  private

   def set_post
     @post = Post.find(params[:id])
   end

   def authorize_post_access
    if !current_user.admin? && @post.post_type != "comment"
      redirect_to root_path, alert: "You do not have access to this resource."
    end
   end
end
Enter fullscreen mode Exit fullscreen mode

Even though it is not possible to update other type of posts other than comment when a user is a non admin user, they can still update other users comments simply changing the comment id.

One way to fix is by adding an additional check in authorize_post_access

def authorize_post_access
   return if current_user.admin?

   if @post.post_type != "comment" || @post.author != current_user
     redirect_to root_path, alert: "You do not have access to this resource."
   end
end
Enter fullscreen mode Exit fullscreen mode

Vertical privilege escalation

This occurs when attackers exploit weak access controls to gain access to higher privilege levels, such as administrative accounts, allowing them to control the entire system.

In this example of a website like twitter, we have two endpoints block_user where any user can block any other user and ban_user where admin users can ban other users.

class UserActionsController < ApplicationController
  before_action :authenticate_user!

  # ...

  def block_user
    @user = User.find(params[:id])
    if current_user.id != @user.id  # Prevent blocking yourself
      @user.update(user_actions_params)
      redirect_to @user, notice: "User has been blocked."
    else
      redirect_to @user, alert: "You cannot block yourself."
    end
  end

  def ban_user
    @user = User.find(params[:id])
    if current_user.admin?
      @user.update(user_actions_params)
      redirect_to @user, notice: "User has been banned."
    else
      redirect_to @user, alert: "You do not have permission to ban users."
    end
  end

  private

  def user_actions_params
    params.require(:user).permit(:blocked, :banned, :follow)  # Mistake: used same method for both actions
  end
end
Enter fullscreen mode Exit fullscreen mode

Since update_user_params is used inside both ban_user and block_user , an attacker can use the param banned=true to ban any users.

Another example will be having a bug using which a user can make themselves admin. Suppose we have an option to update the user profile and the user is able to update the role parameter from there. Then an attacker can change the role to become an admin.

Conclusion

In conclusion, understanding and addressing broken access control is paramount for the security and integrity of your Rails application. This often overlooked vulnerability can lead to disastrous consequences, including unauthorized access, data breaches, and compromised user privacy. By implementing robust access control mechanisms and following best practices, you can significantly mitigate the risk of such attacks.

Prioritize user authentication, authorization, and having an access control mechanism suitable for your app to ensure that only authorized users can access and modify sensitive resources.

Top comments (0)