In this article, we examine the topic of access control and how to provide a robust level of security for applications.
First, we briefly define broken access control. Then, we illustrate, with examples, what broken access control looks like and what vulnerabilities they target. Finally, we offer some mitigating solutions for those vulnerabilities.
If you have no experience working with Node.js, we highly suggest you explore this introduction to Node.js; some aspects we’ll discuss might not be immediately clear unless you have some background in the technology.
With that out of the way, let's jump right in.
Access Control 101
Simply speaking, access control describes a set of policies and mechanisms implemented to supervise and control access to resources on a system. You might know access control by another name: authorization.
Once a server determines your identity through some login or authentication mechanism, it grants or limits what functions and resources you can access within the system. In addition, we usually use access control for user tracking.
Despite the simplicity of its concepts, appropriately implementing a robust and secure access control system is very difficult. Access control is closely bound to a system’s architecture. Moreover, users regularly fall into more than one role, making access management more complex. Therefore, in general, we discourage developing access control from scratch, and instead, we recommend adopting battle-tested, robust solutions like OAuth 2.0 and JWT.
Explaining Broken Access Control
Broken access control comprises a set of known exploits that can represent a threat to your systems' control over resource access.
Despite easy exploitation of many access control vulnerabilities if neglected, you can address them relatively quickly. This point is important because the consequences of a breach in access control can be pretty destructive — attackers can essentially take over your system.
Common Broken Access Control Vulnerabilities
As mentioned in our previous articles, attackers can exploit multiple vulnerabilities depending on your technology stack and architecture. However, in this article, for brevity, we focus only on insecure IDs, path traversal, file permission, and client caching.
Here's a refresher on the common vulnerabilities of access control.
Insecure ID Vulnerability
Most modern websites use some form of ID or index to quickly and efficiently refer to data. For most circumstances, this works satisfactorily. However, if the IDs are easy to figure out, either by hand or brute force, then you have a security problem on your hands.
To illustrate, imagine you have a profile page section where the user profile is displayed. Then, this URL retrieves your user profile:
https://www.mywebsite.com/profile?id=123
Now, if I were to change the number in the query string manually, and no active access control were in place to validate my request, I could access any profile.
Path Traversal Vulnerability
The concept of Path Traversal defines the capacity of a user to navigate a filesystem's directory tree freely.
A system without proper Access Control might be a victim of Path Traversal exploits and allow attackers to access restricted resources in the server.
This situation can happen when the system allows users to retrieve the path of a resource, an attacker modifies the resource path, and the system does not adequately validate the modification. To see an illustration, please refer to this link.
https://www.mywebsite.com/photos?file=user.png
If an attacker changes user.png to something like ../../etc/passwd, they could access the application’s secrets.
File Permission Vulnerability
A file permission vulnerability relates to a weakness in the mechanism that grants permission to specific users to access specific files on a server.
All web applications contain critical configuration files and resources that it should keep inaccessible to users. However, when an application lacks proper configuration policies, an attacker can target these files and take over the entire platform.
To illustrate this, here's an example of an attack attempting to access a file that should be inaccessible.
https://www.mywebsite.com/photos?file=../../gradle.json
Client Caching Vulnerability
Client caching vulnerabilities are tough to address because they involve attackers physically taking over a user’s computer. However, instead of attacking remotely, bad actors take advantage of session credentials, cached pages, or data already present in the browser of an authenticated client. Once this happens, the attacker has compromised the server and can access user data. Game over.
Addressing Broken Access Control
Unfortunately, Node.js doesn’t provide a native way to implement a robust web application authentication system. So, we need to implement it ourselves. In this case, we’ll use Auth0 to implement authentication as a single sign-on service (SSO service) with a robust and reliable third-party solution. Additionally, we’ll use Passport as our middleware library to handle the authentication and authorization processes.
First, go to the Auth0 website and register a new application. If you don't have an Auth0 account, you can sign up here.
Once in the Auth0 dashboard, go to the Applications section and click on the Create application button. Input a name for your application and make sure to choose Regular web applications as the application type.
Lastly, click the Create button.
After creating the application, go to the Settings tab and get your Auth0 domain and client id. Then, set Allowed callback URLs to http://localhost:3000/oauth2/redirect and Allowed logout URLs to http://localhost:3000/.
The first URL tells Auth0 where to redirect the user after authentication, and the second URL tells Auth0 where to redirect the user after logout.
Lastly, save your changes.
Implementing Authentication Middleware
Now, in your project, go to your .env file (create one by using the touch .env command if you don't already have one) and add the following keys.
AUTH0_DOMAIN=__INSERT_DOMAIN_HERE__
AUTH0_CLIENT_ID=__INSERT_CLIENT_ID_HERE__
AUTH0_CLIENT_SECRET=__INSERT_CLIENT_SECRET_HERE__
When finished, proceed to install the passport middleware with the following commands:
$ npm install passport
$ npm install passport-openidconnect
$ npm install express-session
$ npm install connect-sqlite3
Next, create a class file called auth.js in the routes folder and add the following code:
var OpenIDConnectStrategy = require('passport-openidconnect');
passport.use(new OpenIDConnectStrategy({
issuer: 'https://' + process.env['AUTH0_DOMAIN'] + '/',
authorizationURL: 'https://' + process.env['AUTH0_DOMAIN'] + '/authorize',
tokenURL: 'https://' + process.env['AUTH0_DOMAIN'] + '/oauth/token',
userInfoURL: 'https://' + process.env['AUTH0_DOMAIN'] + '/userinfo',
clientID: process.env['AUTH0_CLIENT_ID'],
clientSecret: process.env['AUTH0_CLIENT_SECRET'],
callbackURL: '/oauth2/redirect',
scope: [ 'profile' ]
},
function verify(issuer, profile, cb) {
return cb(null, profile);
}));
passport.serializeUser(function(user, cb) {
process.nextTick(function() {
cb(null, { id: user.id, username: user.username, name: user.displayName });
});
});
passport.deserializeUser(function(user, cb) {
process.nextTick(function() {
return cb(null, user);
});
});
var express = require('express');
var qs = require('querystring');
var router = express.Router();
router.get('/login', passport.authenticate('openidconnect'));
router.get('/oauth2/redirect', passport.authenticate('openidconnect', {
successRedirect: '/',
failureRedirect: '/login'
}));
module.exports = router;
When a user clicks the Sign in button, the system redirects them to your app's sign-in page hosted by Auth0, where the user will log in.
After logging in, Auth0 redirects the user to your app.
Auth Routes
Now, we need to add the auth routes to our app.
Go to the app.js class and modify the code as follows:
var indexRouter = require('./routes/index');
var authRouter = require('./routes/auth');
var logger = require('morgan');
var session = require('express-session');
var passport = require('passport');
var SQLiteStore = require('connect-sqlite3')(session);
///...
app.use('/', indexRouter);
app.use('/', authRouter);
app.use(express.static(path.join(__dirname, 'public')));
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: false,
store: new SQLiteStore({ db: 'sessions.db', dir: './var/db' })
}));
app.use(passport.authenticate('session'));
That should add all the code necessary for authentication. Now we can proceed to enforce it.
In the case of Express.js, the following route exemplifies an unprotected endpoint, because it doesn’t enforce any authentication:
app.post('/admin', function (req, res) {
// ADMIN AREA
});
This endpoint is available to everyone and must be protected by some authentication mechanism.
Thankfully, Express.js allows you to implement an authentication mechanism as middleware. Therefore, a straightforward way to protect this endpoint would be the following:
function authenticate(req, res, next) {
if (!req.session.isLoggedIn) {
res.redirect('/index.html');
} else {
next();
}
}
You can also insert the authentication verification directly in the route, like bellow:
app.post('/admin', authenticate, function (req, res) { /***/ });
That's about it.
Tackling Broken Access Control Vulnerabilities
Given that most access control mitigations are standard on all technology stacks, let’s go over a brief refresher on what we’ve already established for various vulnerabilities in previous articles on the subject.
Insecure IDs: You can easily achieve this solution by implementing global unique identifier numbers (GUIDs) as IDs. You must develop your system with this vulnerability in mind early on. All IDs (or those belonging to sensitive resources) must be obfuscated and unique.
Path Traversal: Path Traversal mitigation requires validating all user inputs and restricting access to critical resources. Luckily, you don't need to do much to implement proper path traversal policies, thanks to the robustness of libraries like Spring Security.
File Permission: Unless you need to tinker with server permissions and add features related to file manipulation, you don't need to do much to keep file permissions secure. Nonetheless, consult with your server manager if you need further instructions.
Client Caching: In this case, the most effective solution is also the most simple. Don't store sensitive user information in the client browser. However, if you must venture into sophisticated features due to requirements, implement proper HTML meta tags and async validations to confirm authority on page load.
Conclusion
As time passes, threats become more abundant and dangerous. To mitigate them and ensure users’ security, software companies constantly incorporate more sophisticated and robust solutions. And as developers, we’re responsible for ensuring that we take advantage of the solutions available to us so our platforms are protected from bad actors on the web.
Furthermore, ensuring the stability of our platforms and the security of our users' information is a critical matter that gets more complex and sensitive over time.
This post was written by Juan Reyes. Juan is an engineer by profession and a dreamer by heart who crossed the seas to reach Japan following the promise of opportunity and challenge. While trying to find himself and build a meaningful life in the east, Juan borrows wisdom from his experiences as an entrepreneur, artist, hustler, father figure, husband, and friend to start writing about passion, meaning, self-development, leadership, relationships, and mental health. His many years of struggle and self-discovery have inspired him and drive to embark on a journey for wisdom.