Introduction

Welcome to the lesson on flawed business logic in snippet quota management. In this lesson, we will explore how business logic flaws can lead to security vulnerabilities in web applications. Business logic is crucial as it dictates how an application behaves and processes data. When flawed, it can open doors to various security issues.

In this lesson, we'll focus on snippet quota management, a common feature in web applications, and learn how to identify and fix vulnerabilities related to it. Let's dive in! 🚀

Understanding Business Logic in Web Applications

Business logic represents the core rules and processes that govern how an application operates. It encompasses all decision - making processes, calculations, and data manipulations that happen behind the scenes. When implementing features like snippet management, business logic determines crucial aspects such as:

  • Who can create snippets.
  • How many snippets can a user create.
  • What are the size limitations for snippets.
  • How storage quota is calculated and enforced.

Oversights in business logic can lead to serious security vulnerabilities. For instance, if we don't validate storage quotas, a malicious user could potentially exhaust the server's storage capacity, causing service disruption for other users. In the following sections, we'll examine a specific example of flawed business logic in snippet management and learn how to properly secure it.

Next, let's look at a simple function that demonstrates one such vulnerability.

The Vulnerable Code

Let's examine code that demonstrates how the absence of size checks and user quota limits can lead to vulnerabilities:

@RestController
@RequestMapping("/api/snippets")
public class SnippetController {
    
    private final SnippetRepository snippets;
    private final UserRepository users;
    private final JwtUtil jwt;
    
    public SnippetController(SnippetRepository snippets, UserRepository users, JwtUtil jwt) {
        this.snippets = snippets;
        this.users = users;
        this.jwt = jwt;
    }
    
    @PostMapping("")
    public ResponseEntity<?> create(@RequestHeader(value = "authorization", required = false) String authorization,
                                    @RequestBody CreateSnippetRequest request) {
        var outcome = getCurrentUser(authorization);
        if (outcome.error() != null) return outcome.error();
        User user = outcome.user();
        
        Snippet s = new Snippet();
        s.setId(UUID.randomUUID().toString());
        s.setTitle(request.title());
        s.setContent(request.content());
        s.setLanguage(request.language());
        s.setUserId(user.getId());
        s = snippets.save(s);
        
        return ResponseEntity.ok(new SnippetResponse(
            s.getId(),
            s.getTitle(),
            s.getContent(),
            s.getLanguage(),
            String.valueOf(s.getUserId())
        ));
    }
    
    private AuthOutcome getCurrentUser(String authorizationHeader) {
        if (authorizationHeader == null || authorizationHeader.isBlank()) {
            return new AuthOutcome(null, error(HttpStatus.UNAUTHORIZED, "Missing authorization header"));
        }
        String token = authorizationHeader.startsWith("Bearer ") ? authorizationHeader.substring(7) : authorizationHeader;
        try {
            DecodedJWT decoded = jwt.verify(token);
            Integer userId = decoded.getClaim("userId").asInt();
            return users.findById(userId)
                    .map(user -> new AuthOutcome(user, null))
                    .orElseGet(() -> new AuthOutcome(null, error(HttpStatus.UNAUTHORIZED, "User not found")));
        } catch (Exception e) {
            return new AuthOutcome(null, error(HttpStatus.UNAUTHORIZED, "Invalid token"));
        }
    }
    
    private ResponseEntity<ErrorResponse> error(HttpStatus status, String detail) {
        return ResponseEntity.status(status).body(new ErrorResponse(detail));
    }
    
    private record AuthOutcome(User user, ResponseEntity<ErrorResponse> error) {}
    
    public record CreateSnippetRequest(String title, String content, String language) {}
    
    public record SnippetResponse(String id, String title, String content, String language, String userId) {}
}

In this code, the application allows users to create snippets without checking the size of each snippet or the total storage used by the user. This oversight can be exploited to overload the system.

Exploiting the Vulnerability
Implementing Size Checks

To prevent such attacks, we might start by implementing size checks to ensure that each snippet does not exceed a certain size:

@RestController
@RequestMapping("/api/snippets")
public class SnippetController {
    
    private static final long MAX_SNIPPET_SIZE = 1024 * 1024; // 1MB
    
    private final SnippetRepository snippets;
    private final UserRepository users;
    private final JwtUtil jwt;
    
