Broken Object Property Level Authorization

Broken Object Property Level Authorization in Spring Boot [CVE-2026-45091]

[Fixed month year] Updated CVE-2026-45091

Overview

CVE-2026-45091 highlights how a secret-management library can leak sensitive data through token payloads when not designed with encrypted or scoped data in mind. In sealed-env's enterprise mode, versions 0.1.0-alpha.1 through 0.1.0-alpha.3 embedded the operator's literal TOTP secret in the JWS payload of every minted unseal token. Since the JWS payload is base64-encoded JSON and not encrypted, anyone who can observe the token (CI build logs, container env dumps, kubectl describe pod, error traces, logging back-ends) could decode the payload and read the TOTP secret in plaintext. In a Spring Boot application, this kind of leakage couples with the broader class of Broken Object Property Level Authorization (BOPLA) risks: if an API exposes full domain objects with multiple properties, an attacker who gains access to a token that represents broader access could also view or infer sensitive properties that should be restricted. The vulnerability is tracked under CWE-200 (Information Exposure) and CWE-522 (Insufficiently Protected Credentials), and the practical effect is that secret data can be exposed even when the high-level authorization checks are in place. This guide references the CVE-2026-45091 details to illustrate how a similar failure mode could manifest in Spring Boot services relying on token-based secret management and how to fix it in real Java code. The root lesson is that token payloads must not carry sensitive material, and APIs must enforce per-property authorization so that even valid object access does not leak restricted fields.

Affected Versions

0.1.0-alpha.1 through 0.1.0-alpha.3

Code Fix Example

Spring Boot API Security Remediation
/* Vulnerable pattern (no per-property filtering; returns entire entity) and fixed pattern (explicit per-property DTO with access checks) in a single, illustrative Spring Boot setup. This example uses inner static classes for brevity and demonstrates the difference between returning a full entity and returning a restricted view of that entity. */

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.security.core.Authentication;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Component;

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    // Vulnerable pattern: returns the full entity with all properties (no per-property filtering)
    @RestController
    @RequestMapping("/api")
    public static class VulnerableController {
        private final UserService userService;
        public VulnerableController(UserService userService) { this.userService = userService; }

        @GetMapping("/users/{id}")
        public UserEntity getUserVulnerable(@PathVariable Long id) {
            return userService.findById(id);
        }
    }

    // Fixed pattern: returns a restricted DTO and enforces per-property access
    @RestController
    @RequestMapping("/api")
    public static class FixedController {
        private final UserService userService;
        private final PropertySecurityService security;
        public FixedController(UserService userService, PropertySecurityService security) {
            this.userService = userService; this.security = security; }

        @GetMapping("/users/{id}")
        public UserPublicDTO getUserFixed(@PathVariable Long id, Authentication auth) {
            UserEntity user = userService.findById(id);
            if (!security.canViewUser(auth, user)) {
                throw new AccessDeniedException("Not authorized to view this user data");
            }
            return new UserPublicDTO(user);
        }
    }

    public static class UserEntity {
        private Long id;
        private String username;
        private String email;
        private String phone;
        private double salary;
        private String secretNote;
        public UserEntity(Long id, String username, String email, String phone, double salary, String secretNote) {
            this.id = id; this.username = username; this.email = email; this.phone = phone; this.salary = salary; this.secretNote = secretNote; }
        public Long getId() { return id; }
        public String getUsername() { return username; }
        public String getEmail() { return email; }
        public String getPhone() { return phone; }
        public double getSalary() { return salary; }
        public String getSecretNote() { return secretNote; }
    }

    public static class UserPublicDTO {
        public Long id;
        public String username;
        public String email;
        public UserPublicDTO(UserEntity u) { this.id = u.id; this.username = u.username; this.email = u.email; }
    }

    @Service
    public static class UserService {
        public UserEntity findById(Long id) {
            // In a real app, fetch from repository; this is a deterministic example
            return new UserEntity(id, "alice", "[email protected]", "+1-555-0100", 100000.0, "secret");
        }
    }

    @Component
    public static class PropertySecurityService {
        public boolean canViewUser(Authentication auth, UserEntity user) {
            // Example: only admins can view private fields; all authenticated users can view public fields
            return auth != null && auth.getAuthorities().stream()
                    .anyMatch(granted -> granted.getAuthority().equals("ROLE_ADMIN"));
        }
    }
}

CVE References

Choose which optional cookies to allow. You can change this any time.