Java Security Code Review: Vulnerability Patterns and Defensive Implementations
SQL Injection Vulnerabilities
JDBC Concatenation Flaws
Direct string concatenation in SQL queries without input validation creates injection vectors. When user-supplied parameters embed directly in to query strings, attackers can manipulate query logic.
Vulnerable Pattern:
public User fetchUserDetails(String userId) {
String query = "SELECT * FROM accounts WHERE uid = '" + userId + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(query);
// ...
}
Defensive Implementation: Parameterized queries eliminate injection risks by separating code from data:
public User fetchUserSecure(String userId) {
String query = "SELECT * FROM accounts WHERE uid = ?";
PreparedStatement pstmt = connection.prepareStatement(query);
pstmt.setString(1, userId);
ResultSet rs = pstmt.executeQuery();
// Process results...
}
Alternatively, input validation using allowlists provides defense-in-depth:
public boolean isValidIdentifier(String input) {
return input != null && input.matches("^[a-zA-Z0-9]{8,16}$");
}
MyBatis Framework Risks
MyBatis dynamic SQL features introduce injection risks when using ${} parameter substitution instead of #{} placeholder syntax.
Risky Implemantation:
@Select("SELECT * FROM products WHERE category = '${cat}'")
List<Product> searchByCategory(@Param("cat") String category);
Secure Alternatives: Use concatenation functions for dynamic patterns:
@Select("SELECT * FROM products WHERE category LIKE CONCAT('%', #{keyword}, '%')")
List<Product> fuzzySearch(@Param("keyword") String keyword);
For sorting operations, avoid direct parameter interpolation. Implement mapped sorting:
public List<User> getSortedUsers(String sortField, String order) {
String column = resolveColumn(sortField); // Maps to allowed columns only
String direction = "DESC".equalsIgnoreCase(order) ? "DESC" : "ASC";
return userMapper.orderBy(column, direction);
}
File Operation Vulnerabilities
Unrestricted File Uploads
Accepting files without validating extensions allows executable content upload.
Vulnerable Handler:
@PostMapping("/upload")
public ResponseEntity<String> handleUpload(MultipartFile incoming) {
Path destination = Paths.get(STORAGE_PATH, incoming.getOriginalFilename());
Files.write(destination, incoming.getBytes());
return ResponseEntity.ok("Uploaded");
}
Validation Strategy:
@PostMapping("/upload")
public ResponseEntity<String> secureUpload(MultipartFile incoming) {
String originalName = incoming.getOriginalFilename();
String extension = originalName.substring(originalName.lastIndexOf(".")).toLowerCase();
Set<String> permittedTypes = Set.of(".jpg", ".jpeg", ".png", ".gif", ".pdf");
if (!permittedTypes.contains(extension)) {
return ResponseEntity.badRequest().body("Unsupported file type");
}
String sanitized = UUID.randomUUID().toString() + extension;
Path destination = Paths.get(STORAGE_PATH, sanitized);
// Continue with storage...
}
Path Traversal
User-controlled filenames accessing filesystems require normalization to prevent directory escape sequences.
Vulnerable Retrieval:
@GetMapping("/download")
public byte[] getDocument(@RequestParam String filename) {
Path target = Paths.get(BASE_DIRECTORY, filename);
return Files.readAllBytes(target);
}
Secure Access Pattern:
public byte[] retrieveSafely(String userInput) throws IOException {
if (userInput.contains("..") || userInput.contains("/") || userInput.contains("\\")) {
throw new SecurityException("Invalid path sequence detected");
}
Path target = Paths.get(BASE_DIRECTORY).resolve(userInput).normalize();
if (!target.startsWith(Paths.get(BASE_DIRECTORY))) {
throw new SecurityException("Path traversal attempt blocked");
}
return Files.readAllBytes(target);
}
Cross-Site Scripting (XSS) Prevention
Output Encoding Strategies
Untrusted data rendered in HTML contexts requires contextual encoding.
Context-Aware Encoding:
import org.springframework.web.util.HtmlUtils;
import org.owasp.encoder.Encode;
public String sanitizeForHtml(String untrusted) {
return HtmlUtils.htmlEscape(untrusted);
}
public String encodeForAttribute(String untrusted) {
return Encode.forHtmlAttribute(untrusted);
}
Rich Content Handling
For HTML content that requires structural elements, use parser-based allowlisting:
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Safelist;
public String sanitizeRichContent(String rawHtml) {
Safelist policy = new Safelist()
.addTags("p", "br", "strong", "em", "a", "ul", "ol", "li")
.addAttributes("a", "href")
.addProtocols("a", "href", "https", "http");
return Jsoup.clean(rawHtml, policy);
}
Server-Side Request Forgery (SSRF) Mitigation
Unrestricted outbound requests enable attackers to probe internal networks and access restricted resources.
Multi-Layer Defense:
public String fetchRemoteResource(String targetUrl) {
// Layer 1: Protocol validation
if (!targetUrl.startsWith("http://") && !targetUrl.startsWith("https://")) {
return "Protocol not allowed";
}
try {
URI uri = new URI(targetUrl);
String host = uri.getHost();
// Layer 2: IP resolution and private range check
InetAddress address = InetAddress.getByName(host);
String ip = address.getHostAddress();
if (isInternalAddress(ip)) {
return "Internal resources inaccessible";
}
// Layer 3: Disable redirects to prevent bypasses
HttpURLConnection conn = (HttpURLConnection) uri.toURL().openConnection();
conn.setInstanceFollowRedirects(false);
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
return readResponse(conn);
} catch (Exception e) {
return "Request failed";
}
}
private boolean isInternalAddress(String ip) {
return ip.startsWith("127.") || ip.startsWith("10.") ||
ip.matches("^172\\.(1[6-9]|2[0-9]|3[01])\\..*") ||
ip.startsWith("192.168.");
}
Remote Code Execution Vulnerabilities
Command Injection Prevention
Executing system commands with user input requires strict validation.
Vulnerable Execution:
public String listDirectory(String path) {
ProcessBuilder pb = new ProcessBuilder("sh", "-c", "ls -la " + path);
Process process = pb.start();
// ...
}
Command Allowlisting:
public String executeAllowedCommand(String userCmd) {
Set<String> allowedCommands = Set.of("status", "version", "help");
String baseCommand = userCmd.split("\\s+")[0];
if (!allowedCommands.contains(baseCommand)) {
return "Command not authorized";
}
ProcessBuilder pb = new ProcessBuilder(baseCommand);
// Execute with restricted environment...
}
Script Engine Security
Dynamic script evaluation poses significant risks. Restrict code sources:
public Object evaluateScript(String scriptSource) {
Pattern allowedSource = Pattern.compile("^https://trusted-domain\\.com/scripts/.*$");
if (!allowedSource.matcher(scriptSource).matches()) {
throw new SecurityException("Untrusted script source");
}
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
// Additional sandboxing recommended...
return engine.eval(scriptSource);
}
Deserialization Security
Object Stream Validation
Java deserialization attacks exploit class instantiation during object reconstruction.
Vulnerable Deserialization:
public Object restoreObject(byte[] data) {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data));
return ois.readObject();
}
Class Allowlisting:
import org.apache.commons.io.serialization.ValidatingObjectInputStream;
public Object restoreSecure(byte[] data) {
try (ValidatingObjectInputStream vois = new ValidatingObjectInputStream(
new ByteArrayInputStream(data))) {
vois.accept(SafeEntity.class, UserProfile.class);
return vois.readObject();
}
}
YAML Processing
SnakeYAML's load() method can instantiate arbitrary classes. Use SafeConstructor:
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
public void parseConfiguration(String yamlContent) {
Yaml parser = new Yaml(new SafeConstructor());
Map<String, Object> config = parser.load(yamlContent);
// Process safe basic types only...
}
XMLDecoder Risks
XMLDecoder executes methods described in XML documents. Avoid with untrusted input:
// Dangerous - do not use with untrusted XML
XMLDecoder decoder = new XMLDecoder(new FileInputStream("data.xml"));
Object result = decoder.readObject();
XML External Entity (XXE) Prevention
XML parsers must disable DTDs and external entities:
public void parseDocument(InputStream xmlInput) throws Exception {
SAXParserFactory factory = SAXParserFactory.newInstance();
// Security configurations
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
SAXParser parser = factory.newSAXParser();
parser.parse(xmlInput, new DefaultHandler());
}
Similar configurations apply to DocumentBuilderFactory, XMLReader, and SAXReader (dom4j).
Access Control Mechanisms
Horizontal Privilege Escalation
Verify resource ownership before data retrieval:
public AccountDetails viewAccount(String accountId, UserSession session) {
AccountDetails account = repository.findById(accountId);
if (!account.getOwnerId().equals(session.getUserId())) {
throw new AccessDeniedException("Resource ownership mismatch");
}
return account;
}
Vertical Privilege Escalation
Enforce role checks for administrative functions:
public AdminPanel accessAdministration(UserSession session) {
if (!session.hasRole("ADMINISTRATOR")) {
throw new AccessDeniedException("Insufficient privileges");
}
return loadAdminInterface();
}
Additional Security Considerations
CSRF Protection
Validate request origins using synchronized tokens:
public ResponseEntity<String> processAction(HttpServletRequest request) {
String requestToken = request.getHeader("X-CSRF-Token");
String sessionToken = (String) request.getSession().getAttribute("csrfToken");
if (!Objects.equals(requestToken, sessionToken)) {
return ResponseEntity.status(403).body("Invalid security token");
}
// Process action...
}
Open Redirect Prevention
Validate redirection targets against appproved domains:
public String redirectUser(String target) {
Set<String> approvedHosts = Set.of("app.example.com", "docs.example.com");
try {
URI uri = new URI(target);
if (!approvedHosts.contains(uri.getHost())) {
return "redirect:/error";
}
return "redirect:" + target;
} catch (URISyntaxException e) {
return "redirect:/error";
}
}
JNDI Injection Defense
Restrict JNDI lookups to specific, safe contexts:
public Object lookupResource(String name) {
Set<String> permittedJndi = Set.of(
"java:comp/env/jdbc/primaryDB",
"java:comp/env/mail/session"
);
if (!permittedJndi.contains(name)) {
throw new NamingException("JNDI resource not authorized");
}
Context ctx = new InitialContext();
return ctx.lookup(name);
}
Regular Expression DoS (ReDoS)
Avoid vulnerable regex patterns that cause catastrophic backtracking. Use linear-time engines:
// Vulnerable to ReDoS
boolean risky = Pattern.matches("(a+)+$", userInput);
// Safer alternative using RE2
boolean safe = com.google.re2j.Pattern.matches("(a+)+$", userInput);
API Documentation Exposure
Restrict API documentation endpoints to development environments:
@Configuration
@Profile({"dev", "local"})
public class ApiDocsConfig {
// Swagger configuration...
}
This approach limits documentation exposure in production deployments.