- API Authorization
- Best Practices
- Fine Grained Authorization
The Arc Browser Vulnerability Exposes the Inefficiency of Row-Level Security (RLS)
Learn what the latest Arc Browser vulnerability can teach us about the proper usage of row-level security.
Gabriel L. Manor
A week ago, a severe vulnerability was discovered in the widely used Arc browser. This vulnerability, which allowed malicious actors to run JavaScript code on other users' browsers, serves as a big red flag, reminding us of the limitations of row-level security (RLS). While RLS is useful for controlling access to specific subsets of our persisted data, it alone is not enough to save your application from sophisticated attacks.
In this blog post, we’ll explore the Arc browser vulnerability, how it was exploited, and why it underscores the need for more advanced, fine-grained security measures. We’ll also discuss what lessons developers can learn from this incident and why RLS, despite being a good security tool, isn’t a comprehensive solution for modern applications.
Understanding the Arc Browser Vulnerability
The Arc browser is popular among developers, largely due to its Boost feature, which allows users to run custom JavaScript code on websites. It allows developers to modify how a website looks by enabling dark mode or automating specific tasks. However, this flexibility also introduced a critical vulnerability.
Source: Arc Browser
Arc saves the custom JavaScript code (Boosts) in a Firestore database, a cloud database service provided by Firebase. Each piece of JavaScript is stored as a row in this database. The problem arose when a hacker discovered that any user could modify the "Creator ID" column in the database and potentially inject malicious code into another user's Arc browser—provided they knew the target's user ID, which wasn’t that hard to obtain.
Imagine a hacker having the ability to insert a script that reads all your emails, naughty Twitter chats with NSFW bots, or even passwords from certain websites. That’s exactly what could happen. This vulnerability allowed attackers to run unauthorized code in other users' browsers, posing a serious security risk. The Arc team has since patched the issue, but the incident serves as a warning about relying solely on row-level security for protection.
Row-Level Security: What It Is and Why It Fell Short
Row Level Security (RLS) is a security mechanism used to control access to specific rows of data in a database. It’s typically used to ensure that only the users who are authorized to see certain rows can access them. In theory, RLS seems like the perfect tool for this type of situation. In Arc’s case, the Boosts—those custom JavaScript snippets—were saved in rows in Firestore, and RLS was supposed to protect these rows from being modified by unauthorized users.
However, this incident exposed a critical flaw in relying solely on RLS. The vulnerability lay in the fact that while Arc used Firestore’s built-in RLS, they overlooked a simple yet crucial rule: preventing users from changing the Creator ID of a Boost. If Arc had implemented a rule ensuring that only the original creator could modify their Boost, the vulnerability could have been avoided entirely.
service cloud.firestore {
match /databases/{database}/boosts {
// Allow only authenticated content owners access
match /some_collection/{userId}/{documents=**} {
allow read, write: if request.auth != null && request.auth.uid == userId
}
}
}
RLS helps limit access to data at a granular level, but it’s far from a foolproof solution. Arc’s failure to implement this one rule highlights a major issue: row-level security, while useful, isn’t enough on its own to protect your application from user-centric exploits. When not implemented thoroughly, it can leave gaps in your security that attackers are quick to exploit.
The Limitations of Row-Level Security
Lack of Policy Streamlining
One of the biggest issues with row-level security is its isolation. RLS is data-centric, meaning it only controls access to specific rows in a single table or collection. However, real-world applications often need more than just simple data access rules. You may want to enforce policies that apply to multiple collections, tables, or even different types of data structures across your entire system. While you can potentially use some join mechanism to enforce cross-collection RLS rules, it is not intuitive, and people find themselves struggling with it.
In the case of Arc, IMO, they had this RLS rule applied to individual Boosts initially, but then a new feature or requirement came that asked to streamline it with some other data, and the rule wasn’t able to streamline this policy, ensuring that all related data was secured under a unified rule. If your application relies solely on RLS, things can quickly get complicated when you need to coordinate policies across different parts of the system. Managing multiple, fragmented policies is a challenge, and any oversight can create vulnerabilities, just like what happened with Arc.
Data-Centric Focus Instead of User-Centric
Another limitation of RLS is its focus on data rather than the user. In Arc’s case, the vulnerability exploited a gap in the control of the data (the Boosts stored in Firestore), but the real issue was the lack of focus on user actions. While RLS can enforce access rules based on data attributes (like the Creator ID), modern applications need more sophisticated, user-centric security models that consider user roles, actions, and relationships with the data.
For instance, instead of just verifying that the user ID matches the Creator ID, a more effective approach would be to use user-centric policies. These would account for what the user is actually doing with the data and the context of their actions. By focusing on the user’s role and behavior, you can create more granular and effective security policies that prevent issues like the one in Arc.
An example diagram of a user-centric fine-grained authorization flow in Banking System
The False Sense of Security in Backend Service Products
Lastly, tools like Firebase, which offer RLS out of the box, can give developers a false sense of security. It’s easy to assume that because the database offers security features like RLS, you’re fully protected. However, as Arc’s vulnerability showed, relying on backend service products can sometimes make you overlook the necessary permission flows and additional safeguards your application actually needs.
The fact that Firebase provides built-in security features doesn’t mean you should skip implementing your own security measures. Even though Firebase protects against issues like DDoS attacks or SQL injections, it’s not enough to trust these built-in tools blindly. Developers must take responsibility for properly implementing and maintaining security policies that go beyond the basic features provided by these services.
Moving Beyond Row-Level Security
To avoid similar security gaps that resulted from the Arc vulnerability, here are some best practices that can help you build a more secure and robust authorization framework:
Externalizing Authorization
One key takeaway from the Arc vulnerability is that authorization configuration should be centralized and externalized, not tied directly to the database. Just like you wouldn’t trust the database to handle user authentication, you shouldn’t rely solely on database-level security for authorization. In the case of Arc, their reliance on row-level security within Firestore was a weak point, allowing an attacker to manipulate database entries directly.
By externalizing authorization, you create a middleware layer that sits between the user and the database. This middleware can understand your data schema and check user permissions before allowing access or changes to the database. It ensures that every time a request is made, whether it’s to read, write, or modify data, the user’s permissions are verified first. With this approach, Arc wouldn’t have missed enforcing a key security rule like the Creator ID check because the policy would have been centralized, not embedded within the database structure.
Externalizing authorization also ensures you can modify and scale your security policies without rewriting parts of the database logic. It adds flexibility, making sure that all the necessary checks are done consistently, regardless of which part of the system is accessing the data.
It is important to mention that centralized policy configuration, doesn’t mean you should centralize the authorization enforcement or decision making. A powerful open source tool that can help you with centralize policy while decentralize decision and enforcement, is OPAL - Open Policy Administration Layer - that provide a unique approach for fine-grained authorization services. You can read more about OPAL (and consider supporting it with a star ⭐️) here: https://github.com/permitio/opal
Implementing User-Centric Policies
Instead of just thinking about data rows, a more effective security approach is to start with the user. User-centric security models, like Role-Based Access Control (RBAC) or Relationship-Based Access Control (ReBAC), place users at the center of the authorization process. This way, you’re defining what actions a user can take based on their role, relationships with other users, and their access to specific resources.
A basic example is RBAC, where users are assigned roles that dictate their permissions. Arc could have benefited from a model like this, where specific roles have distinct permissions to modify or create Boosts. As your application grows in complexity, you can extend RBAC to more fine-grained controls, like multi-tenancy RBAC, which allows you to manage permissions across different user groups or organizations.
ReBAC takes this a step further by considering the relationships between users, resources, and even instances of resources. With ReBAC, you can define rules based on how users interact with each other or specific pieces of data. This would allow for much more detailed security policies that reflect the complexity of real-world application interactions, something that row-level security alone cannot achieve.
Audit and Review Security Policies Regularly
Authorization is not a "set it and forget it" process. As your application evolves, so do your security needs. Regularly audit your security policies to ensure that they are up to date with current best practices and that no new vulnerabilities have been introduced.
In Arc’s case, they initially missed a key rule in their RLS implementation, which left them vulnerable to attack. By performing regular audits, you can catch oversights like these early and ensure that your application remains secure as it scales.
Don’t Rely Solely on Backend Services for Security
While backend services like Firebase offer security features like RLS, these should not be your sole line of defense. The security provided by backend services is helpful, but it’s not a complete solution. You still need to implement your own authorization logic and enforce rules at the application level to ensure that nothing slips through the cracks.
Arc’s over-reliance on Firestore’s row-level security left them vulnerable. The lesson here is to always take ownership of your security, even if you’re using trusted third-party services. In the following article, you’ll find a tutorial on how to add authorization middleware for another Backend as a Service product - Supabase - that would help you understand more this middleware idea: https://www.permit.io/blog/how-to-implement-rbac-in-supabase
Conclusion
While RLS is a valuable tool for controlling access to specific data, it has its limitations—especially when it comes to modern, complex applications that require user-centric and fine-grained security models.
To build a more secure application, it’s crucial to centralize authorization, focus on user roles and actions, and adopt advanced models like role-based access control (RBAC) or relationship-based access control (ReBAC). Regularly auditing your security policies and avoiding over-reliance on backend services like Firebase are also essential steps in maintaining strong security.
Permit.io offers a powerful, user-centric authorization solution that makes it easy to implement fine-grained permissions across your application. With Permit.io, you can externalize your authorization layer, integrate role-based and relationship-based access control, and scale your security as your application grows. Whether you’re managing permissions for internal users, customers, or third parties, Permit.io provides the tools to help you stay secure without the complexity.
Ready to secure your app and streamline your authorization? Try Permit.io for free today and take the next step in building a robust, scalable security model for your application.