Do this, not that: 18 better ways to build secure Java apps
Is your Java code secure? Here are 18 changes developers can make to eliminate vulnerabilities and adopt safer methods for stronger, resilient application code.
Dec 19, 2024 • 7 Minute Read
Java security can feel like a daunting challenge, but it doesn't have to be. By making small but impactful changes, you can significantly enhance the security posture of your Java applications. Here are ten common practices and the more secure alternatives you should adopt instead.
1. Up-to-Date Dependencies
Not That: Using outdated versions of libraries can introduce vulnerabilities that have already been fixed in newer releases.
Do This: Use tools like OWASP Dependency-Check or Snyk to keep track of vulnerable dependencies and apply updates regularly.
Even Better: Automate dependency checking as part of your CI/CD pipeline to ensure that outdated libraries are caught early.
2. Injection
Not That: Whether it is adding user input to a SQL query, a SpEL expression, a log message, or HTML, you should never add it to the expression by way of concatenation
Do This: Use APIs that allow you to send in user input separately from the instruction; `PreparedStatement` for SQL, `SimpleEvaluationContext` for SpEL. For things that don’t have an API, encode the output.
Even Better: Use allow lists to validate that all inputs have exactly the values you consider safe and appropriate for each datatype.
3. Exception Swallowing
Not That: Ignoring exceptions or logging insufficient information can lead to undetected issues and vulnerabilities.
Do This: Implement proper exception handling to log relevant information without exposing sensitive data. Make sure your application fails securely, providing minimal information to an attacker.
Even Better: Use centralized logging and monitoring tools like ELK stack or Splunk to ensure exceptions are tracked and addressed promptly.
4. Exception Propagation
Not That: Showing detailed exception messages to end users can reveal sensitive information about your application's internals, such as server configurations, database schema, or even stack traces.
Do This: Translate exceptions into user-friendly error messages that do not expose underlying infrastructure details.
Even Better: Log detailed error information on the server side that correlates the user error with the system error for simpler root cause analysis.
5. Resource Management
Not That: Manually closing resources like file streams or database connections can lead to resource leaks if not handled correctly.
Do This: Use a try-with-resources statement to automatically close resources, ensuring they are properly released.
Even Better: Always use logging or monitoring tools to detect and alert on potential resource leaks to catch issues early.
6. Fully-Constructed Objects
Not That: Using non-final member variables in classes can lead to incomplete construction, making objects vulnerable to race conditions or being manipulated in an inconsistent state. It also allows applications to break encapsulation through reflection
Do This: Declare member variables as `final` wherever possible to ensure they are initialized once and not changed afterward. This helps prevent concurrency errors and security vulnerabilities related to incomplete construction.
Even Better: Use `final` on classes, too. Attackers can't then override to alter sensitive behavior. Also, you can always remove the `final` keyword later when needed.
7. Reflection
Not That: Using reflection to access private fields or methods can bypass Java's security model and lead to unintended vulnerabilities, especially when combined with user input.
Do This: Avoid using reflection where possible. Instead, use proper APIs to access data and functionality, maintaining clear access controls.
Even Better: If reflection is necessary, validate all inputs rigorously and use Java's `AccessController` to ensure permissions are properly enforced.
8. Random Number Generation
Not That: Using `java.util.Random` for generating tokens or passwords is insecure as it's not designed for cryptographic purposes.
Do This: Use `java.security.SecureRandom` to ensure your random values are secure and harder to predict.
9. SSL Validation
Not That: Disabling SSL certificate validation for HTTPS connections can expose your application to man-in-the-middle attacks.
Do This: Always validate SSL certificates, even in development environments. (I know it can be a pain)
Even Better: Consider integrating tools like Let's Encrypt to automate certificate management.
10. Hashing Algorithms
Not That: MD5 and SHA-1 are no longer considered secure hashing algorithms. They have too many collisions for securing sensitive information or for verifying data integrity.
Do This: Use more secure algorithms like SHA-256 or, better yet, use a password-hashing algorithm like bcrypt, scrypt, or Argon2.
Even Better: Use libraries like Spring Security that not only do the hashing for you but provide APIs for upgrading your old insecure passwords to modern secure ones.
11. Key Management
Not That: Using outdated or weak algorithms like DSA for key pair generation is not recommended.
Do This: Use strong algorithms such as RSA with a key size of at least 2048 bits or EC (Elliptic Curve) with curves like secp256r1 for better security. Use KeyPairGenerator with RSA or EC and appropriate key sizes to ensure cryptographic strength.
Even Better: Outsource key management to a utility like Vault.
12. Serialization and Deserialization
Not That: Using Java's built-in serialization can expose your application to security vulnerabilities such as deserialization attacks, where an attacker crafts malicious serialized objects.
Do This: Avoid Java's native serialization and use a more secure serialization protocol, such as JSON or Protocol Buffers.
Even Better: Assess whether signing and verifying serialized data is necessary to ensure data integrity and non-repudiation, especially when handling sensitive information.
13. Configuration Values
Not That: Storing passwords or sensitive credentials in plaintext within configuration files is risky. These files can easily be accessed if they end up in source control or fall into the wrong hands.
Do This: Use environment variables or a secrets management service to store sensitive information. This makes it less likely that these secrets will be inadvertently exposed.
Even Better: Use a static analysis tool to warn of potential hardcoded passwords.
14. XML Parsing
Not That: Using default configurations in XML parsers like `DocumentBuilderFactory` or `SAXParserFactory` can expose your application to XXE (XML External Entity) attacks.
Do This: Disable external entity resolution by configuring XML parsers properly. For example, call `factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)` to prevent external entities.
Even Better: Consider moving to JSON or something else that is referenceless.
15. File System
Not That: Querying the file system using unsanitized user input can lead to security issues like directory traversal
Do This: Always canonicalize and validate user input before using it in any file system or system command operations. For file paths and ZIP files, ensure that the user input does not allow for navigation outside intended directories.
Even Better: Avoid passing user-supplied input to the file system, separating, for example, the user’s desired name for the file from its internal name.
16. URL Names
Not That: Revealing the underlying technology stack through URL extensions, such as `.jsp` or `.do` can give attackers insight into your application's structure and make it easier for them to exploit known vulnerabilities.
Do This: Use generic URL extensions like `.html` that do not reveal implementation details.
Even Better: Move your UI layer to the browser or mobile device, allowing Java to serve data and enforce security.
17. ORMs and Databases
Not That: While ORMs are powerful and important, their impact on the garbage collector is often neglected in favor of the convenience of letting them hydrate large object graphs. This is taxes the system in a way that attackers can leverage.
Do This: Favor raw queries, allowing the ORM to perform the task of hydrating exactly what you need to perform a given task. This approach ensures that only the data you need is queried, reducing memory overhead.
Even Better: Favor the Java `Stream` API and iterators, allowing you to load less into memory at once. Use pagination and set query limits to control the amount of data being loaded.
18. String Pools
Not That: Storing sensitive data, such as passwords, in standard `String` objects can leave it visible in thread dumps.
Do This: Use mutable character arrays (`char[]`) to store sensitive data so that they can be explicitly overwritten after use.
Even Better: Leverage secure APIs or third-party libraries that handle sensitive data securely and ensure that data is wiped from memory as soon as it is no longer needed.
Key Takeaways
Improving your Java application's security doesn't require a complete overhaul. Often, it's a matter of making small pivots over time and then adding tests and automation to ensure that those practices stay in place. By addressing these common security pitfalls, you can make your application significantly more resilient against threats.
Security is a process, not a one-time checklist. Keep learning, adapting, and applying best practices as the threat landscape evolves. See which of these you can start doing today.