PreparedStatement & SQL Injection
PreparedStatement & SQL Injection
In the previous lesson you sent SQL strings built by simple concatenation. That approach works — until someone supplies malicious input. This lesson covers PreparedStatement, the standard JDBC mechanism for parameterized queries, and explains exactly why parameterization eliminates SQL injection rather than merely reducing it.
What SQL Injection Actually Is
SQL injection happens when user-supplied data is interpreted as SQL syntax rather than as a value. Consider a login query built with string concatenation:
If a user supplies the username ' OR '1'='1 and any password, the resulting query becomes:
The condition '1'='1' is always true, so the query returns every user row — a complete authentication bypass. More destructive payloads can drop tables, exfiltrate data, or write files to disk depending on database permissions.
PreparedStatement: Pre-compilation and Parameters
A PreparedStatement sends the SQL template to the database server first, before any values are known. The server parses and compiles the query plan at that point. Values are then sent separately as typed parameters — they can never alter the query's structure because the structure is already locked.
The placeholder character is a question mark ?. Parameters are bound by position (starting at 1) using typed setter methods:
If the attacker again passes ' OR '1'='1 as the username, the database treats that entire string as a literal value to compare against the username column — it is never parsed as SQL syntax. No row matches, so no bypass occurs.
Setter Methods and Type Safety
PreparedStatement provides setters for every SQL type. Using the correct setter lets the driver perform proper type conversion and ensures column comparisons use the right type semantics:
Common setters: setString, setInt, setLong, setDouble, setBigDecimal, setBoolean, setDate, setTimestamp, setNull. For nullable columns, always use setNull(index, Types.VARCHAR) rather than passing a Java null to setString — the behaviour of passing null is driver-dependent.
Reusing a PreparedStatement
One of the performance benefits of prepared statements is that the same compiled plan can be reused for many executions with different parameters. This is especially useful for bulk operations:
prepareStatement has an up-front cost (a round-trip to compile the plan). If you are inserting or updating many rows in a loop, prepare the statement outside the loop and bind new parameters inside it. For very large batches, combine this with batch execution (covered in a later lesson).
Retrieving Auto-Generated Keys
When you insert a row into a table with an auto-increment primary key, you often need the generated ID immediately. Pass the flag Statement.RETURN_GENERATED_KEYS to prepareStatement, then retrieve it via getGeneratedKeys():
What PreparedStatement Does Not Protect Against
Parameterization protects values — the data that fills placeholders. It does not protect dynamic structural elements such as table names, column names, or ORDER BY directions, because those cannot be sent as parameters. If you must make these dynamic, use an allowlist:
Summary
SQL injection exploits the failure to separate code from data. PreparedStatement enforces that separation at the protocol level: the SQL template is compiled before any user data is sent, making it structurally impossible for a parameter value to alter the query's intent. Use the correct typed setter for each column, prepare outside loops, retrieve generated keys when needed, and guard dynamic structural fragments with allowlists.