Writing secure Java
Input validation, injection, unsafe deserialization, secrets handling.
All input is hostile
Treat every external input as hostile, even from "your own" frontend: attackers don't have to use it. Validate type, length and format at the boundary with allow-lists (only known-good values pass). Deny-lists always miss something.
Climbing out of the folder
What does this print?
Path base = Path.of("/srv/files");
Path p = base.resolve("docs/../../secret.txt")
.normalize();
System.out.println(p);
System.out.println(p.startsWith(base));/srv/secret.txt false/srv/files/secret.txt true/srv/files/docs/../../secret.txt true
Show the answer
normalize() resolves the .. segments, which climb out of /srv/files. The startsWith(base) check after normalizing reveals the escape: reject such paths.
Injection, everywhere
Never build SQL, OS commands or file paths by concatenating input. Use parameterized queries, pass command arguments as a list, and for paths normalize, then check they stay inside the allowed folder.
Deserializing strangers' bytes
Calling readObject() on untrusted data is one of Java's most dangerous lines. Crafted bytes can trigger gadget chains in classes on your classpath during deserialization, up to remote code execution, before you ever check the result. Use JSON with explicit types, or at least an ObjectInputFilter allow-list.
var in = new ObjectInputStream(request);
Object o = in.readObject(); // danger!Handling a secret
static final String KEY = "prod-9f2c";Ends up in source control, builds and logs. Rotate it!
String key = System.getenv("PAY_KEY");From the environment or a secrets manager; never in code.
Random enough?
You generate password-reset tokens. Why not new Random() or Math.random()?
Think about it, then reveal the answer
They're predictable once an attacker has seen a few outputs. Use **SecureRandom**, which is cryptographically strong.
No more Security Manager
Old Java tried to sandbox untrusted code with the Security Manager. It has been permanently disabled since Java 24 (JEP 486). To isolate untrusted code today, use OS-level mechanisms: containers or separate processes.
Security reviews
Reviewers look for exactly these lines: concatenated SQL, readObject() on request data, keys in source, user-controlled paths, Random for tokens. Automated scanners help, but knowing the patterns lets you avoid them while writing the code.
Key takeaways
- Allow-list validation beats deny-lists
- Parameterized queries; normalize and check file paths
- Untrusted ObjectInputStream data can mean remote code execution
- Secrets from env vars or a secrets manager; SecureRandom for tokens
The 2017 Equifax breach began with an unpatched vulnerability in Apache Struts, a Java web framework.
Practice questions
A download endpoint resolves a user-supplied file name. What does this print?
Path base = Path.of("/srv/files");
Path p = base.resolve("../../etc/passwd")
.normalize();
System.out.println(p);
System.out.println(p.startsWith(base));- /srv/etc/passwd false
- /srv/files/etc/passwd true
- /etc/passwd false
- /srv/files/../../etc/passwd true
Check your answer
/etc/passwd false. normalize() resolves the .. segments, which climb out of /srv/files to /etc/passwd. The startsWith check reveals the escape.
An endpoint accepts a Base64 blob and calls `new ObjectInputStream(in).readObject()` on it. What's the biggest risk?
- Base64 is slow to decode
- readObject() always returns null for remote data
- The blob might be too large to log
- Crafted data can trigger gadget chains during deserialization, up to remote code execution
Check your answer
Crafted data can trigger gadget chains during deserialization, up to remote code execution. Deserialization can run code in classes on your classpath before you ever check the result. Use JSON with explicit types, or at least an ObjectInputFilter allow-list.