Secure Coding
Security is a habit you build into every line, not a feature you bolt on later. By the end of this lesson you'll validate input, write injection-proof SQL, generate unguessable tokens, hash passwords correctly, and recognise the OWASP Top 10 risks in your own Java code.
Learn Secure Coding in our free Java course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free Java course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
You should be comfortable with JDBC (where SQL injection lives), REST APIs (where untrusted input arrives), and Exception Handling (so errors don't leak internals). A basic grasp of HTTP helps too.
Think of your application like a nightclub. Input validation is the bouncer who checks IDs at the door and only lets in people on the guest list — an allow-list. PreparedStatement is the rule that drinks orders are handed to the bartender on a printed ticket, so a customer can't shout an extra instruction and have it obeyed.
SecureRandom is the cloakroom ticket that's impossible to forge, while Math.random() is a ticket numbered 1, 2, 3 that anyone can guess. Hashing passwords is shredding the guest list so even a thief who grabs it can't read the names. And the OWASP Top 10 is the safety inspector's checklist of the ten ways clubs most often get robbed.
Every value that comes from outside your program — form fields, URL parameters, headers, uploaded files — is untrusted until you prove otherwise. The strongest approach is an allow-list (also called a whitelist): describe exactly what a valid value looks like and reject everything else. That's safer than a block-list, because you can't possibly list every bad input an attacker might invent.
In the example below, a username must match a strict regular expression. The two injection payloads are rejected before they can ever reach a database or a web page.
SQL injection happens when user input is glued into a query string and the database parses part of that input as SQL. It is OWASP A03: Injection and has caused some of the largest breaches in history. The fix is simple and absolute: never concatenate input into SQL. Use a PreparedStatement with ? placeholders, and bind each value with setString / setInt .
The placeholder sends the SQL and the data to the driver separately, so the value ' OR '1'='1 is searched for as a literal name instead of changing the query. Compare the vulnerable and fixed methods side by side.
The same idea applies when you run external programs. If you build a shell command by concatenating input and hand it to Runtime.exec("sh -c ...") , the shell interprets metacharacters like ; , | , and & — so a filename of report.txt; rm -rf / runs a second, destructive command.
Use ProcessBuilder with each argument as its own list element and no shell . The OS receives the arguments as a list, so metacharacters are just literal characters in a filename — never commands.
Security tokens must be unpredictable . Math.random() and java.util.Random are ordinary pseudo-random generators: see a few outputs and you can predict the rest. For session IDs, reset links, and API keys use java.security.SecureRandom , which draws from the OS's cryptographic entropy.
Passwords are different again — you should never be able to recover them. Hash them with a slow, salted algorithm built for passwords: bcrypt (cost 12+), scrypt, or Argon2. Spring Security's BCryptPasswordEncoder adds a fresh random salt per password and verifies by re-hashing — you never decrypt.
❌ Never: plain text passwords, MD5/SHA-256 for passwords, Math.random() for tokens, secrets hardcoded in source.
✅ Always: bcrypt (cost ≥ 12), SecureRandom tokens, secrets from env vars or a secret manager.
Java's built-in serialization is dangerous on untrusted data. Calling new ObjectInputStream(stream).readObject() rebuilds arbitrary objects and runs their readObject methods, so a crafted byte stream can trigger a "gadget chain" and achieve remote code execution — before you ever look at the result. This is OWASP A08 / CWE-502.
For data in transit , use TLS everywhere (HTTPS), keep your JDK and crypto libraries current so you're on modern TLS 1.2/1.3 cipher suites, and add the Strict-Transport-Security (HSTS) header so browsers refuse to downgrade to plain HTTP. Use vetted libraries for cryptography — never invent your own algorithm.
The OWASP Top 10 is the industry's standard list of the most critical web application risks. Here's how the risks in this lesson map to concrete Java defences:
Finish the allow-list regex and the match check so only an exact 6-digit PIN is accepted. Fill in the blanks, then run it.
Turn this login lookup into an injection-proof query: add ? placeholders and bind each value by position.
No blanks this time — just a brief and a comment outline. Build a secure, URL-safe password-reset token from scratch using SecureRandom .
You can now treat every external value as untrusted, validate it with an allow-list, kill SQL injection with PreparedStatement , avoid command injection with ProcessBuilder , generate unguessable tokens with SecureRandom , hash passwords with bcrypt, refuse to deserialize untrusted data, keep secrets out of source — and map each risk to the OWASP Top 10.
Next up: JavaFX — building desktop GUI applications.
Practice quiz
What is the strongest approach to validating untrusted input?
- An allow-list that describes exactly what is valid and rejects everything else
- A block-list that bans known bad characters
- Trusting input the client already validated
- Logging the input and continuing
Answer: An allow-list that describes exactly what is valid and rejects everything else. An allow-list (whitelist) is safer than a block-list because you cannot possibly list every malicious input an attacker might invent.
Why does a PreparedStatement stop SQL injection while string concatenation does not?
- It encrypts the SQL before sending it
- It sends the SQL and the data separately, so values are never parsed as SQL
- It escapes the query with HTML encoding
- It runs the query in a sandbox
Answer: It sends the SQL and the data separately, so values are never parsed as SQL. The ? placeholder keeps data and code separate: the driver sends the values as DATA, so input like ' OR '1'='1 is treated as a literal name.
To avoid command injection, you should run external programs with...
- Runtime.exec("sh -c ...") built by concatenating input
- A shell so metacharacters work
- ProcessBuilder with each argument as its own list element and no shell
- eval() on the command string
Answer: ProcessBuilder with each argument as its own list element and no shell. ProcessBuilder with a List of arguments and no shell means ';' '|' '&' are literal characters in a filename, never separate commands.
Which class should you use to generate session IDs and password-reset tokens?
- Math.random()
- java.util.Random
- java.security.SecureRandom
- System.currentTimeMillis()
Answer: java.security.SecureRandom. SecureRandom draws from the OS's cryptographic entropy and is unpredictable. Math.random() and java.util.Random are ordinary PRNGs an attacker can predict.
How should passwords be stored?
- In plain text so you can email them back
- Hashed with a slow, salted algorithm like bcrypt (cost 12+)
- Encrypted with a fast algorithm like MD5
- Hashed with SHA-256 and no salt
Answer: Hashed with a slow, salted algorithm like bcrypt (cost 12+). Use a deliberately slow, salted password hash (bcrypt, scrypt, or Argon2). MD5 and SHA-256 are too fast and let attackers try billions of guesses per second.
Why is calling readObject() on untrusted data dangerous?
- It is slow
- It only works on local files
- It rebuilds arbitrary objects and can trigger a gadget chain leading to remote code execution
- It always returns null
Answer: It rebuilds arbitrary objects and can trigger a gadget chain leading to remote code execution. ObjectInputStream.readObject() runs the objects' readObject methods, so a crafted byte stream can execute code (OWASP A08 / CWE-502). Use JSON instead.
What does the username allow-list regex "^[A-Za-z0-9_]{3,16}
quot; require?- Any string containing at least one letter
- 3 to 16 characters that are only letters, digits, or underscores, matching the whole string
- Exactly 16 characters of any kind
- A string that starts with a digit
Answer: 3 to 16 characters that are only letters, digits, or underscores, matching the whole string. The anchors ^ and $ force the WHOLE string to match: 3-16 chars from [A-Za-z0-9_]. Payloads like <script>...</script> are rejected.
Which OWASP Top 10 category covers SQL and command injection?
- A01 Broken Access Control
- A02 Cryptographic Failures
- A03 Injection
- A08 Software/Data Integrity Failures
Answer: A03 Injection. Injection (SQL, command, etc.) is OWASP A03, defended with PreparedStatement and ProcessBuilder.
How should you store a database password or API key?
- Hardcoded in source and committed to git
- In a comment so it is easy to find
- Read at runtime from environment variables or a secret manager
- In a public README
Answer: Read at runtime from environment variables or a secret manager. Read secrets from System.getenv or a secret manager (Vault, AWS Secrets Manager). Hardcoding leaks every secret when the repo leaks.
Is input validation alone enough to stop SQL injection and XSS?
- Yes, validation makes input completely safe
- No - pair it with PreparedStatement parameters for SQL and context-aware output encoding for HTML
- Yes, if the regex is strict enough
- No, you must encrypt all input instead
Answer: No - pair it with PreparedStatement parameters for SQL and context-aware output encoding for HTML. Validation is the first filter, not the last defence. The parameterised query and the output encoder actually keep data and code separate.