Java Security Code Review Guide
1. Introduction
I put this guide together as a structured approach to security-focused code review for Java applications. Whether you’re just starting to identify security vulnerabilities in Java code or you’re an experienced developer looking for a language-specific checklist, I’ve tried to make it useful at both levels.
Java’s strong type system, managed memory, and mature ecosystem offer many safety guarantees, but the more I researched Java security, the more I realised the language and its frameworks still expose developers to a wide range of pitfalls including injection, deserialisation, reflection abuse, JNDI injection, and misconfigured XML parsers. What follows covers manual review strategies, common anti-patterns, recommended tooling, and vulnerability patterns organised by class, with cross-references to the intentionally vulnerable examples in this project.
Audience: Security trainees, application developers, code reviewers, and anyone evaluating Java codebases for security weaknesses.
2. Manual Review Best Practices
2.1 Trace Data from Entry to Sink
Spring Boot applications accept user input through @RequestParam, @PathVariable, @RequestBody, @RequestHeader, @CookieValue, and HttpServletRequest methods. The approach I’ve found most effective is to trace every external input to where it’s consumed, JDBC queries, Runtime.exec() calls, file operations, HTTP clients, or rendered HTML. Any path from source to sink without validation or sanitisation is a potential vulnerability.
2.2 Inspect String Concatenation in Sensitive Contexts
Java offers string concatenation (+), String.format(), StringBuilder, and template expressions. When any of these are used to build SQL queries, shell commands, HTML output, LDAP queries, or URLs with user-controlled data, treat it as a high-priority finding.
2.3 Review Import Statements
Scan imports for classes known to introduce risk:
java.io.ObjectInputStream, deserialisation of untrusted datajava.lang.Runtime,java.lang.ProcessBuilder, command injectionjavax.script.ScriptEngine, arbitrary code executionjavax.naming.InitialContext,javax.naming.Context, JNDI injectionjava.lang.reflect.*, reflection abuse, access control bypassjavax.xml.parsers.*,org.xml.sax.*without secure configuration, XXE attacksjava.sql.Statement(vs.PreparedStatement), SQL injectionjavax.crypto.Cipherwith"DES"or"/ECB/", weak cryptography
2.4 Check for Hardcoded Secrets
Search for string literals that look like passwords, API keys, tokens, or connection strings. Common patterns include fields named SECRET_KEY, API_KEY, PASSWORD, TOKEN, MASTER_KEY, or JDBC URLs containing credentials like jdbc:mysql://user:pass@host.
2.5 Examine Error Handling
Look for catch blocks that expose stack traces, internal paths, or database details to the caller. In Spring Boot, check for @ExceptionHandler methods that return e.getMessage(), e.getStackTrace(), or raw exception objects. Watch for @ResponseStatus annotations that may leak internal error details.
2.6 Evaluate Cryptographic Choices
Flag uses of MessageDigest.getInstance("MD5") or "SHA-1" for password hashing or integrity verification. Check for Cipher.getInstance("DES/ECB/PKCS5Padding") or any ECB mode usage. Verify that java.util.Random (not java.security.SecureRandom) is not used for security-sensitive token generation. Look for hardcoded encryption keys stored as byte[] or String literals.
2.7 Assess Access Control Logic
For every endpoint, verify:
- Authentication is checked before any business logic executes (Spring Security filters,
@PreAuthorize, or manual checks) - Authorisation checks use server-side session data, not client-supplied headers (e.g.,
X-User-Role) - Object-level access control prevents users from accessing resources belonging to others (IDOR)
- Method-level security annotations (
@Secured,@RolesAllowed) are applied consistently
2.8 Review Configuration Defaults
Check application.properties / application.yml for verbose error exposure (server.error.include-stacktrace=always), permissive CORS (@CrossOrigin(origins = "*")), disabled CSRF protection, exposed actuator endpoints, and debug-level logging in production.
3. Common Security Pitfalls
3.1 Injection
| Anti-Pattern | Risk |
|---|---|
"SELECT * FROM t WHERE x = '" + userInput + "'" with Statement.executeQuery() |
SQL injection via string concatenation |
String.format("SELECT ... WHERE id = %s", id) passed to Statement |
SQL injection via format string |
Runtime.getRuntime().exec("ping " + host) |
Command injection via Runtime.exec() |
new ProcessBuilder("sh", "-c", "nslookup " + domain).start() |
Command injection via shell invocation |
| User input rendered directly into HTML string without escaping | Stored and reflected XSS |
| JSONP callback parameter used without sanitisation | XSS via callback injection |
3.2 Broken Access Control
| Anti-Pattern | Risk |
|---|---|
| Endpoint returns full user object (including SSN, salary) without field filtering | Excessive data exposure |
Authorisation check reads role from X-User-Role header instead of session/token |
Client-side role spoofing |
| No ownership check on resource access/update, any authenticated user can read/modify any record | IDOR vulnerability |
| Actuator or admin endpoints exposed without authentication | Information disclosure of secrets and configuration |
3.3 Cryptographic Failures
| Anti-Pattern | Risk |
|---|---|
MessageDigest.getInstance("MD5") for password storage |
Weak, unsalted password hashing |
Cipher.getInstance("DES/ECB/PKCS5Padding") |
Deprecated cipher in insecure mode |
new Random() or new Random(System.currentTimeMillis()) for token generation |
Predictable PRNG seeding |
private static final String MASTER_KEY = "s3cr3tK3y!!" |
Key exposure in source code |
3.4 Insecure Design
| Anti-Pattern | Risk |
|---|---|
Plaintext passwords stored in HashMap or configuration |
No hashing at all |
| Error responses revealing usernames, email existence, or attempt counts | User enumeration |
| Password reset tokens returned in API response body | Token leakage |
e.getStackTrace() or e.getMessage() returned to client in JSON |
Stack trace information disclosure |
3.5 Security Misconfiguration
| Anti-Pattern | Risk |
|---|---|
server.error.include-stacktrace=always in application.properties |
Stack traces exposed to clients |
@CrossOrigin(origins = "*") or reflecting any Origin header |
Overly permissive CORS |
DocumentBuilderFactory without disabling external entities |
XXE via XML entity resolution |
SAXParserFactory without FEATURE_SECURE_PROCESSING |
XXE and billion-laughs attacks |
Server header exposing framework and Java version |
Technology fingerprinting |
3.6 Vulnerable Components
| Anti-Pattern | Risk |
|---|---|
Outdated Spring Boot version with known CVEs in pom.xml |
Known vulnerabilities in framework |
Using SnakeYAML Yaml.load() without SafeConstructor |
YAML deserialisation to arbitrary objects |
| Outdated Jackson or Gson with known deserialisation CVEs | Remote code execution via polymorphic deserialisation |
| Log4j versions vulnerable to Log4Shell (CVE-2021-44228) | Remote code execution via JNDI lookup |
3.7 Integrity Failures (Deserialisation)
| Anti-Pattern | Risk |
|---|---|
new ObjectInputStream(untrustedStream).readObject() |
Arbitrary code execution via Java deserialisation |
Yaml.load(untrustedInput) with default constructor |
Arbitrary object instantiation via YAML |
XMLDecoder on untrusted XML input |
Remote code execution |
| Accepting serialized objects from HTTP request bodies without validation | Deserialisation gadget chain attacks |
3.8 Authentication Failures
| Anti-Pattern | Risk |
|---|---|
Hardcoded credentials: if (password.equals("admin123")) |
Credential exposure in source code |
| No rate limiting on login endpoint | Brute-force attacks |
| JWT secret stored as string literal | Token forgery if secret is leaked |
password.equals(storedPassword) with plaintext comparison |
Timing attack and no hashing |
3.9 Logging and Monitoring Failures
| Anti-Pattern | Risk |
|---|---|
Login response includes password and apiKey fields |
Credential leakage in responses |
| No logging of failed authentication attempts | Brute-force attacks go undetected |
| No logging of privilege changes (role updates, deactivations) | Unauthorized changes are invisible |
| Bulk operations with no audit trail | Mass data exfiltration undetected |
| Data export endpoint returns raw passwords | Sensitive data in export payloads |
3.10 SSRF
| Anti-Pattern | Risk |
|---|---|
new URL(userUrl).openStream() without allowlist |
Unrestricted SSRF |
Blocklist checking only localhost and 127.0.0.1 (missing 0.0.0.0, [::1], decimal IPs) |
SSRF blocklist bypass |
Proxy endpoint accepting arbitrary baseUrl parameter |
Open proxy to internal services |
| Webhook callback URLs not validated against internal ranges | SSRF via webhook registration |
3.11 Race Conditions
| Anti-Pattern | Risk |
|---|---|
Check-then-act on this.balance without synchronized |
Double-spend / negative balance |
checkAvailability() then reserve() without atomicity |
Overselling tickets |
Read-modify-write on shared HashMap without synchronization |
Lost updates, corrupted state |
Singleton with Thread.sleep in constructor and no synchronized |
Multiple singleton instances |
4. Recommended SAST Tools & Linters
4.1 SpotBugs
SpotBugs is a static analysis tool for Java that finds bug patterns in bytecode. With the Find Security Bugs plugin, it detects a wide range of security vulnerabilities.
Installation (standalone):
# Download SpotBugs
wget https://github.com/spotbugs/spotbugs/releases/download/4.8.3/spotbugs-4.8.3.tgz
tar xzf spotbugs-4.8.3.tgz
export PATH=$PATH:$(pwd)/spotbugs-4.8.3/bin
Installation (Maven plugin, recommended):
Add to your pom.xml:
<build>
<plugins>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.8.3.1</version>
<configuration>
<plugins>
<plugin>
<groupId>com.h3xstream.findsecbugs</groupId>
<artifactId>findsecbugs-plugin</artifactId>
<version>1.13.0</version>
</plugin>
</plugins>
</configuration>
</plugin>
</plugins>
</build>
Basic usage, run via Maven:
mvn spotbugs:check
Generate an HTML report:
mvn spotbugs:spotbugs
# Report at target/spotbugsXml.xml; use spotbugs:gui to view interactively
Scan a single compiled class directory:
spotbugs -textui -effort:max -low target/classes/
What SpotBugs + Find Security Bugs catches: SQL injection, command injection, XSS, path traversal, XXE, LDAP injection, insecure deserialisation (ObjectInputStream), weak cryptography (MD5, SHA-1, DES, ECB), hardcoded passwords, predictable Random usage, SSRF patterns, unvalidated redirects, and Spring-specific security issues.
4.2 PMD
PMD is a source code analyser that finds common programming flaws including security issues. It works on source code (no compilation required) and supports custom rule sets.
Installation:
# Download PMD
wget https://github.com/pmd/pmd/releases/download/pmd_releases%2F7.0.0/pmd-dist-7.0.0-bin.zip
unzip pmd-dist-7.0.0-bin.zip
export PATH=$PATH:$(pwd)/pmd-bin-7.0.0/bin
Installation (Maven plugin):
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-pmd-plugin</artifactId>
<version>3.21.2</version>
<configuration>
<rulesets>
<ruleset>category/java/security.xml</ruleset>
<ruleset>category/java/errorprone.xml</ruleset>
</rulesets>
</configuration>
</plugin>
</plugins>
</build>
Basic usage, scan a single file:
pmd check -d injection/sql-injection/java/App.java -R category/java/security.xml -f text
Scan a directory with multiple rule sets:
pmd check -d security-bug-examples/ -R category/java/security.xml,category/java/errorprone.xml -f text
Run via Maven:
mvn pmd:check
What PMD catches: Hardcoded cryptographic keys, insecure Random usage, empty catch blocks that swallow security exceptions, overly broad exception handling, unused security-related imports, and code style issues that may mask vulnerabilities. PMD’s security ruleset is smaller than SpotBugs but complements it well for source-level analysis.
4.3 Semgrep
Semgrep is a multi-language static analysis tool with pattern-based rules. It supports custom rules and has a large community rule registry with extensive Java coverage.
Installation:
pip install semgrep
# or
brew install semgrep
Scan with the default Java security ruleset:
semgrep --config "p/java" security-bug-examples/
Scan with OWASP Top 10 rules:
semgrep --config "p/owasp-top-ten" security-bug-examples/
Scan a single file:
semgrep --config "p/java" injection/sql-injection/java/App.java
Run with auto configuration (recommended for first-time scans):
semgrep --config auto security-bug-examples/
What Semgrep catches: SQL injection via Statement (vs. PreparedStatement), command injection via Runtime.exec() and ProcessBuilder, XSS in Spring MVC responses, insecure deserialisation (ObjectInputStream), XXE via unconfigured XML parsers, SSRF patterns, hardcoded secrets, weak cryptography, JNDI injection, and many Spring Boot-specific issues. Semgrep’s pattern matching is particularly effective at detecting framework-specific anti-patterns that bytecode-based tools may miss.
5. Language-Specific Vulnerability Patterns
5.1 SQL Injection (CWE-89)
Pattern: String concatenation with Statement
// Vulnerable, user input concatenated directly into SQL
String query = "SELECT * FROM products WHERE name LIKE '%" + keyword + "%'";
ResultSet rs = connection.createStatement().executeQuery(query);
Pattern: String.format() in query construction
// Vulnerable, format string builds WHERE clause from user input
String query = String.format("SELECT * FROM users WHERE id = %s", userId);
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(query);
Pattern: Unvalidated ORDER BY injection
// Vulnerable, sortColumn and sortOrder not validated against allowlist
String query = "SELECT * FROM products ORDER BY " + sortColumn + " " + sortOrder;
Safe alternative:
PreparedStatement ps = connection.prepareStatement(
"SELECT * FROM products WHERE name LIKE ?");
ps.setString(1, "%" + keyword + "%");
ResultSet rs = ps.executeQuery();
5.2 Command Injection (CWE-78)
Pattern: Runtime.exec() with string concatenation
// Vulnerable, user input passed directly to shell command
String cmd = "ping -c 3 " + host;
Process proc = Runtime.getRuntime().exec(new String[]{"sh", "-c", cmd});
Pattern: ProcessBuilder with shell invocation
// Vulnerable, domain from user input injected into shell command
String cmd = "nslookup " + domain;
ProcessBuilder pb = new ProcessBuilder("sh", "-c", cmd);
Process proc = pb.start();
Safe alternative:
// Use argument array without shell interpretation
ProcessBuilder pb = new ProcessBuilder("ping", "-c", "3", host);
Process proc = pb.start();
5.3 Cross-Site Scripting, XSS (CWE-79)
Pattern: Stored XSS via string concatenation in HTML
// Vulnerable, post content rendered without escaping
String html = "<h2>" + post.getTitle() + "</h2>";
html += "<div>" + post.getContent() + "</div>";
Pattern: Reflected XSS in search results
// Vulnerable, query parameter reflected back into page
String html = "<p>Results for: <em>" + query + "</em></p>";
Pattern: XSS via href attribute (javascript: protocol)
String html = "<a href=\"" + userWebsite + "\">" + userWebsite + "</a>";
Safe alternative:
import org.apache.commons.text.StringEscapeUtils;
String safeTitle = StringEscapeUtils.escapeHtml4(post.getTitle());
String html = "<h2>" + safeTitle + "</h2>";
5.4 Broken Access Control (CWE-200, CWE-284, CWE-639)
Pattern: No authentication on sensitive endpoint
@GetMapping("/api/users/{userId}")
public ResponseEntity<?> getUserProfile(@PathVariable int userId) {
Map<String, Object> user = users.get(userId);
return ResponseEntity.ok(user); // Returns SSN, salary, no auth check
}
Pattern: Client-controlled authorisation header
String role = request.getHeader("X-User-Role");
if (role == null) role = "employee";
if (!"admin".equals(role)) {
return ResponseEntity.status(403).body("Admin access required");
}
Pattern: Missing object-level authorisation
// Any authenticated user can access any document, no ownership check
Map<String, Object> doc = documents.get(docId);
return ResponseEntity.ok(doc);
5.5 Cryptographic Failures (CWE-327, CWE-328, CWE-330)
Pattern: MD5 for password hashing
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] hash = md.digest("admin123".getBytes());
String hexHash = DatatypeConverter.printHexBinary(hash).toLowerCase();
Pattern: DES in ECB mode with hardcoded key
private static final String MASTER_KEY = "s3cr3t!!";
Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(MASTER_KEY.getBytes(), "DES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
Pattern: Predictable PRNG for session tokens
Random rng = new Random(System.currentTimeMillis());
StringBuilder token = new StringBuilder();
for (int i = 0; i < 32; i++) {
token.append(CHARS.charAt(rng.nextInt(CHARS.length())));
}
Safe alternative:
// Use BCrypt for password hashing
import org.mindrot.jbcrypt.BCrypt;
String hashed = BCrypt.hashpw(password, BCrypt.gensalt());
// Use SecureRandom for tokens
SecureRandom sr = new SecureRandom();
byte[] tokenBytes = new byte[32];
sr.nextBytes(tokenBytes);
5.6 Insecure Design (CWE-209, CWE-522)
Pattern: Plaintext password storage
Map<String, Object> admin = new HashMap<>();
admin.put("username", "admin");
admin.put("password", "Adm1n_Pr0d!");
Pattern: Verbose error responses
Map<String, Object> error = new HashMap<>();
error.put("error", "Report generation failed");
error.put("details", e.getMessage());
error.put("trace", Arrays.toString(e.getStackTrace()));
error.put("database", DATABASE_PATH);
return ResponseEntity.status(500).body(error);
Pattern: User enumeration via distinct error messages
// Different messages for "user not found" vs. "wrong password"
return ResponseEntity.status(404).body("No account found for username '" + username + "'");
// vs.
return ResponseEntity.status(401).body("Incorrect password. Attempts: " + failedAttempts);
5.7 Security Misconfiguration (CWE-16, CWE-611)
Pattern: XXE via DocumentBuilderFactory without secure configuration
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// Missing: dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
// Missing: dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(new InputSource(new StringReader(rawXml)));
Pattern: XXE via SAXParserFactory
SAXParserFactory spf = SAXParserFactory.newInstance();
// No secure processing features set
SAXParser parser = spf.newSAXParser();
parser.parse(inputSource, handler);
Pattern: Overly permissive CORS
String origin = request.getHeader("Origin");
response.setHeader("Access-Control-Allow-Origin", origin != null ? origin : "*");
response.setHeader("Access-Control-Allow-Credentials", "true");
Pattern: Verbose error handler exposing internals
@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleException(Exception e) {
Map<String, Object> body = new HashMap<>();
body.put("trace", Arrays.toString(e.getStackTrace()));
body.put("message", e.getMessage());
return ResponseEntity.status(500).body(body);
}
5.8 Vulnerable and Outdated Components (CWE-1104)
Pattern: Unsafe YAML loading with SnakeYAML
Yaml yaml = new Yaml(); // Default constructor allows arbitrary object instantiation
Object data = yaml.load(untrustedInput);
Pattern: Outdated dependencies in pom.xml
<!-- Vulnerable Spring Boot version -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version> <!-- Known CVEs -->
</parent>
5.9 Integrity Failures, Deserialisation (CWE-502, CWE-829)
Pattern: Java native deserialisation of untrusted data
byte[] rawBytes = Base64.getDecoder().decode(payload);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(rawBytes));
Object obj = ois.readObject(); // Arbitrary code execution via gadget chains
Pattern: Unsafe YAML deserialisation
Yaml yaml = new Yaml();
Object data = yaml.load(untrustedYamlString); // Arbitrary object instantiation
Pattern: XMLDecoder on untrusted input
XMLDecoder decoder = new XMLDecoder(new ByteArrayInputStream(xmlBytes));
Object obj = decoder.readObject(); // Remote code execution
Safe alternative:
// Use JSON (Jackson) instead of Java serialization
ObjectMapper mapper = new ObjectMapper();
MyData data = mapper.readValue(jsonString, MyData.class);
// Use SafeConstructor for YAML
Yaml yaml = new Yaml(new SafeConstructor());
5.10 Logging and Monitoring Failures (CWE-778)
Pattern: Credentials in API responses
Map<String, Object> response = new HashMap<>();
response.put("token", token);
response.put("password", user.get("password"));
response.put("apiKey", user.get("apiKey"));
return ResponseEntity.ok(response);
Pattern: No audit logging for privilege changes
targetUser.put("role", newRole); // Role changed with no log entry
5.11 SSRF (CWE-918)
Pattern: Unrestricted URL fetch
URL url = new URL(userProvidedUrl);
InputStream is = url.openStream(); // No validation of target host
Pattern: Incomplete blocklist
List<String> blocked = Arrays.asList("localhost", "127.0.0.1");
// Missing: 0.0.0.0, [::1], 169.254.x.x, decimal IP representations
Pattern: Open proxy via user-supplied base URL
String baseUrl = request.getParameter("base_url");
String fullUrl = baseUrl + path;
HttpURLConnection conn = (HttpURLConnection) new URL(fullUrl).openConnection();
5.12 Race Conditions (CWE-362)
Pattern: Check-then-act without synchronization
public boolean withdraw(double amount) {
if (this.balance >= amount) {
Thread.sleep(1); // Simulates processing delay
this.balance -= amount; // Another thread may have changed balance
return true;
}
return false;
}
Pattern: Read-modify-write on shared HashMap
int count = counters.getOrDefault(key, 0);
count++;
counters.put(key, count); // Lost update if concurrent
Safe alternative:
public synchronized boolean withdraw(double amount) {
if (this.balance >= amount) {
this.balance -= amount;
return true;
}
return false;
}
// Or use ConcurrentHashMap with atomic operations
counters.merge(key, 1, Integer::sum);
5.13 NULL Pointer Dereference (CWE-476)
Pattern: Unchecked return value from Map.get()
Map<String, Object> user = users.get(userId);
String name = (String) user.get("name"); // NPE if userId not found
Pattern: Missing null check after external lookup
Connection conn = dataSource.getConnection();
ResultSet rs = stmt.executeQuery(query);
rs.next();
String value = rs.getString("column"); // NPE if no rows returned
Safe alternative:
Map<String, Object> user = users.get(userId);
if (user == null) {
throw new NotFoundException("User not found: " + userId);
}
5.14 Integer Overflow (CWE-190)
Pattern: Unchecked arithmetic on int or long
int total = quantity * priceInCents; // Overflow if both are large
Pattern: Casting long to int without range check
long bigValue = computeLargeValue();
int result = (int) bigValue; // Silent truncation
Safe alternative:
int total = Math.multiplyExact(quantity, priceInCents); // Throws ArithmeticException on overflow
// Or use Math.toIntExact for safe narrowing
int result = Math.toIntExact(bigValue);
6. Cross-References to Examples
The table below maps each vulnerability class to the Java source file and companion documentation in this project. All paths are relative to the docs/ directory.
7. Quick-Reference Checklist
Use this checklist during Java code reviews. Each item maps to a vulnerability class covered in this guide.
- Input validation, All user inputs (
@RequestParam,@PathVariable,@RequestBody, headers, cookies) are validated and sanitized before use in SQL, shell commands, HTML, or URLs - Parameterized queries, All SQL queries use
PreparedStatementwith?placeholders, neverStatementwith string concatenation - No shell invocation with user input,
Runtime.exec()andProcessBuilderdo not pass user input throughsh -c; arguments are passed as separate array elements - Output encoding, All user-controlled data rendered in HTML is escaped (via
StringEscapeUtils.escapeHtml4(), Thymeleaf autoescaping, or equivalent) - Authentication on every endpoint, All sensitive endpoints verify the session/token before processing (Spring Security,
@PreAuthorize, or manual checks) - Server-side authorisation, Role and permission checks use server-side session data, not client-supplied headers
- Object-level access control, Resource access verifies the requesting user owns or is authorized for the specific resource
- Strong password hashing, Passwords are hashed with BCrypt, SCrypt, or Argon2, never MD5 or SHA-1
- Modern encryption, AES-GCM or AES-CBC with HMAC is used; no DES, no ECB mode
- Cryptographic randomness, Tokens and secrets use
SecureRandom, notjava.util.Random - No hardcoded secrets, API keys, passwords, and encryption keys are loaded from environment variables, a secrets manager, or externalized configuration
- Safe deserialisation,
ObjectInputStream.readObject()is never called on untrusted data; JSON (Jackson/Gson) is preferred over Java native serialisation - Safe YAML parsing,
SnakeYAMLusesSafeConstructor; defaultYaml()constructor is not used with untrusted input - XXE prevention,
DocumentBuilderFactoryandSAXParserFactorydisable external entities (FEATURE_SECURE_PROCESSING,disallow-doctype-decl) - SSRF protection, Outbound HTTP requests validate URLs against an allowlist; internal/metadata IPs are blocked
- Minimal error responses, Error responses do not include stack traces, database paths, or internal configuration;
server.error.include-stacktraceis set tonever - Secure CORS,
Access-Control-Allow-Originis set to specific trusted origins, not*or reflected from the request - Audit logging, Authentication events, privilege changes, and sensitive operations are logged
- No credentials in responses, API responses do not include passwords, API keys, or other secrets
- Thread safety, Shared mutable state accessed by multiple threads uses
synchronized,ReentrantLock, or concurrent collections (ConcurrentHashMap,AtomicInteger) - Null safety, Return values from
Map.get(), database queries, and external calls are checked fornullbefore use - Integer overflow protection, Arithmetic on
int/longusesMath.addExact(),Math.multiplyExact(), orMath.toIntExact()where overflow is possible - Dependency hygiene,
pom.xmlpins dependency versions; dependencies are checked for known CVEs withmvn dependency-check:check(OWASP Dependency-Check) ormvn versions:display-dependency-updates