Fortifying Front-End Fortresses: A Comprehensive Guide to Security Best Practices in Angular Development: Angular Security 101 - Part 1(Broken Access Control)
Angular app security essentials: Broken Access Control.
This article various coding standards to securely develop Angular Applications. We will be covering Broken access control related vulnerabilities which is an OWASP Top 10 based vulnerability related to Angular and how to securely code it so that the web vulnerability does not occur resulting in major security vulnerabilities.
A01. Broken Access Control
Missing Functional Level Access Control :→
What is it Missing Functional Level Access Control ?
This vulnerability occurs when users can perform functions they have not been authorized for or when resources can be accessed by unauthorized users.
What causes Missing Functional Level Access Control?
When access checks are not implemented or when a protection mechanism exists but is not properly configured. What could happen, if this vulnerability exists in your application?
An attacker could forge requests in order to access functionality without proper authorization. An attacker could gain access to the admin , panel of your application . An Employee from the sales department could view information from the financial department.
How to prevent MFA vulnerability?
Protect all business functions using role based authorization mechanism, server side. Authorization should be implemented using centralized authorization routes. Deny access by default.
Scenario 1:→
Scenario 2:→
Potential Impacts:
Accounts could be taken over , including privileged ones. With a stolen account , an attacker could do anything the victim could do.
Sensitive end-user (customer) data could be stolen leading to reputational damage and revenue loss.
A stolen administrator account could lead to disruption of the website causing loss of customers and revenue.
In the context of Angular createUserComponent is not properly created and protected with authentication token , thus attacker is able to gain access to the component.
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AdminComponent } from './admin/admin.component';
import { UnauthorizedComponent } from './unauthorized/unauthorized.component';
import { RoleGuard } from './role.guard';
const routes: Routes = [
{ path: 'home', component: HomeComponent },
{ path: 'admin', component: AdminComponent, canActivate: [RoleGuard], data: { expectedRole: 'admin' } },
{ path: 'createUserComponent', component: UnauthorizedComponent },
{ path: '', redirectTo: '/home', pathMatch: 'full' }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root'
})
export class RoleGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
const expectedRole = route.data.expectedRole;
if (!this.authService.hasPermission(expectedRole)) {
this.router.navigate(['/unauthorized']); // Redirect to unauthorized page
return false;
}
return true;
}
}
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AuthService {
userRoles: string[] = ['admin', 'user'];
constructor() { }
hasPermission(role: string): boolean {
return this.userRoles.includes(role);
}
}
Mitigation:
Always deny access by default . See the least privilege module for more information. Implementation of Missing Function Level Access Control on the server and never on the client side.
Example of a nodeJS backend code:
import { Router } from 'express';
import { check } from 'express-validator/check';
import { requiresToBeLoggedIn } from '../../../middleware/auth';
import * as questionHandler from './questions.handlers';
export function init(api) {
const router = new Router();
router.get('/', questionHandler.getQuestions);
router.post('/', questionHandler.addQuestion);// a post req is used without login
router.post('/delete/:id', requiresToBeLoggedIn, questionHandler.deleteQuestion);
router.post('/reply/:id', requiresToBeLoggedIn, questionHandler.replyToQuestion);
router.post('/reply/delete/:questionId/:replyId', requiresToBeLoggedIn, questionHandler.deleteReply);
api.use('/questions', router);
}
How to solve this:
router.post('/', requiresToBeLoggedIn, **questionHandler.addQuestion**);
Sensitive Data Storage - Plain Text Storage of Passwords
a sample File Structure is
Storing passwords in plain text is considered a security vulnerability. However, here's an example of how NOT to store passwords in Angular:
In Angular, you might have a user registration form where users enter their desired password. Instead of securely hashing and storing the password, you store it directly in plain text in a variable or database. Here's an example code snippet showcasing this incorrect approach:
import { Component } from '@angular/core';
@Component({
selector: 'app-registration',
templateUrl: './registration.component.html',
styleUrls: ['./registration.component.css']
})
export class RegistrationComponent {
password: string;
registerUser() {
// Store the password directly in plain text
this.storePassword(this.password);
}
storePassword(password: string) {
// This is a vulnerable implementation, storing password in plain text
// In real-world scenarios, you should never store passwords like this
// Instead, use secure hashing algorithms like bcrypt to store passwords securely
console.log('Storing password:', password);
}
}
crypto.js
export const encrypt = (password) => {
return Promise.resolve(password);
};
export const compare = (password, encryptedPassword) => {
return encrypt(password).then((result) => {
return result === encryptedPassword;
});
};
Code snippet where sensitive data was stored unencrypted.
users.handler.js
export function checkEmail(req, res) {
const { email } = req.query;
User.findOne({ email })
.then((user) => {
if (user) {
return res.status(200).json({ msg: USER_DETAIL_NOT_UNIQUE });
}
return res.status(200).json({ msg: USER_DETAIL_UNIQUE });
});
}
questions.handler.js
import mongoose from 'mongoose';
import {
QUESTION_INTERNAL_SERVER_ERROR_MSG,
QUESTION_UNAUTHORIZED_MSG,
QUESTION_DELETE_SUCCESS_MSG,
QUESTION_REPLY_DELETE_SUCCESS_MSG,
QUESTION_NOT_FOUND_MSG,
QUESTION_REPLY_NOT_FOUND,
} from './questions.constants';
const Question = mongoose.model('Question');
const Reply = mongoose.model('Reply');
export function getQuestions(req, res) {
Question
.find()
.populate({
path: 'user',
select: '-password',
})
.populate({
path: 'replies',
populate: {
path: 'user',
select: '-password',
model: 'User',
},
})
.then(questions => res.status(200).json({ questions }))
.catch((err) => {
console.error('MONGOOSE ERROR', err);
return res.status(500).json({ msg: QUESTION_INTERNAL_SERVER_ERROR_MSG });
});
}
export function addQuestion(req, res, next) {
const { userId } = req.session;
const { title, question } = req.body;
const newQuestion = new Question({
user: userId,
title,
question,
});
newQuestion.save()
.then(question => res.status(200).json({ question }))
.catch((err) => {
console.error('MONGOOSE ERROR', err);
return res.status(500).json({ msg: QUESTION_INTERNAL_SERVER_ERROR_MSG });
});
}
export function deleteQuestion(req, res, next) {
const { id } = req.params;
const { userId } = req.session;
Question.findById(id)
.then((question) => {
const questionUserId = question.user;
if (!questionUserId.equals(userId)) {
return res.status(401).json({ msg: QUESTION_UNAUTHORIZED_MSG });
}
Question.find({ _id: id })
.remove()
.then(() => res.status(200).json({ msg: QUESTION_DELETE_SUCCESS_MSG }))
.catch((err) => {
console.error('MONGOOSE ERROR', err);
return res.status(500).json({ msg: QUESTION_INTERNAL_SERVER_ERROR_MSG });
});
})
.catch((err) => {
console.error('MONGOOSE ERROR', err);
return res.status(500).json({ msg: QUESTION_INTERNAL_SERVER_ERROR_MSG });
});
}
export function replyToQuestion(req, res) {
const { id } = req.params;
const { text } = req.body;
const { userId } = req.session;
const reply = new Reply({
user: userId,
text,
});
reply.save()
.then((reply) => {
Question.findById(id)
.then((question) => {
if (!question) {
return res.status(404).json({ msg: QUESTION_NOT_FOUND_MSG });
}
question.replies.push(reply._id);
question.save()
.then(question => res.status(200).json({ reply }))
.catch((err) => {
console.error('MONGOOSE ERROR', err);
return res.status(500).json({ msg: QUESTION_INTERNAL_SERVER_ERROR_MSG });
});
})
.catch((err) => {
console.error('MONGOOSE ERROR', err);
return res.status(500).json({ msg: QUESTION_INTERNAL_SERVER_ERROR_MSG });
});
})
.catch((err) => {
console.error('MONGOOSE ERROR', err);
return res.status(500).json({ msg: QUESTION_INTERNAL_SERVER_ERROR_MSG });
});
}
export function deleteReply(req, res, next) {
const { questionId, replyId } = req.params;
const { userId } = req.session;
Question
.findById(questionId)
.populate({
path: 'replies',
match: { _id: replyId, user: userId },
})
.then((question) => {
if (!question || question.replies.length === 0) {
return res.status(404).json({ msg: QUESTION_NOT_FOUND_MSG });
}
Reply
.findById(replyId)
.remove();
Question
.findByIdAndUpdate(questionId, {
$pull: { replies: { $in: [replyId] } },
})
.then(() => res.status(200).json({ msg: QUESTION_REPLY_DELETE_SUCCESS_MSG }));
})
.catch((err) => {
console.error('MONGOOSE ERROR', err);
return res.status(500).json({ msg: QUESTION_INTERNAL_SERVER_ERROR_MSG });
});
}
IDENTIFY SOLUTION:
Vulnerable code is crypto.js as this code does not demonstrate secure encryption practices . In fact it does not perform any encryption at all.The encrypt
function simply returns the password as is, without any transformation or encryption. The compare
function compares the plain text password with the encrypted password, but since no encryption is applied, it essentially compares the plain text password with itself.
To ensure secure encryption of passwords, it is recommended to use established encryption algorithms and libraries specifically designed for password hashing, such as bcrypt or Argon2. These algorithms provide additional security features like salting and multiple rounds of hashing to protect against brute-force attacks.
import bcrypt from 'bcrypt';
export const encrypt = async (password) => {
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
return hashedPassword;
};
export const compare = async (password, hashedPassword) => {
const match = await bcrypt.compare(password, hashedPassword);
return match;
};
In this updated code , the encrypt function uses bcrypt’s hash function to generate a salted hashed password. The compare function uses bcrypt’s compare function to compare the plain text password with the hashed password and returns a Boolean indicating whether they match.