    public SnippetController(SnippetRepository snippets, UserRepository users, JwtUtil jwt) {
        this.snippets = snippets;
        this.users = users;
        this.jwt = jwt;
    }
    
    @PostMapping("")
    public ResponseEntity<?> create(@RequestHeader(value = "authorization", required = false) String authorization,
                                    @RequestBody CreateSnippetRequest request) {
        var outcome = getCurrentUser(authorization);
        if (outcome.error() != null) return outcome.error();
        User user = outcome.user();
        
        String content = request.content();
        
        // Check snippet size
        long contentSize = content.getBytes(StandardCharsets.UTF_8).length;
        if (contentSize > MAX_SNIPPET_SIZE) {
            return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
                    .body(new ErrorResponse("Snippet too large"));
        }
        
        Snippet s = new Snippet();
        s.setId(UUID.randomUUID().toString());
        s.setTitle(request.title());
        s.setContent(content);
        s.setLanguage(request.language());
        s.setUserId(user.getId());
        s = snippets.save(s);
        
        return ResponseEntity.ok(new SnippetResponse(
            s.getId(),
            s.getTitle(),
            s.getContent(),
            s.getLanguage(),
            String.valueOf(s.getUserId())
        ));
    }
    
    private AuthOutcome getCurrentUser(String authorizationHeader) {
        if (authorizationHeader == null || authorizationHeader.isBlank()) {
            return new AuthOutcome(null, error(HttpStatus.UNAUTHORIZED, "Missing authorization header"));
        }
        String token = authorizationHeader.startsWith("Bearer ") ? authorizationHeader.substring(7) : authorizationHeader;
        try {
            DecodedJWT decoded = jwt.verify(token);
            Integer userId = decoded.getClaim("userId").asInt();
            return users.findById(userId)
                    .map(user -> new AuthOutcome(user, null))
                    .orElseGet(() -> new AuthOutcome(null, error(HttpStatus.UNAUTHORIZED, "User not found")));
        } catch (Exception e) {
            return new AuthOutcome(null, error(HttpStatus.UNAUTHORIZED, "Invalid token"));
        }
    }
    
    private ResponseEntity<ErrorResponse> error(HttpStatus status, String detail) {
        return ResponseEntity.status(status).body(new ErrorResponse(detail));
    }
    
    private record AuthOutcome(User user, ResponseEntity<ErrorResponse> error) {}
    
    public record CreateSnippetRequest(String title, String content, String language) {}
    
    public record SnippetResponse(String id, String title, String content, String language, String userId) {}
}

Here, we define a MAX_SNIPPET_SIZE of 1MB. Before creating a new snippet, we check if the content exceeds this limit by converting the string to bytes using UTF-8 encoding via content.getBytes(StandardCharsets.UTF_8).length. If it does, we return an error response with HTTP status 413 (Payload Too Large), preventing the creation of oversized snippets.

Request Size Limits

Another crucial layer of protection is implementing request size limits at the Spring Boot application level.

By default, Spring Boot has reasonable limits for request sizes, but these can be configured based on your application's needs. You can set these limits in your application.properties file:

# Maximum size for a single file upload
spring.servlet.multipart.max-file-size=1MB

# Maximum size for the entire request
spring.servlet.multipart.max-request-size=1MB

# Maximum size of HTTP post content (Tomcat - specific)
server.tomcat.max-http-form-post-size=1MB

Alternatively, if you're using application.yml:

spring:
  servlet:
    multipart:
      max-file-size: 1MB
      max-request-size: 1MB

server:
  tomcat:
    max-http-form-post-size: 1MB

These configuration properties set limits on the size of requests that your application will accept. When a request exceeds these limits, Spring Boot will automatically reject it with a 413 (Payload Too Large) status code before it reaches your controller methods.

While these properties are primarily designed for multipart file uploads, the server.tomcat.max-http-form-post-size setting applies to regular post requests as well. For JSON payloads specifically, you should adjust this value based on your application's requirements.

It is important to set these limits appropriately for your use case. If your application only handles small text snippets, keeping the limit at 1MB provides a good balance between functionality and security. Setting limits too high might still allow attackers to consume excessive resources, while setting them too low might prevent legitimate use.

Remember that these global limits work in conjunction with your per - snippet size checks. The application - level configuration provides the first line of defense against large payloads, while your controller - level checks implement your specific business rules for snippet sizes.

Implementing User Quota Limits

