Access Control in Spring Boot

Access control enforces policy such that users cannot act outside of their intended permissions. Failures typically lead to unauthorized information disclosure, modification or destruction of all data, or performing a business function outside of the limits of the user.

Authentication identifies the user and confirms that he is who he says he is and Authorization checks if the user has access to the resource she requests.

Simply put, applications with access control vulnerability expose information to users who are not really authorized to access them.
This can be

  • resources left wide open to the public
  • users able to access other user’s information
  • end user able to perform operations that only an admin was supposed to perform

Prevention

Access control is only effective if enforced in trusted server-side code or server-less API, where the attacker cannot modify the access control check or metadata.

  • With the exception of public resources, deny by default.
  • Implement access control mechanisms once and re-use them throughout the application, including minimizing CORS usage.
  • Model access controls should enforce record ownership, rather than accepting that the user can create, read, update, or delete any record.
  • Unique application business limit requirements should be enforced by domain models.
  • Disable web server directory listing and ensure file metadata (e.g. .git) and backup files are not present within web roots.
  • Log access control failures, alert admins when appropriate (e.g. repeated failures).
  • Rate limit API and controller access to minimize the harm from automated attack tooling.
  • JWT tokens should be invalidated on the server after logout. Developers and QA staff should include functional access control unit and integration tests.

Vertical access controls

Vertical access controls are mechanisms that restrict access to sensitive functionality that is not available to other types of users. With vertical access controls, different types of users have access to different application functions. For example, an administrator might be able to modify or delete any user’s account, while an ordinary user has no access to these actions.

Horizontal access controls

Horizontal access controls are mechanisms that restrict access to resources to the users who are specifically allowed to access those resources. With horizontal access controls, different users have access to a subset of resources of the same type. For example, a banking application will allow a user to view transactions and make payments from their own accounts, but not the accounts of any other user.

Sample code for this article in github is here.

Vertical privilege escalation

Our sample web application is a simple inventory system for Books. User can create, view and search for Books. The web application is backed by a REST api for CRUD operations. Our application has an admin page, which contains sensitive information. Only Admins should have access to this page. The application also has few public pages under /public URL. REST api is built using Spring Boot

Let’s start with adding below dependency

implementation 'org.springframework.boot:spring-boot-starter-security'

When we add this dependency, authentication gets enabled for the application.

Security can be configured using HttpSecurity. HttpSecurity is global and applies to all requests, it is a great place to set global authentication policies. Having all of our security in one place and defined by web endpoints is neat!

Below is our security configuration

protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable() // NEVER disable CSRF, it is disabled here to simplify the demo. Disabling CSRF protection should never be in a real application.
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic(); //NEVER use Basic Authentication, it is used here to simplify demo
}

As you can see, all requests are protected, which is great, but there are no authorization checks.

Let’s say, we have three users in our application: Amar, Akbar and Anthony. Amar and Akbar are end users. Anthony is our admin. But we do not assign authorities to any of them.

After Anthony logs in, he can access the Admin page. But Amar can access it too, even though he is not supposed to, as he is not really an Admin.

Both the below requests

curl --user amar:password http://localhost:8080/admin
curl --user anthony:password http://localhost:8080/admin

return below response

{
"_links": {
"self": {
"href": "http://localhost:8080/admin",
"templated": false
},
"health": {
"href": "http://localhost:8080/admin/health",
"templated": false
},
"health-path": {
"href": "http://localhost:8080/admin/health/{*path}",
"templated": true
},
"info": {
"href": "http://localhost:8080/admin/info",
"templated": false
}
}
}

This can be a more serious problem once we add ‘delete user’ functionality to the admin page. Amar will be able to delete all other users.

Another issue is that the application has public pages under /public URL that we want anyone to access. But our security configuration above only allows authenticated user to access these pages, which is not good.

Use Authorities

Let’s fix this by assigning specific authorities (privileges) to users

We are giving READ and WRITE privileges to Amar and Akbar. Anthony has additional privileges: READ_ADMIN and WRITE_ADMIN.

Let’s map URLs to privileges

protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()// NEVER disable CSRF, it is disabled here to simplify the demo. Disabling CSRF protection should never be in a real application.
.authorizeRequests()
.antMatchers("/admin/**").hasAnyAuthority("READ_ADMIN", "WRITE_ADMIN")
.antMatchers("/books/**").hasAnyAuthority("READ", "WRITE")
.antMatchers("/public/**").permitAll() //Allow access to everyone for /public folder
.anyRequest().authenticated()
.and()
.httpBasic();//NEVER use Basic Authentication, it is used here to simplify demo
}

After we make the above two changes, Amar’s request now returns HTTP 403 Forbidden error

{
"timestamp": "2020-12-06T03:16:59.584+00:00",
"status": 403,
"error": "Forbidden",
"message": "Forbidden",
"path": "/admin"
}

And for Anthony, below is the response

{
"_links": {
"self": {
"href": "http://localhost:8181/admin",
"templated": false
},
"health": {
"href": "http://localhost:8181/admin/health",
"templated": false
},
"health-path": {
"href": "http://localhost:8181/admin/health/{*path}",
"templated": true
},
"info": {
"href": "http://localhost:8181/admin/info",
"templated": false
}
}
}

OWASP’s recommendation is to deny all requests by default with the exception of public resources.
We have taken care of this by adding below line to our security configuration

anyRequest().authenticated()

We have also allowed unauthenticated access to /public folder by

antMatchers("/public/**").permitAll()

For example, below request does not need authentication

GET /public/index.html

Let’s add a new page to our application: custom.html that can be accessed as

GET /secured/custom.html

