Java Secure Coding Practices: OWASP Top 10 Mitigation Guide
Incorporating secure coding practices is essential to mitigate the OWASP Top 10 vulnerabilities in Java applications. This comprehensive guide provides practical code examples illustrating common vulnerabilities and their secure implementations.
1. Injection Attacks
SQL Injection Prevention
Vulnerable Code:
String userId = request.getParameter("userId");
String query = "SELECT * FROM users WHERE user_id = '" + userId + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(query);
Secure Code:
String userId = request.getParameter("userId");
String query = "SELECT * FROM users WHERE user_id = ?";
PreparedStatement pstmt = connection.prepareStatement(query);
pstmt.setString(1, userId);
ResultSet rs = pstmt.executeQuery();
Explanation: Using PreparedStatement with parameterized queries prevents SQL injection by separating SQL logic from data.
NoSQL Injection Prevention
Vulnerable Code (MongoDB):
// Direct string concatenation
String userQuery = "{ 'username': '" + username + "' }";
BasicDBObject query = BasicDBObject.parse(userQuery);
Secure Code:
// Using proper query construction
BasicDBObject query = new BasicDBObject();
query.put("username", username);
LDAP Injection Prevention
Vulnerable Code:
String filter = "(&(uid=" + username + ")(userPassword=" + password + "))";
NamingEnumeration results = ctx.search("ou=users,dc=example,dc=com", filter, controls);
Secure Code:
// Escape special characters
private static String escapeLDAPSearchFilter(String filter) {
StringBuilder sb = new StringBuilder();
for (char c : filter.toCharArray()) {
switch (c) {
case '\\': sb.append("\\5c"); break;
case '*': sb.append("\\2a"); break;
case '(': sb.append("\\28"); break;
case ')': sb.append("\\29"); break;
case '\0': sb.append("\\00"); break;
default: sb.append(c);
}
}
return sb.toString();
}
String escapedUsername = escapeLDAPSearchFilter(username);
String filter = "(&(uid=" + escapedUsername + ")(userPassword=" + escapeLDAPSearchFilter(password) + "))";
2. Broken Authentication
Secure Password Storage
Vulnerable Code:
// Password stored in plain text
String storedPassword = getUserPassword(username);
if (inputPassword.equals(storedPassword)) {
// Authenticate user
}
Secure Code:
import org.mindrot.jbcrypt.BCrypt;
// Password stored as a hashed value
public class SecurePasswordUtil {
public static String hashPassword(String password) {
return BCrypt.hashpw(password, BCrypt.gensalt(12));
}
public static boolean verifyPassword(String password, String hashedPassword) {
return BCrypt.checkpw(password, hashedPassword);
}
}
// Usage
String storedHashedPassword = getUserPassword(username);
if (SecurePasswordUtil.verifyPassword(inputPassword, storedHashedPassword)) {
// Authenticate user
}
Session Management
Vulnerable Code:
// Weak session ID generation
String sessionId = String.valueOf(System.currentTimeMillis());
Secure Code:
import java.security.SecureRandom;
import java.util.Base64;
public class SecureSessionManager {
private static final SecureRandom secureRandom = new SecureRandom();
public static String generateSessionId() {
byte[] randomBytes = new byte[32];
secureRandom.nextBytes(randomBytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
}
public static void configureSecureSession(HttpServletResponse response) {
// Set secure cookie attributes
Cookie sessionCookie = new Cookie("JSESSIONID", generateSessionId());
sessionCookie.setHttpOnly(true); // Prevent XSS
sessionCookie.setSecure(true); // HTTPS only
sessionCookie.setMaxAge(1800); // 30 minutes
sessionCookie.setPath("/");
sessionCookie.setSameSite(Cookie.SameSite.STRICT);
response.addCookie(sessionCookie);
}
}
3. Sensitive Data Exposure
Data Encryption
Vulnerable Code:
// Transmitting data over an unencrypted connection
URL url = new URL("http://example.com/api");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
Secure Code:
// Transmitting data over an encrypted connection
URL url = new URL("https://example.com/api");
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
// Additional security: Certificate pinning
conn.setSSLSocketFactory(createPinnedSSLSocketFactory());
Data Masking and Encryption
Secure Implementation:
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;
public class DataProtectionUtil {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 16;
public static SecretKey generateKey() throws Exception {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(256);
return keyGenerator.generateKey();
}
public static String encrypt(String plainText, SecretKey key) throws Exception {
byte[] iv = new byte[GCM_IV_LENGTH];
SecureRandom.getInstanceStrong().nextBytes(iv);
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
byte[] encryptedData = cipher.doFinal(plainText.getBytes());
byte[] encryptedWithIv = new byte[iv.length + encryptedData.length];
System.arraycopy(iv, 0, encryptedWithIv, 0, iv.length);
System.arraycopy(encryptedData, 0, encryptedWithIv, iv.length, encryptedData.length);
return Base64.getEncoder().encodeToString(encryptedWithIv);
}
public static String decrypt(String encryptedText, SecretKey key) throws Exception {
byte[] decodedData = Base64.getDecoder().decode(encryptedText);
byte[] iv = new byte[GCM_IV_LENGTH];
System.arraycopy(decodedData, 0, iv, 0, iv.length);
byte[] encrypted = new byte[decodedData.length - GCM_IV_LENGTH];
System.arraycopy(decodedData, GCM_IV_LENGTH, encrypted, 0, encrypted.length);
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec);
byte[] decryptedData = cipher.doFinal(encrypted);
return new String(decryptedData);
}
}
4. XML External Entities (XXE)
Vulnerable Code:
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(new File("input.xml"));
Secure Code:
import javax.xml.XMLConstants;
public class SecureXMLParser {
public static Document parseXML(File xmlFile) throws Exception {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// Disable external entity processing
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
// Secure processing
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
DocumentBuilder db = dbf.newDocumentBuilder();
return db.parse(xmlFile);
}
}
5. Security Misconfiguration
Error Handling
Vulnerable Code:
// Default error page revealing stack trace
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
Secure Code:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SecureErrorHandler {
private static final Logger logger = LoggerFactory.getLogger(SecureErrorHandler.class);
public static void handleError(HttpServletResponse response, Exception e, String userMessage) {
// Log detailed error for developers
logger.error("Application error occurred", e);
// Return generic message to user
try {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
userMessage != null ? userMessage : "An unexpected error occurred.");
} catch (IOException ioException) {
logger.error("Failed to send error response", ioException);
}
}
}
Security Headers
Secure Implementation:
@WebFilter("/*")
public class SecurityHeadersFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
// Prevent clickjacking
httpResponse.setHeader("X-Frame-Options", "DENY");
// Prevent MIME type sniffing
httpResponse.setHeader("X-Content-Type-Options", "nosniff");
// Enable XSS protection
httpResponse.setHeader("X-XSS-Protection", "1; mode=block");
// Enforce HTTPS
httpResponse.setHeader("Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload");
// Content Security Policy
httpResponse.setHeader("Content-Security-Policy",
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'");
// Hide server information
httpResponse.setHeader("Server", "");
chain.doFilter(request, response);
}
}
6. Cross-Site Scripting (XSS)
Vulnerable Code:
out.println("<div>" + userInput + "</div>");
Secure Code:
import org.apache.commons.text.StringEscapeUtils;
import org.owasp.encoder.Encode;
public class XSSPrevention {
// Using Apache Commons Text
public static String escapeHtml(String input) {
return StringEscapeUtils.escapeHtml4(input);
}
// Using OWASP Java Encoder
public static String encodeForHTML(String input) {
return Encode.forHtml(input);
}
public static String encodeForJavaScript(String input) {
return Encode.forJavaScript(input);
}
public static String encodeForCSS(String input) {
return Encode.forCssString(input);
}
public static String encodeForURL(String input) {
return Encode.forUriComponent(input);
}
}
// Usage
out.println("<div>" + XSSPrevention.encodeForHTML(userInput) + "</div>");
Content Security Policy Implementation
@WebServlet("/secure-page")
public class SecurePageServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// Set CSP header
response.setHeader("Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' https://cdn.example.com; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"font-src 'self' https://fonts.googleapis.com; " +
"connect-src 'self' https://api.example.com");
// Process request...
}
}
7. Insecure Deserialization
Vulnerable Code:
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data.ser"));
Object obj = ois.readObject();
Secure Code:
import java.io.*;
import java.util.Set;
import java.util.HashSet;
public class SecureDeserialization {
private static final Set<String> ALLOWED_CLASSES = new HashSet<>();
static {
ALLOWED_CLASSES.add("com.example.SafeClass");
ALLOWED_CLASSES.add("com.example.AnotherSafeClass");
// Add other safe classes
}
public static Object deserializeSecurely(InputStream input) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(input) {
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
String className = desc.getName();
if (!ALLOWED_CLASSES.contains(className)) {
throw new InvalidClassException("Unauthorized deserialization attempt", className);
}
return super.resolveClass(desc);
}
};
return ois.readObject();
}
// Alternative: Using JSON instead of Java serialization
public static <T> T deserializeFromJSON(String json, Class<T> clazz) {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
try {
return mapper.readValue(json, clazz);
} catch (Exception e) {
throw new RuntimeException("Deserialization failed", e);
}
}
}
8. Using Components with Known Vulnerabilities
Dependency Management
Maven Security Plugin Configuration:
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>8.4.0</version>
<configuration>
<failBuildOnCVSS>7</failBuildOnCVSS>
<suppressionFiles>
<suppressionFile>suppression.xml</suppressionFile>
</suppressionFiles>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
Vulnerable Dependency Example:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.1.0.RELEASE</version> <!-- Outdated version -->
</dependency>
Secure Dependency:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>6.0.11</version> <!-- Updated version -->
</dependency>
9. Insufficient Logging and Monitoring
Vulnerable Code:
// No logging of authentication attempts
public void authenticate(String username, String password) {
// Authentication logic
}
Secure Code:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
public class SecureAuthenticationService {
private static final Logger logger = LoggerFactory.getLogger(SecureAuthenticationService.class);
private static final Logger securityLogger = LoggerFactory.getLogger("SECURITY");
public boolean authenticate(String username, String password) {
MDC.put("username", username);
MDC.put("remoteIP", getCurrentUserIP());
try {
logger.info("Authentication attempt for user: {}", username);
// Authentication logic
boolean isAuthenticated = performAuthentication(username, password);
if (isAuthenticated) {
securityLogger.info("Successful authentication for user: {}", username);
} else {
securityLogger.warn("Failed authentication attempt for user: {}", username);
// Implement rate limiting/account lockout here
}
return isAuthenticated;
} catch (Exception e) {
securityLogger.error("Authentication error for user: {}", username, e);
return false;
} finally {
MDC.clear();
}
}
private String getCurrentUserIP() {
// Implementation to get current user's IP
return "192.168.1.1"; // placeholder
}
private boolean performAuthentication(String username, String password) {
// Actual authentication logic
return true; // placeholder
}
}
Security Event Monitoring
@Component
public class SecurityEventLogger {
private static final Logger securityLogger = LoggerFactory.getLogger("SECURITY");
public void logSecurityEvent(String eventType, String details, String severity) {
Map<String, Object> event = new HashMap<>();
event.put("timestamp", Instant.now());
event.put("eventType", eventType);
event.put("details", details);
event.put("severity", severity);
event.put("source", "application");
// Log as structured data
securityLogger.info("Security Event: {}", new ObjectMapper().writeValueAsString(event));
}
@EventListener
public void handleAuthenticationFailure(AuthenticationFailureEvent event) {
logSecurityEvent("AUTHENTICATION_FAILURE",
"Failed login attempt for user: " + event.getUsername(),
"HIGH");
}
@EventListener
public void handleUnauthorizedAccess(UnauthorizedAccessEvent event) {
logSecurityEvent("UNAUTHORIZED_ACCESS",
"Unauthorized access attempt to: " + event.getResource(),
"CRITICAL");
}
}
10. Unvalidated Redirects and Forwards
Vulnerable Code:
String url = request.getParameter("url");
response.sendRedirect(url);
Secure Code:
import java.net.URL;
import java.util.Set;
public class SecureRedirectHandler {
private static final Set<String> ALLOWED_DOMAINS = Set.of(
"example.com",
"subdomain.example.com",
"api.example.com"
);
public static boolean isValidRedirectUrl(String url) {
try {
URL redirectUrl = new URL(url);
String host = redirectUrl.getHost().toLowerCase();
// Check if it's a relative URL (safe)
if (url.startsWith("/") && !url.startsWith("//")) {
return true;
}
// Check if host is in allowed domains
return ALLOWED_DOMAINS.contains(host) ||
ALLOWED_DOMAINS.stream().anyMatch(domain -> host.endsWith("." + domain));
} catch (Exception e) {
return false;
}
}
public static void safeRedirect(HttpServletResponse response, String url) throws IOException {
if (isValidRedirectUrl(url)) {
response.sendRedirect(url);
} else {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid redirect URL");
}
}
}
// Usage in servlet
String url = request.getParameter("url");
SecureRedirectHandler.safeRedirect(response, url);
Additional Security Best Practices
Input Validation Framework
import javax.validation.constraints.*;
import org.hibernate.validator.constraints.SafeHtml;
public class UserInput {
@NotNull
@Size(min = 3, max = 50)
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "Username must contain only alphanumeric characters and underscores")
private String username;
@NotNull
@Email
private String email;
@NotNull
@Size(min = 8, max = 100)
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]",
message = "Password must contain uppercase, lowercase, digit and special character")
private String password;
@SafeHtml
@Size(max = 1000)
private String description;
// Getters and setters...
}
Secure Random Number Generation
import java.security.SecureRandom;
import java.security.NoSuchAlgorithmException;
public class SecureRandomGenerator {
private static final SecureRandom secureRandom;
static {
try {
secureRandom = SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Strong SecureRandom not available", e);
}
}
public static String generateSecureToken(int length) {
byte[] randomBytes = new byte[length];
secureRandom.nextBytes(randomBytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
}
public static int generateSecureInt(int bound) {
return secureRandom.nextInt(bound);
}
}
Security Testing
Security Unit Test Example
@Test
public void testSQLInjectionPrevention() {
String maliciousInput = "'; DROP TABLE users; --";
// This should not cause SQL injection
assertDoesNotThrow(() -> {
userService.findUserById(maliciousInput);
});
// Verify that no actual SQL injection occurred
assertTrue(userService.getAllUsers().size() > 0);
}
@Test
public void testXSSPrevention() {
String xssPayload = "<script>alert('XSS')</script>";
String encoded = XSSPrevention.encodeForHTML(xssPayload);
assertFalse(encoded.contains("<script>"));
assertTrue(encoded.contains("<script>"));
}
@Test
public void testPasswordHashing() {
String password = "TestPassword123!";
String hashedPassword = SecurePasswordUtil.hashPassword(password);
assertNotEquals(password, hashedPassword);
assertTrue(SecurePasswordUtil.verifyPassword(password, hashedPassword));
assertFalse(SecurePasswordUtil.verifyPassword("WrongPassword", hashedPassword));
}
Conclusion
Implementing these secure coding practices helps protect Java applications against the OWASP Top 10 vulnerabilities. Key takeaways include:
- Always validate and sanitize input before processing
- Use parameterized queries to prevent injection attacks
- Implement proper authentication and session management
- Encrypt sensitive data both in transit and at rest
- Configure security headers and error handling properly
- Keep dependencies updated and monitor for vulnerabilities
- Implement comprehensive logging for security events
- Validate all redirects and forwards against allowed destinations
Regular security testing, code reviews, and staying updated with the latest security practices are essential for maintaining secure Java applications.