Next, let's implement user quota limits to ensure that users do not exceed their allocated storage:

@RestController
@RequestMapping("/api/snippets")
public class SnippetController {
    
    private static final long MAX_SNIPPET_SIZE = 1024 * 1024; // 1MB
    private static final long USER_QUOTA = 10 * 1024 * 1024; // 10MB
    
    private final SnippetRepository snippets;
    private final UserRepository users;
    private final JwtUtil jwt;
    
    public SnippetController(SnippetRepository snippets, UserRepository users, JwtUtil jwt) {
        this.snippets = snippets;
        this.users = users;
        this.jwt = jwt;
    }
    
    @PostMapping("")
    public ResponseEntity<?> create(@RequestHeader(value = "authorization", required = false) String authorization,
                                    @RequestBody CreateSnippetRequest request) {
        var outcome = getCurrentUser(authorization);
        if (outcome.error() != null) return outcome.error();
        User user = outcome.user();
        
        String content = request.content();
        
        // Check snippet size
        long contentSize = content.getBytes(StandardCharsets.UTF_8).length;
        if (contentSize > MAX_SNIPPET_SIZE) {
            return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
                    .body(new ErrorResponse("Snippet too large"));
        }
        
        // Check user quota
        List<Snippet> userSnippets = snippets.findByUserId(user.getId());
        long currentUsage = userSnippets.stream()
                .mapToLong(snippet -> snippet.getContent()
                        .getBytes(StandardCharsets.UTF_8).length)
                .sum();
        
        if (currentUsage + contentSize > USER_QUOTA) {
            return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
                    .body(new ErrorResponse("Storage quota exceeded"));
        }
        
        Snippet s = new Snippet();
        s.setId(UUID.randomUUID().toString());
        s.setTitle(request.title());
        s.setContent(content);
        s.setLanguage(request.language());
        s.setUserId(user.getId());
        s = snippets.save(s);
        
        return ResponseEntity.ok(new SnippetResponse(
            s.getId(),
            s.getTitle(),
            s.getContent(),
            s.getLanguage(),
            String.valueOf(s.getUserId())
        ));
    }
    
    private AuthOutcome getCurrentUser(String authorizationHeader) {
        if (authorizationHeader == null || authorizationHeader.isBlank()) {
            return new AuthOutcome(null, error(HttpStatus.UNAUTHORIZED, "Missing authorization header"));
        }
        String token = authorizationHeader.startsWith("Bearer ") ? authorizationHeader.substring(7) : authorizationHeader;
        try {
            DecodedJWT decoded = jwt.verify(token);
            Integer userId = decoded.getClaim("userId").asInt();
            return users.findById(userId)
                    .map(user -> new AuthOutcome(user, null))
                    .orElseGet(() -> new AuthOutcome(null, error(HttpStatus.UNAUTHORIZED, "User not found")));
        } catch (Exception e) {
            return new AuthOutcome(null, error(HttpStatus.UNAUTHORIZED, "Invalid token"));
        }
    }
    
    private ResponseEntity<ErrorResponse> error(HttpStatus status, String detail) {
        return ResponseEntity.status(status).body(new ErrorResponse(detail));
    }
    
    private record AuthOutcome(User user, ResponseEntity<ErrorResponse> error) {}
    
    public record CreateSnippetRequest(String title, String content, String language) {}
    
    public record SnippetResponse(String id, String title, String content, String language, String userId) {}
}

In this code, we calculate the user's current storage usage by retrieving all of their snippets from the repository and summing their sizes using java streams. The mapToLong() method converts each snippet to its size in bytes, and sum() calculates the total. If adding the new snippet would exceed the USER_QUOTA of 10MB, we return an error response, preventing the creation of the snippet.

The repository method findByUserId() would be defined in your Spring Data JPA repository interface:

public interface SnippetRepository extends JpaRepository<Snippet, String> {
    List<Snippet> findByUserId(Integer userId);
}
Conclusion and Next Steps

In this lesson, we explored the importance of business logic in web applications and how flaws in snippet quota management can lead to vulnerabilities. We learned how to identify these flaws, exploit them, and implement effective solutions to mitigate them.

As you move on to the practice exercises, remember the key points from this lesson and apply them to enhance the security of your applications. In the next lesson, we'll continue to build on these concepts and explore additional security measures. Keep up the great work! 🎉

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal