Missing Access Controls

Overview

Overview

Access Controls are Authorization. Access Controls exist everywhere and there are many examples in everyday life. Such as when a person enters a debit card into a ATM, the information the ATM user gets access to is their account information only, the ATM user can only read and edit(take out money or input money) the accounts they have access to. Similarly, when you sign into a banking app you can only see your banking info and perform actions with that account. A Missing Access Control (Horizontal) in this app would be the ability to perform actions on other user accounts or retrieve their information. There are other access controls that could be broken depending on the implementation of the app, such as getting access to the Admin panel or Administrative functions as a lower privilege user (Vertical).

Access controls can be thought of Vertical and Horizontal. The different role levels representing the vertical axis and the Horizontal Access represents users of the same role or similar role.

An example of a Vertical Missing Access Control could be the lowest privilege user getting access to the Admin dashboard or some functionality therein by forced browsing. A Horizontal example would be accessing another persons bank account info in the example above.

There are often times many overlapping rights split between roles, as many applications have their own Authorization logic implemented. A Missing Access Controls allows a user to break out of the intended scope of their role/privilege.

I always think of Access Controls at the functional level. Who has access to that function?

Common Access Control Findings

Anywhere where there is user input there is potential to be a missing access control. When testing an application you are looking for any URL, URL Path, URL parameter, or HTTP Body parameters that are being sent that have the possibility of read/write other user's data.

Every single route and parameter that is being passed on all of the application routes must be tested for missing access controls.

URL

Test every route. Pay attention to any path variables such as numeric paths.

GET /accounts/1003

Pay attention to any GUID/UUID in the URL path that can be modified. Always note user GUID's for the User Accounts you have access to.

POST /accounts/18ce53wqoxd/payment_methods/

Basically any non default looking URL pathway that changes based on the user or what they are doing.

URL Parameters

GET /GetAccount.aspx?user_id=1003

Body Parameters

This example HTTP GET request below retrieves the /EditUser page for the user supplied in the body id parameter, if a missing access control existed on this route changing the id parameter would return user info that the authenticated user does not have access to.

GET /EditUser HTTP/1.1
Host: buggyapp.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0
Accept: */*
Accept-Language: nl,en-US;q=0.7,en;q=0.3
Referer: https://buggyapp.com
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-CSRF-TOKEN: 34eb64be-ed52-442a-b945-f94b516da205
X-Requested-With: XMLHttpRequest
Content-Length: 77
Cookie: [cookiedata]
Connection: close

id=1003&name=user

Example

Consider the example from appsecco's DVNA. The application allows user to change passwords on the user edit page, URL http://127.0.0.1:9090/app/useredit.

Issuing the Submit button will send an HTTP POST request with body parameters id, password, and cpassword.

The code that handles this functionality is in core/appHandler.js. The userEditSubmit function retrieves the user to be edited in lines 2-5. Line 4 retrieves the user from the database using the HTTP body parameter id. Once the user has been retrieved from the database using the user input with no validation checks, it checks that the password and cpassword body parameters are the same. Then saves the new password in line 10.

module.exports.userEditSubmit = function (req, res) {
	db.User.find({
		where: {
			'id': req.body.id
		}		
	}).then(user =>{
		if(req.body.password.length>0){
			if(req.body.password.length>0){
				if (req.body.password == req.body.cpassword) {
					user.password = bCrypt.hashSync(req.body.password, bCrypt.genSaltSync(10), null)
				}else{
					req.flash('warning', 'Passwords dont match')
					res.render('app/useredit', {
						userId: req.user.id,
						userEmail: req.user.email,
						userName: req.user.name,
					})
					return		
				}
			}else{
				req.flash('warning', 'Invalid Password')
				res.render('app/useredit', {
					userId: req.user.id,
					userEmail: req.user.email,
					userName: req.user.name,
				})
				return
			}
		}
		user.email = req.body.email
		user.name = req.body.name
		user.save().then(function () {
			req.flash('success',"Updated successfully")
			res.render('app/useredit', {
				userId: req.body.id,
				userEmail: req.body.email,
				userName: req.body.name,
			})
		})
	})
}

The code does not check if the user id in the body parameter is the same as the user id who is sending the request, or if that user has the right privileges to edit other users (such as an admin).

The code could be fixed by checking if the user id in the body parameter is the same as the user who is sending the request, by retrieving the user id from the session.

if (req.user.id == req.body.id)

Or instead of looking up the id from the body parameter, use the id from the session which user input can not change. The code could be restructured to avoid the db.User.Find and use the id from the session:

req.user.id

In NodeJS and Express with Passport.js the requests user object (req.user) can be used to access the current authenticated user's data.

For further reading:

Last updated