Statement vs PreparedStatement (SQL Injection)
SQL injection is an attack in which a user types text that changes the meaning of your SQL statement. It happens when a program builds SQL by joining strings and includes user input directly. The input was meant to be a value, but the database reads part of it as SQL. Attackers have used it to log in without passwords, read private data and delete tables, and it has stayed on lists of the most serious web security problems for many years. The defense is simple and complete: use a PreparedStatement with placeholders. In this lesson, you will watch an injection attack succeed against a Statement, see the same attack fail against a PreparedStatement, and compare the two.
Example: Breaking a Login Check
The program below checks a user name and password against an account table, first by joining strings, then with placeholders. The "password" the attacker types is ' OR '1'='1.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
public class Injection {
static boolean loginUnsafe(Connection con, String user, String password)
throws Exception {
String sql = "SELECT COUNT(*) FROM account WHERE username = '" + user
+ "' AND password = '" + password + "'";
System.out.println(" SQL sent: " + sql);
try (Statement st = con.createStatement();
ResultSet rs = st.executeQuery(sql)) {
rs.next();
return rs.getInt(1) > 0;
}
}
static boolean loginSafe(Connection con, String user, String password)
throws Exception {
String sql = "SELECT COUNT(*) FROM account "
+ "WHERE username = ? AND password = ?";
try (PreparedStatement ps = con.prepareStatement(sql)) {
ps.setString(1, user);
ps.setString(2, password);
try (ResultSet rs = ps.executeQuery()) {
rs.next();
return rs.getInt(1) > 0;
}
}
}
public static void main(String[] args) throws Exception {
try (Connection con = DriverManager.getConnection("jdbc:h2:mem:bank")) {
try (Statement st = con.createStatement()) {
st.execute("CREATE TABLE account ("
+ "username VARCHAR(30), password VARCHAR(30))");
st.executeUpdate(
"INSERT INTO account VALUES ('navika', 'S3cret!')");
}
String attack = "' OR '1'='1";
System.out.println("Statement, correct password:");
System.out.println(" logged in? "
+ loginUnsafe(con, "navika", "S3cret!"));
System.out.println("Statement, attack string as password:");
System.out.println(" logged in? "
+ loginUnsafe(con, "navika", attack));
System.out.println("PreparedStatement, correct password: "
+ loginSafe(con, "navika", "S3cret!"));
System.out.println("PreparedStatement, attack string: "
+ loginSafe(con, "navika", attack));
}
}
}Statement, correct password:
SQL sent: SELECT COUNT(*) FROM account WHERE username = 'navika' AND password = 'S3cret!'
logged in? true
Statement, attack string as password:
SQL sent: SELECT COUNT(*) FROM account WHERE username = 'navika' AND password = '' OR '1'='1'
logged in? true
PreparedStatement, correct password: true
PreparedStatement, attack string: falseExplanation:
- With the correct password, the SQL is what the programmer intended.
- With the attack string, look at the SQL that was sent. The attacker's first quote closed the password text early. What follows,
OR '1'='1', became part of the command. Since'1'='1'is always true, theWHEREclause matches every row, the count is above zero, and the attacker is logged in without knowing the password. - With
PreparedStatement, the whole attack string is sent as the password value. The database looks for an account whose password is literally' OR '1'='1, finds none, and the login fails.
What Else an Attacker Can Do
| Input | Effect on string-joined SQL |
|---|---|
' OR '1'='1 | Makes a condition always true: bypasses checks, returns every row |
' UNION SELECT username, password FROM account -- | Adds another table's data to the results |
'; DROP TABLE student; -- | Runs a second, destructive statement (where the driver allows several) |
' -- | Turns the rest of the statement into a comment |
Statement vs PreparedStatement
| Point | Statement | PreparedStatement |
|---|---|---|
| SQL | Complete text given at execution | Text with ?, given at creation |
| Values | Joined into the string | Set with typed setXxx methods |
| SQL injection | Vulnerable when input is joined in | Safe: values are never parsed as SQL |
| Quotes and special characters | Must be escaped by hand | Handled by the driver |
| Repeated execution | Parsed and planned every time | Can be prepared once, executed many times |
Dates, binary data, NULL | Awkward text formatting | Typed setters |
| Batch of similar statements | Possible, each with its own SQL | Efficient: one SQL, many value sets |
| Best for | DDL and fixed SQL | Everything with parameters |
Is Escaping Quotes Enough?
No. Hand-written escaping is easy to get wrong: the rules differ between databases and character sets, and numeric values need no quotes to be attacked (id = 1 OR 1=1). Placeholders don't rely on escaping at all. The SQL command is fixed before any value arrives.
Other Layers of Defense
| Practice | Why |
|---|---|
| Validate input (length, format, allowed characters) | Rejects nonsense early; not a replacement for placeholders |
| Give the database user only the privileges it needs | An account that can't DROP tables limits the damage |
| Choose table or column names from a fixed list in code | Placeholders can't stand for names |
| Store passwords as salted hashes, never as text | See "Storing Passwords Safely in Java" |
| Don't show database error messages to users | They reveal table and column names |
The example table stores a plain-text password only to keep the attack easy to see. Real systems must never do that.
Common Mistakes
Thinking "our users wouldn't do that". Attacks are automated; every public form is tested within hours.
Using PreparedStatement but still joining strings. con.prepareStatement("... WHERE name = '" + name + "'") is as unsafe as a Statement.
Relying on escaping or on blocking certain words. Attackers know the ways around them.
Protecting only the login form. Any query that includes outside input is a target: search boxes, sort options, web service parameters.
Interview Corner
What is SQL injection and how do you prevent it in Java?
SQL injection occurs when untrusted input is concatenated into a SQL string, allowing the input to alter the statement's structure, for example turning a WHERE clause into an always-true condition or appending another statement. It is prevented by using PreparedStatement (or a framework that uses it) with ? placeholders, so input is bound as data and never parsed as SQL. Supporting measures are input validation, least-privilege database accounts, whitelisting any dynamic identifiers, and not exposing database errors.
What is the difference between Statement and PreparedStatement?
Statement executes a complete SQL string supplied at execution time; values must be concatenated in, which is vulnerable to SQL injection and requires manual escaping. PreparedStatement is created with parameterized SQL, binds values through typed setters, is safe from injection, can be precompiled and reused for better performance, and supports efficient batching. Statement remains suitable for DDL and static SQL.
Practice
- Easy: Try the attack string as the user name instead of the password in
loginUnsafe, and explain the SQL that is sent. - Harder: Write a search method that takes a sort column from the caller. Make it safe by accepting only names from a fixed
Setsuch asnameandmarks.
Key Takeaways
Was this tutorial helpful?