As we are protecting all pages, only authenticated can access this page.
But this page was supposed to be accessible only for users with READ_ADMIN authority. We forgot adding the URL pattern to the security config, this is a problem!

Deny unauthorized access

We should deny all requests without authorities by default, we can achieve this by adding

antMatchers("/**").denyAll()

and explicitly allowing requests with hasAuthority().
With this change, any user even authenticated will not be access the page

{
"timestamp": "2020-05-29T16:03:39.519+0000",
"status": 403,
"error": "Forbidden",
"message": "Forbidden",
"path": "/secured/custom.html"
}

And we see this in the logs

2020-05-29 12:03:39.505 ERROR 59498 --- [nio-8181-exec-1] k.cookbook.accesscontrol.SecurityLogger  : Unauthorized access - [username: "anthony", message: "Access is denied", resource: "FilterInvocation: URL: /secured/custom.html"]

until we add this pattern

antMatchers("/secured/**").hasAnyAuthority("READ_ADMIN")

Users with this authority will now be able to access the page.
Final configuration is

protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()
.authorizeRequests()
.antMatchers("/admin/**").hasAnyAuthority("READ_ADMIN", "WRITE_ADMIN")
.antMatchers("/books/**").hasAnyAuthority("READ", "WRITE")
.antMatchers("/public/**").permitAll()
.antMatchers("/secured/**").hasAnyAuthority("READ_ADMIN")
.antMatchers("/**").denyAll()
.and()
.httpBasic();
}

Log access errors

Another OWASP recommendation is to log access control failures and alert admins when appropriate. That means, when Akbar tries to access the admin page, we should log it.
In Spring, we can enable logging using event listeners. Let’s add below class to our application

@Component
@Slf4j
public class SecurityLogger {
@EventListener
public void authenticated(final @NonNull AuthenticationSuccessEvent event) {
final Object principal = event.getAuthentication().getPrincipal();
log.info("Successful login - [username: \"{}\"]", principal);
}
@EventListener
public void authenticationFailure(final @NonNull AbstractAuthenticationFailureEvent event) {
final Object principal = event.getAuthentication().getPrincipal();
log.info("Unsuccessful login - [username: \"{}\"]", principal);
}
@EventListener
public void authorizationFailure(final @NonNull AuthorizationFailureEvent event) {
final Object principal = event.getAuthentication().getPrincipal();
final String message = event.getAccessDeniedException().getMessage();
log.error("Unauthorized access - [username: \"{}\", message: \"{}\"]", principal, Optional.ofNullable(message).map(Function.identity()).orElse("<null>"));
}
@EventListener
public void logoutSuccess(final @NonNull LogoutSuccessEvent event) {
final Object principal = event.getAuthentication().getPrincipal();
log.info("Successful logout - [username: \"{}\"]", principal);
}
}

Now, when Amar tries to access, we will see below error in the log

2020-12-06 22:20:16.289  INFO 54850 --- [nio-8080-exec-5] c.p.spring.accesscontrol.SecurityLogger  : Unauthorized access - [username: "amar", message: "Access is denied", resource: "FilterInvocation: URL: /secured/custom.html"]

Similarly, when an unauthenticated user tries to access a secured URL, we will see below error

2020-12-06 22:18:47.782  INFO 54850 --- [nio-8080-exec-1] c.p.spring.accesscontrol.SecurityLogger  : Unauthorized access - [username: "anonymousUser", message: "Access is denied", resource: "FilterInvocation: URL: /error"]

Unsuccesful login attempt

2020-12-06 22:18:47.759  INFO 54850 --- [nio-8080-exec-1] c.p.spring.accesscontrol.SecurityLogger  : Unsuccessful login - [username: "amar"]

Successful login

2020-12-06 22:20:16.288  INFO 54850 --- [nio-8080-exec-5] c.p.spring.accesscontrol.SecurityLogger  : Successful login - [username: "amar"]

As we can see, these log entries are consistent. Automated alerts can be setup based on these entries.

Horizontal privilege escalation

Our application also has a page to allow user to read and update his/her email. For example Amar can get his profile by making this call:

HTTP GET /users/amar

Response

{
"username": "amar",
"email": "amar@gmail.com"
}

As our application does not have ownership based access control, he can also check (and update) Akbar’s profile information

HTTP GET /users/akbar

Which obviously is not good, let’s fix by adding method-level security.
We can add the @PreAuthorize annotation on controller methods. This annotation contains a Spring Expression Language (SpEL) snippet that is assessed to determine if the request should be authenticated. If access is not granted, the method is not executed and an HTTP Unauthorized error is returned.
Let’s add a check based on username

@GetMapping("/users/{username}")
@PreAuthorize("#username == authentication.principal.username")
public ResponseEntity<User> getUsers(@PathVariable String username) {
User user = userService.findUserByUsername(username);
return ResponseEntity.ok(user);
}

The @EnableGlobalMethodSecurity(prePostEnabled = true) annotation below is what enables the @PreAuthorize annotation. This can be added to any class with the @Configuration annotation.

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
}

Now, as our application has both HttpSecurity and @PreAuthorize annotation, HttpSecurity is applied (in a web request filter) before @PreAuthorize.

Although centralizing access control using HttpSecurity is the preferred way, in many applications it is not feasible and the team resorts do use @PreAuthorize annotations. If that’s the case, make sure to implement deny by default as illustrated in this site: https://www.baeldung.com/spring-deny-access

Summary

Preventing this vulnerability involves

  • Deny by default
  • Control access using Authorities or Roles
  • Implement ownership based access control
  • Log access failures

Sample code for this article in github is here.

References

OWASP
Spring Role Based Access Authorization
Spring Access Denied Handler
@PreAuthorize
Deny by Default

Software professional with a passion for new technologies