Skip to main content

    Lesson 42 • Advanced

    Secure Coding Practices

    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.

    What You'll Learn in This Lesson

    • Validate untrusted input with allow-lists
    • Stop SQL injection with PreparedStatement
    • Avoid command injection with ProcessBuilder
    • Generate secrets with SecureRandom, not Math.random()
    • Hash passwords with bcrypt and handle secrets safely
    • Map real bugs to the OWASP Top 10 and avoid unsafe deserialization

    Before You Start

    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.

    🏰 A Real-World Analogy

    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.

    1️⃣ Validate Untrusted Input First

    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.

    Worked Example — Allow-List Input Validation
    import java.util.regex.Pattern;
    
    public class Main {
        // Allow-list validation: say what IS allowed, reject everything else.
        // A username may only contain letters, digits and underscores, 3-16 chars.
        private static final Pattern USERNAME = Pattern.compile("^[A-Za-z0-9_]{3,16}$");
    
        static boolean isValidUsername(String input) {
            if (input == null) return false;            // null is never valid
            return USERNAME.matcher(input).matches();   // true only if the WHOLE string matches
        }
    
        public static void main(String[] args) {
            String[] tries = {
                "alice_99",                                   // good
                "ab",                                         // too short
                "robert'); DROP TABLE users;--",              // SQL injection attempt
                "<script>alert(1)</script>"                   // XSS attempt
            };
    
            for (String t : tries) {
                // Validate at the boundary, BEFORE the value reaches a database or page.
                System.out.println(isValidUsername(t) + "  <- " + t);
            }
        }
    }
    Output
    true  <- alice_99
    false  <- ab
    false  <- robert'); DROP TABLE users;--
    false  <- <script>alert(1)</script>
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    2️⃣ Stop SQL Injection with PreparedStatement

    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.

    Worked Example — SQL Injection: Vulnerable vs Fixed
    import java.sql.Connection;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    import java.sql.Statement;
    
    public class Main {
        // ❌ VULNERABLE: user input is concatenated straight into the SQL text.
        static String findUserBad(Connection conn, String name) throws Exception {
            String sql = "SELECT email FROM users WHERE name = '" + name + "'";
            // If name = "' OR '1'='1", the WHERE clause is always true ->
            //   SELECT email FROM users WHERE name = '' OR '1'='1'
            // The attacker dumps EVERY row. This is SQL injection (OWASP A03).
            try (Statement st = conn.createStatement();
                 ResultSet rs = st.executeQuery(sql)) {
                return rs.next() ? rs.getString("email") : null;
            }
        }
    
        // ✅ FIXED: the ? placeholder keeps data and code separate.
        static String findUserSafe(Connection conn, String name) throws Exception {
            String sql = "SELECT email FROM users WHERE name = ?";
            try (PreparedStatement ps = conn.prepareStatement(sql)) {
                ps.setString(1, name);   // the driver sends 'name' as DATA, never as SQL
                try (ResultSet rs = ps.executeQuery()) {
                    return rs.next() ? rs.getString("email") : null;
                }
            }
            // Now "' OR '1'='1" is looked up as a literal name and simply finds nothing.
        }
    }
    This is real JDBC. It needs a live Connection and ausers table, so it has no fixed console output. The lesson is structural: the findUserBad method is exploitable andfindUserSafe is not. Always use the? placeholder version.

    3️⃣ Avoid Command Injection

    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.

    Worked Example — Command Injection: Vulnerable vs Fixed
    import java.util.List;
    
    public class Main {
        public static void main(String[] args) throws Exception {
            String filename = "report.txt; rm -rf /";   // attacker-controlled input
    
            // ❌ VULNERABLE: building a shell command string.
            //    Runtime.getRuntime().exec("sh -c \"cat " + filename + "\"");
            //    The shell sees the ';' and runs "rm -rf /" as a SECOND command.
            System.out.println("Bad  -> sh -c \"cat " + filename + "\"  (shell runs rm -rf /!)");
    
            // ✅ FIXED: no shell, and each argument is a separate list element.
            //    ProcessBuilder never interprets ';' '|' '&' — filename is ONE argument.
            ProcessBuilder pb = new ProcessBuilder(List.of("cat", filename));
            System.out.println("Good -> ProcessBuilder runs: cat [" + filename + "]");
            System.out.println("        The ';' is part of the file name, not a command.");
            // pb.start();  // would just fail to find that oddly-named file - harmless.
        }
    }
    Output
    Bad  -> sh -c "cat report.txt; rm -rf /"  (shell runs rm -rf /!)
    Good -> ProcessBuilder runs: cat [report.txt; rm -rf /]
            The ';' is part of the file name, not a command.
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    4️⃣ Secrets: Randomness, Hashing & Storage

    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.

    Worked Example — SecureRandom Tokens
    import java.security.SecureRandom;
    import java.util.Base64;
    
    public class Main {
        // ❌ Math.random() is a predictable PRNG - fine for dice, NEVER for security.
        // ✅ SecureRandom is a cryptographically strong source for tokens, salts, keys.
        private static final SecureRandom RNG = new SecureRandom();
    
        static String newToken() {
            byte[] bytes = new byte[32];              // 32 bytes = 256 bits of entropy
            RNG.nextBytes(bytes);                     // fill with unpredictable bytes
            return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
        }
    
        public static void main(String[] args) {
            // Each token is unguessable - use for session IDs, password-reset links, API keys.
            System.out.println("Session token: " + newToken());
            System.out.println("Reset token:   " + newToken());
    
            // Math.random() returns a double in [0,1) - an attacker who sees a few
            // outputs can predict the rest. Don't build secrets on it:
            System.out.println("Math.random()  -> " + Math.random() + "  (predictable - avoid!)");
        }
    }
    Real Java using SecureRandom. The token values are random, so they differ on every run and can't be shown as fixed output — run it ononecompiler.com/java to watch two unguessable 256-bit tokens print. Use this generator (not Math.random()) for anything an attacker must not predict.

    5️⃣ Safe Deserialization & TLS Basics

    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.

    6️⃣ OWASP Top 10 → Java Defences

    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:

    OWASP RiskAttackJava Defence
    A01 Broken Access ControlActing beyond your roleServer-side authorization checks
    A02 Cryptographic FailuresWeak/plain-text secretsbcrypt, TLS, SecureRandom
    A03 InjectionSQL / command injectionPreparedStatement, ProcessBuilder
    A05 Security MisconfigurationDefaults, verbose errorsHarden config, generic error pages
    A07 Auth FailuresWeak passwords, guessable tokensSpring Security + SecureRandom
    A08 Software/Data IntegrityUntrusted deserializationAvoid Java serialization; use JSON

    🎯 Your Turn #1 — Validate a PIN

    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.

    Your Turn #1 — Allow-List Validation
    import java.util.regex.Pattern;
    
    public class Main {
        public static void main(String[] args) {
            // 🎯 YOUR TURN — fill in the blanks marked with ___
    
            // 1) Validate a 6-digit PIN. Build an allow-list regex:
            //    start ^, then a digit class, repeated exactly 6 times, then end $.
            Pattern pin = Pattern.compile(___);   // 👉 e.g. "^[0-9]{6}$"
    
            // 2) A safe check returns true only if the WHOLE string matches.
            String input = "1234";                // too short on purpose
            boolean ok = pin.matcher(input).___;  // 👉 replace ___ with matches()
    
            System.out.println("PIN valid? " + ok);
    
            // ✅ Expected output: PIN valid? false
            //    (Try input = "123456" and you should get: PIN valid? true)
        }
    }
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    🎯 Your Turn #2 — Parameterize the Query

    Turn this login lookup into an injection-proof query: add ? placeholders and bind each value by position.

    Your Turn #2 — PreparedStatement
    import java.sql.Connection;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    
    public class Main {
        // 🎯 YOUR TURN — fill in the blanks marked with ___
        // Turn this lookup into an injection-proof query.
    
        static boolean login(Connection conn, String user, String pwHash) throws Exception {
            // 1) Use a ? placeholder for EACH user-supplied value - never concatenation.
            String sql = "SELECT 1 FROM users WHERE name = ___ AND pw_hash = ___";  // 👉 use ? and ?
    
            try (PreparedStatement ps = conn.prepareStatement(sql)) {
                // 2) Bind the values by position (1-based). user goes to the first ?.
                ps.setString(1, ___);   // 👉 bind 'user'
                ps.setString(2, ___);   // 👉 bind 'pwHash'
    
                try (ResultSet rs = ps.executeQuery()) {
                    return rs.next();   // a row exists => credentials matched
                }
            }
            // ✅ Expected: input like "admin' --" is searched for literally and finds nothing.
        }
    }
    Structural exercise — it needs a live database to execute. Check your answer against the fixed method in section 2: each user value becomes a ? bound with setString.

    🧩 Mini-Challenge — Reset-Token Generator

    No blanks this time — just a brief and a comment outline. Build a secure, URL-safe password-reset token from scratch using SecureRandom.

    Mini-Challenge — Secure Token (faded)
    import java.security.SecureRandom;
    import java.util.Base64;
    
    public class Main {
        public static void main(String[] args) {
            // 🎯 MINI-CHALLENGE: a secure password-reset token generator
            // 1. Create ONE SecureRandom instance (reuse it - don't make a new one each call).
            // 2. Make a byte[] of length 24 and fill it with rng.nextBytes(...).
            // 3. Encode it with Base64.getUrlEncoder().withoutPadding() so it's URL-safe.
            // 4. Print the token, then print its length.
            //
            // ✅ Expected: a random ~32-char string that changes every run,
            //    e.g. "Token: 8Kf2..."  then  "Length: 32"
            //    (24 bytes Base64-encode to 32 characters.)
    
            // your code here
        }
    }
    Write it, then run it on onecompiler.com/java. You should see a ~32-character random token that changes every run, plus Length: 32.

    Common Errors (and the Fix)

    • Building SQL by string concatenation: "... WHERE name='" + name + "'" is injectable. Fix: use a PreparedStatement with ? and setString(1, name) — never glue input into the query.
    • Using Math.random() for tokens: the output is predictable, so session IDs and reset links can be guessed. Fix: use SecureRandom.nextBytes(...) and Base64-encode the bytes.
    • Storing plaintext (or MD5/SHA) passwords: one database leak exposes every account. Fix: hash with BCryptPasswordEncoder(12) and verify with matches(...) — you never store or decrypt the original.
    • Deserializing untrusted data: new ObjectInputStream(stream).readObject() on a request body can run attacker code (CWE-502). Fix: parse JSON with Jackson into known types instead of Java serialization.
    • Hardcoding secrets: String pw = "admin123"; ends up committed to git. Fix: read from System.getenv("DB_PASSWORD") or a secret manager.

    Five Rules You Never Break

    • ⚠️ Never concatenate user input into SQL or shell commands — parameterise instead.
    • ⚠️ Never store passwords in plain text or fast hashes — use bcrypt (cost ≥ 12).
    • ⚠️ Never trust client input — validate on the server, even if the client already did.
    • ⚠️ Never log secrets — passwords, tokens, card numbers, session IDs.
    • ⚠️ Never deserialize untrusted data with ObjectInputStream.

    📋 Quick Reference

    ThreatDefenceTool / API
    Untrusted inputPattern.matches(allowList)java.util.regex
    SQL injectionps.setString(1, v)PreparedStatement
    Command injectionnew ProcessBuilder(List.of(...))ProcessBuilder (no shell)
    Weak randomnessrng.nextBytes(b)SecureRandom
    Password storageencoder.encode(pw)BCryptPasswordEncoder(12)
    Unsafe deserializationobjectMapper.readValue(...)Jackson JSON (not readObject)
    Secrets / CVEsSystem.getenv(...) / dep scanVault, OWASP Dependency-Check

    ❓ Frequently Asked Questions

    Is input validation enough to stop SQL injection and XSS?

    No — validation is your first filter, not your last defence. Always pair it with the right output-time control: PreparedStatement parameters for SQL, and context-aware output encoding (OWASP Java Encoder) for HTML. Validation reduces the attack surface, but it is the parameterised query and the encoder that actually make the input safe, because they keep data and code separate no matter what slipped through.

    Why is PreparedStatement safe when string concatenation is not?

    A concatenated query sends the SQL and the user input as one finished string, so input like ' OR '1'='1 changes what the query means. A PreparedStatement sends the SQL with ? placeholders first, then the values separately, so the database always treats those values as data and never parses them as SQL. Use a placeholder for every user-supplied value and never build query text by gluing strings together.

    Can I hash passwords with SHA-256 or MD5 if I add a salt?

    No. MD5 and SHA-256 are designed to be fast, which means an attacker with your hashes can try billions of guesses per second even with a salt. Use a deliberately slow, salted algorithm built for passwords: bcrypt (cost 12 or higher), scrypt, or Argon2. Spring Security's BCryptPasswordEncoder generates a fresh random salt per password and embeds the cost and salt in the hash string, so you store and compare one value.

    When must I use SecureRandom instead of Math.random()?

    Use SecureRandom for anything an attacker must not be able to predict: session IDs, password-reset tokens, API keys, salts, CSRF tokens, and cryptographic keys. Math.random() and java.util.Random are ordinary PRNGs — observing a few outputs lets an attacker predict the rest, which is fine for a game but catastrophic for a token. Math.random() is only acceptable for non-security randomness like shuffling a deck or picking a sample.

    Why is Java deserialization of untrusted data dangerous?

    ObjectInputStream.readObject() rebuilds arbitrary objects and runs their readObject/readResolve methods, so a crafted byte stream can trigger 'gadget chains' that execute code — remote code execution — before you ever inspect the result (OWASP A08, CWE-502). Never call readObject() on data from a network, file upload, cookie, or any source you do not fully control. Use a data format like JSON (Jackson) that only produces the plain types you ask for.

    How should I store secrets like database passwords and API keys?

    Never hardcode them in source or commit them to git — a leaked repo leaks every secret. Read them at runtime from environment variables (System.getenv) or, better, a secret manager such as HashiCorp Vault, AWS Secrets Manager, or your platform's secret store. Keep secrets out of logs and error messages, scope each one to least privilege, and rotate them if they are ever exposed.

    🎉 Lesson Complete!

    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.

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service