As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!
Building strong security for a Java application is a lot like securing a house. You don't just put a lock on the front door and call it a day. You check the materials the house is built with, you install alarms inside the rooms, you teach everyone who lives there good habits, and you make sure the walls themselves are strong. Modern Java security works the same way—it’s about layers. We need to protect our applications while they're being built and while they're running.
I want to share five practical techniques that create these layers. We'll look at how to check the building blocks we use, how to document them, how to build alarms into the application itself, how to write safer code from the start, and how to use the Java platform's own features to build a strong perimeter.
Let's start with the foundation: the libraries and frameworks we bring into our projects. Today, most of an application is made from these third-party components. If one of them has a flaw, it becomes our flaw. The first line of defense is to find these known problems before they ever reach a production environment.
We can use automated tools that scan our project's list of dependencies. These tools connect to databases that track publicly known security vulnerabilities. They tell us if a library we're using, or even a library that one of our libraries uses, has a reported issue. The goal is to catch this during the build process. I set up my builds to fail if they find a critical vulnerability. This forces the team to address the issue immediately, either by updating the library, replacing it, or documenting a justified exception.
Here is how you can integrate this into a Maven project. The plugin runs during the verify phase of the build.
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>9.0.9</version>
<executions>
<execution>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
<configuration>
<!-- Fail the build if a vulnerability with a score of 7 or higher is found -->
<failBuildOnCVSS>7</failBuildOnCVSS>
<!-- A file where we can list false positives or accepted risks -->
<suppressionFile>${project.basedir}/suppressions.xml</suppressionFile>
<!-- Output reports in HTML and JSON formats -->
<format>HTML</format>
<formats>HTML,JSON</formats>
<!-- Skip scanning for .NET assemblies in a Java project -->
<analyzer>
<assemblyEnabled>false</assemblyEnabled>
</analyzer>
</configuration>
</plugin>
For Gradle users, the setup is similarly straightforward in your build.gradle file.
plugins {
id 'org.owasp.dependencycheck' version '9.0.9'
}
dependencyCheck {
failBuildOnCVSS = 7.0
suppressionFile = file('suppressions.xml')
formats = ['HTML', 'JSON']
analyzers {
assemblyEnabled = false
}
}
When the build runs, it generates a report. You'll see a list of dependencies, the vulnerabilities found, their severity scores, and links to more information. I make it a habit to run this scan locally before I push any code update. It’s a simple step that prevents a lot of potential trouble.
Once you know what's in your application, you need a formal record of it. This is especially important for larger organizations or when you supply software to others. A Software Bill of Materials, or SBOM, is a complete list of all the components in your application. Think of it as the ingredient list on a food package. If there's a recall on peanuts, you can instantly check which of your products contains them.
Generating an SBOM is not complicated. It can be done as part of your build process. A common format is CycloneDX. Here's how to create one with Maven.
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>2.7.11</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>makeAggregateBom</goal>
</goals>
</execution>
</executions>
<configuration>
<projectType>application</projectType>
<schemaVersion>1.5</schemaVersion>
<includeBomSerialNumber>true</includeBomSerialNumber>
<!-- Include dependencies used at compile and runtime -->
<includeCompileScope>true</includeCompileScope>
<includeRuntimeScope>true</includeRuntimeScope>
<includeProvidedScope>true</includeProvidedScope>
<!-- Usually, test dependencies are excluded from the SBOM -->
<includeTestScope>false</includeTestScope>
<outputFormat>all</outputFormat>
<outputName>sbom</outputName>
</configuration>
</plugin>
This creates a file, typically target/bom.json or target/bom.xml, that details every component. You can then use this SBOM file in other tools. For instance, you could write a simple utility to continuously check your SBOM against new vulnerability disclosures.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
public class SbomMonitor {
private static final String VULN_API_URL = "https://api.vulndb.example.com/check";
public void monitorForNewVulnerabilities(File sbomFile) throws Exception {
ObjectMapper mapper = new ObjectMapper();
JsonNode bom = mapper.readTree(sbomFile);
JsonNode components = bom.get("components");
HttpClient client = HttpClient.newHttpClient();
for (JsonNode component : components) {
String name = component.get("name").asText();
String version = component.get("version").asText();
String purl = component.get("purl").asText(); // Package URL
// Query an external vulnerability database
String queryBody = String.format("{\"purl\": \"%s\"}", purl);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(VULN_API_URL))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(queryBody))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
// Process the response to see if new vulns were found for this component
if (response.statusCode() == 200) {
JsonNode vulnResponse = mapper.readTree(response.body());
if (vulnResponse.has("vulnerabilities")) {
System.out.println("Alert: New vulns found for " + name + "@" + version);
// Trigger an alert: email, Slack message, JIRA ticket, etc.
}
}
}
}
}
This proactive approach means you're not waiting for a weekly scan. You can be notified as soon as a new problem is found in a component you've already shipped.
The next layer moves us from build-time to runtime. Traditional security is like a castle wall and a moat—it protects the perimeter. But what if an attacker gets inside? Runtime Application Self-Protection, or RASP, is like having guards and motion sensors inside the castle itself. The security is built into the application, so it travels with the code wherever it runs.
A RASP agent can monitor the application's behavior and its interactions. It can detect and block attacks like SQL injection, path traversal, or excessive data exposure from within the application logic. While full RASP solutions are commercial products, we can implement some core ideas ourselves to understand the concept.
For example, we can create a custom security manager to watch for suspicious patterns. This is a more advanced technique, but it shows the principle.
import java.security.Permission;
import java.util.regex.Pattern;
import java.util.logging.Logger;
public class AppSecurityMonitor extends SecurityManager {
private static final Logger LOG = Logger.getLogger(AppSecurityMonitor.class.getName());
private final Pattern dangerousSqlPattern =
Pattern.compile("(?i)(\\bunion\\b.*\\bselect\\b|\\binsert\\b.*\\binto\\b|'\\s*or\\s*['1]|;\\s*--|/\\*.*\\*/)");
@Override
public void checkPermission(Permission perm) {
// We can log specific permission checks for auditing
if (perm.getName().startsWith("exitVM")) {
LOG.warning("Attempt to exit JVM detected: " + perm.getName());
// We could throw a SecurityException here to block it
}
}
// A method our own code can call for validation
public void inspectUserInput(String input, String context) {
if (input == null) return;
// Check for basic SQL injection patterns
if (dangerousSqlPattern.matcher(input).find()) {
String msg = "Potential SQL injection in " + context + ": " + input.substring(0, Math.min(input.length(), 50));
LOG.severe(msg);
throw new SecurityException("Invalid input detected.");
}
// Check for path traversal patterns
if (input.contains("../") || input.contains("..\\")) {
String msg = "Potential path traversal in " + context + ": " + input;
LOG.severe(msg);
throw new SecurityException("Invalid input detected.");
}
}
public void monitorFileOperation(String path, String action) {
// Log or block sensitive file access
if (path.contains("/etc/passwd") || path.contains("/etc/shadow")) {
LOG.severe("Blocked attempt to access sensitive file: " + path + " via action: " + action);
throw new SecurityException("Access to protected file denied.");
}
}
}
To install this monitor, you would set it as the security manager when the application starts. Be cautious, as a poorly configured security manager can break your application.
public class ApplicationMain {
public static void main(String[] args) {
// Install our custom security monitor
if (System.getSecurityManager() == null) {
System.setSecurityManager(new AppSecurityMonitor());
}
// Start the rest of the application...
new SpringApplication(ApplicationMain.class).run(args);
}
}
Then, in your data access code, you can actively use the monitor.
@Repository
public class UserRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
// Assume we have a reference to our monitor
private AppSecurityMonitor securityMonitor = new AppSecurityMonitor();
public User findUserByUsername(String username) {
// Validate input before using it
securityMonitor.inspectUserInput(username, "findUserByUsername");
// Use a parameterized query, which is the primary defense
String sql = "SELECT * FROM users WHERE username = ?";
return jdbcTemplate.queryForObject(sql, new Object[]{username}, new UserRowMapper());
}
}
The key idea here is that the security logic is intertwined with the application logic. It can make context-aware decisions that a firewall at the network boundary never could.
All the runtime protection in the world won't help if the code itself is written in an unsafe way. This is where secure coding practices come in. The goal is to catch common mistakes as early as possible, ideally while the developer is still writing the code. We use static analysis tools that examine source code for patterns that often lead to vulnerabilities.
These tools can be integrated into your IDE, your build process, or your code review pipeline. They look for things like using String for passwords (which linger in memory), building SQL queries by concatenating strings, or exposing internal data structures.
Consider this flawed code that a tool would flag immediately.
public class LoginService {
// PROBLEM: Password as String. Strings are immutable and may linger in memory.
public boolean authenticate(String username, String password) {
String sql = "SELECT password_hash FROM users WHERE username = '" + username + "'";
// MAJOR PROBLEM: Concatenating user input directly into SQL.
ResultSet rs = statement.executeQuery(sql);
// ... compare password hashes ...
}
}
A static analysis tool like SpotBugs with the Security plugin would highlight both issues. Let's look at a corrected version that also uses the tool's annotations.
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
public class SecureLoginService {
// Use char[] for passwords so we can wipe the array after use
public boolean authenticate(String username, char[] password) {
boolean authenticated = false;
try {
// Use parameterized queries ALWAYS
String sql = "SELECT password_hash, salt FROM users WHERE username = ?";
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setString(1, username);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
byte[] expectedHash = rs.getBytes("password_hash");
byte[] salt = rs.getBytes("salt");
byte[] actualHash = computePbkdf2Hash(password, salt);
authenticated = MessageDigest.isEqual(expectedHash, actualHash);
}
} catch (SQLException e) {
// Handle exception appropriately
} finally {
// Crucial step: Clear the password from memory
Arrays.fill(password, '\0');
}
return authenticated;
}
// A case where you might need dynamic SQL (e.g., a dynamic table name)
// The tool might flag it, but you can suppress with justification if it's safe.
@SuppressFBWarnings(
value = "SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING",
justification = "Table name is validated against a strict allow-list."
)
public ResultSet queryAuditTable(String yearMonth) {
// Validate the input format and allow-list
if (!yearMonth.matches("\\d{4}_\\d{2}") || !ALLOWED_AUDIT_TABLES.contains(yearMonth)) {
throw new IllegalArgumentException("Invalid or disallowed table identifier.");
}
// Even though the table name is dynamic, the WHERE clause parameters are still safe.
String sql = "SELECT * FROM audit_" + yearMonth + " WHERE user_id = ?";
// The actual parameter for user_id will be set using stmt.setInt()
return connection.prepareStatement(sql);
}
private static final Set<String> ALLOWED_AUDIT_TABLES = Set.of("2024_01", "2024_02", "2024_03");
}
Integrating these checks into the build is simple. For Maven, you can use the SpotBugs Maven plugin. When you run mvn spotbugs:check, the build will fail if it finds high-priority security bugs. This turns secure coding from a guideline into a requirement that's enforced automatically.
Finally, we come to the walls and moat—the security features built into the Java platform itself. The Java Security Manager and the Module System (introduced in Java 9) allow you to define very precisely what your code is allowed to do. This is called the principle of least privilege. Your application should only have the permissions it absolutely needs to function.
While the Security Manager is being deprecated in recent JDK releases, understanding it teaches the core concepts of sandboxing. The Module System is the modern, more robust way to enforce boundaries. Let's look at both.
First, a Java policy file defines permissions. You can create a file named app.policy.
// Grant permissions to our application code
grant codeBase "file:/opt/myapp/application.jar" {
// Allow reading/writing only to its own data directory
permission java.io.FilePermission "/opt/myapp/data/*", "read,write";
// Allow network connections only to specific backend services
permission java.net.SocketPermission "db.internal.example.com:3306", "connect";
permission java.net.SocketPermission "cache.internal.example.com:11211", "connect";
// Allow reading its own configuration properties
permission java.util.PropertyPermission "app.config.*", "read";
// Explicitly DENY dangerous permissions
permission java.lang.RuntimePermission "exitVM", "none";
permission java.lang.RuntimePermission "setSecurityManager", "none";
// Deny reading sensitive system files
permission java.io.FilePermission "/etc/passwd", "read";
permission java.io.FilePermission "/etc/shadow", "read";
permission java.io.FilePermission "C:\\Windows\\System32\\-", "read";
};
You would run your application with these JVM arguments: -Djava.security.manager -Djava.security.policy==/opt/myapp/conf/app.policy. The double equals (==) means use only this policy file.
The more modern approach is to use the Java Module System (JPMS). By defining a module-info.java file, you explicitly declare what your module needs and what it exposes. This prevents accidental use of internal APIs and enables strong encapsulation.
// module-info.java for the main application module
module com.example.myapp {
// What we depend on
requires java.sql;
requires java.logging;
requires com.fasterxml.jackson.databind;
// What we expose to other modules. Only the public API.
exports com.example.myapp.api;
exports com.example.myapp.dto;
// Allow reflective access ONLY to our testing framework and ORM.
// This is a controlled opening, not a free-for-all.
opens com.example.myapp.entity to org.hibernate.orm, org.junit.platform.commons;
// Declare that we use a Service Provider Interface (SPI)
uses com.example.myapp.spi.EncoderProvider;
// And declare our own implementation of that SPI
provides com.example.myapp.spi.EncoderProvider
with com.example.myapp.internal.Sha256EncoderProvider;
}
When you run a modular application, you can add the JVM flag --illegal-access=deny. This will block any code from using reflection to access non-public members of APIs in other modules unless it has been explicitly opens-ed to them. This significantly reduces the attack surface available to a malicious library that might find its way into your classpath.
Combining these five techniques creates a robust, multi-layered security posture for your Java applications. It starts with vetting your supplies (dependency scanning), keeping a good inventory (SBOM), building in alarms (RASP concepts), training your builders in safe practices (secure coding), and finally, constructing strong, compartmentalized walls (JVM security).
None of these layers is perfect on its own. A vulnerability might be too new for the scanner. A clever attack might evade a simple RASP rule. A developer might miss a warning from a static analysis tool. But together, they create a formidable defense. They ensure that security is not an afterthought or a separate team's problem, but an integral part of the development and operation lifecycle. You build it in from the start, and you protect it while it runs. That’s how you build applications that are not just functional, but resilient and trustworthy.
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva
Top comments (0)