DEV Community

Cover image for 5 Essential Java Security Techniques: Build Unbreakable Multi-Layered Defense Systems
Nithin Bharadwaj
Nithin Bharadwaj

Posted on

5 Essential Java Security Techniques: Build Unbreakable Multi-Layered Defense Systems

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>
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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.");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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 ...
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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";
};
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)