Security in Java Modules: Limiting Attack Surface with JPMS

Illustration for Security in Java Modules: Limiting Attack Surface with JPMS
By Last updated:

A common mistake developers make when working with Java applications is leaving too many classes and packages exposed on the classpath. This makes sensitive internals accessible to attackers or malicious code. With the Java Platform Module System (JPMS) introduced in Java 9, developers now have a powerful way to limit the attack surface by controlling visibility and encapsulation.

In large enterprise systems and microservices, security breaches often occur because internal APIs were unintentionally accessible. By properly designing modules, you can restrict what is visible to consumers and prevent unauthorized reflection, creating more secure applications.

Think of JPMS modules like secure office buildings. Each department has locked doors (non-exported packages), while only the reception desk is public (exported APIs). Unauthorized personnel cannot wander into payroll or IT rooms without explicit permission.


Key Security Features of JPMS

1. Strong Encapsulation

By default, packages are not exported. Only explicitly exported ones are accessible:

module com.example.secureapp {
    exports com.example.secureapp.api;
    // internal logic hidden
}

2. Controlling Reflection with opens

Reflection is a major source of attack vectors. JPMS lets you control it:

module com.example.secureapp {
    opens com.example.secureapp.model to com.fasterxml.jackson.databind;
}
  • opens grants selective reflective access to trusted libraries.
  • Avoid using open (opens all packages) unless absolutely required.

3. Using requires transitive Carefully

Only expose dependencies clients truly need:

module com.example.secureapp {
    requires transitive java.sql; // only if clients use it
}

4. Service-Oriented Security

Use uses and provides for safe extensibility without leaking internals:

module com.example.secureapp {
    exports com.example.secureapp.spi;
    uses com.example.secureapp.spi.Plugin;
    provides com.example.secureapp.spi.Plugin with com.example.secureapp.internal.SecurePlugin;
}

Pitfalls That Weaken Security

❌ Exporting all packages (exports ...) → Exposes internals to attackers
❌ Using open unnecessarily → All packages open to reflection
❌ Overusing requires transitive → Bloats client dependencies and increases exposure
❌ Relying on automatic modules → Can unintentionally expose packages


Best Practices for Securing Modules

✅ Export only stable, public APIs
✅ Use opens sparingly and target only required packages
✅ Keep internal logic hidden by default
✅ Minimize transitive dependencies
✅ Bundle applications with jlink to remove unused modules and reduce runtime surface
✅ Regularly audit dependencies with jdeps


Example: Secure API Design

module-info.java

module com.example.banking {
    exports com.example.banking.api;
    opens com.example.banking.dto to com.fasterxml.jackson.databind; 
    requires java.sql;
    uses com.example.banking.spi.AuditLogger;
    provides com.example.banking.spi.AuditLogger
        with com.example.banking.impl.SecureAuditLogger;
}

Here:

  • Only api is exported
  • Reflection is restricted to Jackson for DTOs
  • Services allow extensibility without exposing internals

What's New in Java Versions?

  • Java 5–8 → N/A (No JPMS)
  • Java 9 → JPMS introduced, strong encapsulation, exports, opens
  • Java 11 → Tooling improvements (jdeps, better error reporting)
  • Java 17 → Performance and stability refinements in JPMS
  • Java 21 → No significant updates across Java versions for this feature

Real-World Analogy

Securing modules is like airport security. Passengers (classes) can only access the gates (public APIs) if properly ticketed (exported). Restricted areas (internal packages) remain locked down. Reflection rules (opens) act like special access badges granted only to trusted staff.


Summary & Key Takeaways

  • JPMS helps minimize the attack surface by restricting access to internal code
  • Use exports and opens selectively to balance usability and security
  • Avoid overexposing with open and automatic modules
  • Use jlink to remove unnecessary runtime modules
  • Regularly audit dependencies to detect overexposed packages

FAQ: Security in Java Modules

1. What is the difference between classpath and module path in terms of security?
Classpath exposes everything, while module path enforces strict boundaries.

2. Why do I get “package is not visible” errors?
Because the package is not exported in module-info.java.

3. What is the purpose of requires transitive?
It allows downstream modules to access dependencies automatically but may widen exposure.

4. How do open and opens differ?
open exposes the entire module to reflection; opens restricts reflection to specific packages.

5. What are automatic modules, and should I use them?
They are plain JARs treated as modules. Use them temporarily, not in production.

6. How does JPMS improve security compared to the classpath?
By enforcing encapsulation and preventing unauthorized access.

7. When should I use jlink vs jmod?
Use jmod for packaging, jlink for building reduced, secure runtimes.

8. Can I secure legacy projects incrementally with modules?
Yes, modularize gradually, exporting only stable APIs.

9. How do I handle third-party libraries that aren’t modularized?
Use them on the classpath or convert them into automatic modules carefully.

10. Do frameworks like Spring or Hibernate work with JPMS security rules?
Yes, but some reflective frameworks may need opens for certain packages.