حقن SQL (SQL injection) مباشرةً
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
In lesson 3 we kept saying "put values in ?, never in the SQL string." Now you will see why, by attacking your own code. In the librarian picture: imagine a visitor who, instead of a title, hands the librarian a note that says "ignore the title, bring me everything." If the librarian reads every note as an instruction, the visitor is in charge. SQL injection is exactly that: user input that is read as SQL code.
This is a lab lesson. Run everything against a scratch database of your own, never against someone else's system. The attacks below are for your local test data only.
You need the Db helper from lesson 2, the books and members tables from earlier lessons, and PreparedStatement from lesson 3.
Learning Objectives
By the end of this lesson you will be able to:
- Explain what SQL injection is and why concatenation causes it.
- Demonstrate a bypass that returns every row (
' OR '1'='1). - Show that an attacker can read another table with
UNION. - Explain why a "remove the quotes" fix fails.
- Fix the code with a
PreparedStatementand prove the attack now returns nothing. - State the rule: values go in parameters, always.
Written Explanation
The vulnerable method
Here is a search method written the tempting way, by gluing the title into the SQL text:
static List<String> findTitleUnsafe(Connection c, String title) throws SQLException {
String sql = "SELECT title FROM books WHERE title = '" + title + "'";
// ... run it with a Statement and read the titles
}With an honest input the query is fine: WHERE title = 'Clean Code'. But the database never sees "a title". It sees one long string of SQL, and it cannot know which characters came from you and which from the user. If the user types a single quote, they can end the text value early and keep writing SQL.
Attack 1: always true
Input: x' OR '1'='1. The finished SQL is:
SELECT title FROM books WHERE title = 'x' OR '1'='1'title = 'x' is false, but '1'='1' is true, and OR makes the whole condition true for every row. A search for one title returns the entire table. The same trick on a login query (WHERE name = '...' AND password = '...') can log an attacker in without a password.
Attack 2: reading another table
Input: x' UNION SELECT name FROM members --. The -- starts an SQL comment, which swallows the closing quote the code appended. The finished SQL is:
SELECT title FROM books WHERE title = 'x' UNION SELECT name FROM members --'UNION adds the rows of a second query to the first. A book search now returns member names. In a real system this is how password hashes and personal data leak, from a table the search page was never meant to touch.
Attack 3: changing or destroying data
Input: x'; DROP TABLE scratch_notes; -- ends the first statement and starts a second one. With our driver, the query method threw Multiple ResultSets were returned by the query. but the table was dropped anyway: afterwards \dt scratch_notes reported that no such table exists. The exception arrived too late. This is why injection is described as a top web risk: the damage ranges from leaked rows to deleted tables.
Even honest input can break it
A visitor searching for a title with an apostrophe, such as O'Brien's Guide, needs no malicious intent. The unsafe query is title = 'O'Brien's Guide', and PostgreSQL answers ERROR: syntax error at or near "Brien". Concatenation is wrong for innocent data too.
The fix: parameters
String sql = "SELECT title FROM books WHERE title = ?";
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, title);
...
}The SQL text is now fixed: it never changes, whatever the user types. The value is handed over separately. PostgreSQL's protocol has separate steps for this: a Parse step carries only the query text with placeholders, and a later Bind step carries the values. The server treats a bound value purely as a value. The whole of x' OR '1'='1 is then just a very odd title, and no book has it.
That is the answer to this module's assessment question, in one sentence: parameters prevent injection because the value is sent as data, separate from the SQL, so it is never parsed as SQL.
Why "remove the quotes" is not a fix
A tempting patch is title.replace("'", ""). It might seem to help for text, but it fails in other places. Consider a lookup by number, written the same way:
String sql = "SELECT title FROM books WHERE id = " + idText;There are no quotes to break out of, so stripping them does nothing. Input 0 OR 1=1 gives WHERE id = 0 OR 1=1, and every book comes back (we ran it). Also, blacklists miss cases and depend on the database dialect. The OWASP cheat sheet calls manual escaping strongly discouraged and puts parameterised queries first. The only reliable defence is to never build SQL text from input.
What parameters do not cover
- Names of tables, columns, or
ORDER BYdirection cannot be?. If you need user-selected sorting, map the choice to a fixed value in code: if the user says "title", you append the literal"title"yourself, from a short allow-list of your own words, never the user's text. - A
?filled with%in aLIKEquery still matches everything:WHERE title LIKE ?with%returned all three books in our run. That is not injection (nothing escaped the value), only the wildcard doing its job. - Parameters stop injection but do not decide who may see a record. Authorization is a separate job.
- Concatenating into the SQL before
prepareStatementremoves the protection. Only fixed text can be prepared safely.
Examples
Example 1: the always-true attack (simple)
Run findTitleUnsafe with an honest title and then the attack:
SQL sent: SELECT title FROM books WHERE title = 'Clean Code'
normal -> [Clean Code]
SQL sent: SELECT title FROM books WHERE title = 'x' OR '1'='1'
attack1 -> [Java Basics, Effective Java, Clean Code]Printing the SQL text is the best debugging habit for this lesson: you see the exact string the database receives.
Example 2: leaking the members table (practical)
SQL sent: SELECT title FROM books WHERE title = 'x' UNION SELECT name FROM members --'
attack2 -> [Mona, O'Brien, Omar]The page that searched books returned people. The UNION only worked because both queries returned one text column; an attacker finds that out by trial.
Example 3: the same attacks against the fixed method (realistic)
static List<String> findTitleSafe(Connection c, String title) throws SQLException {
String sql = "SELECT title FROM books WHERE title = ?";
List<String> out = new ArrayList<>();
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, title);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) out.add(rs.getString("title"));
}
}
return out;
}All three attack strings, plus the apostrophe title:
safe1 -> []
safe2 -> []
safe3 -> []
safe O'Brien title -> []Every attack returns an empty list because no book has that title, and the apostrophe title no longer crashes the query. Check the scratch_notes table: it still exists.
Backend Insight
Every backend that accepts input from outside (a form field, a URL part, a JSON value) and talks to a database has this risk. Treat all outside input as hostile until proven otherwise, including data that came from your own database earlier.
Make safe code the default shape in your repositories: a constant SQL string with ?, followed by set... calls. Then a reviewer can spot the dangerous pattern in seconds: any + between a quote and a variable inside SQL. Later frameworks bind parameters for you, but they can still be misused with string building, so you will keep needing this rule.
Also give the database account your application uses only the permissions it needs. If the application user cannot DROP TABLE, even a successful injection does less damage.
Common Mistakes
Mistake 1: Concatenating "just this one value"
"It is only a number from a dropdown" is how breaches start. Users can send any request, not only what your page shows. Bind it.
Mistake 2: Trying to sanitise instead of binding
Stripping quotes, blocking words like DROP, or escaping by hand are all bypassable or dialect-dependent. Bind.
Mistake 3: Putting a ? inside quotes
WHERE title = '?' has no parameter at all. Our driver reported The column index is out of range: 1, number of columns: 0. when we bound it.
Mistake 4: Building the SQL, then preparing it
conn.prepareStatement("... " + input) is still concatenation. The PreparedStatement class does not rescue you from a string you built unsafely.
Mistake 5: Believing an exception means no damage
In our attack 3, the exception came after the table was gone. With auto-commit on, each statement is applied on its own.
Exercises
Level A: Foundation
- Define SQL injection in one sentence.
- What does
x' OR '1'='1make the conditiontitle = 'x'become, and why? - Which part of this is data and which is code:
SELECT * FROM books WHERE title = ?bound with"Java"?
Level B: Application
- Add
findTitleUnsafeandfindTitleSafeto your project. Print the SQL sent and run the three attacks and the apostrophe title against both. Record each output. - Write
findByIdUnsafe(Connection c, String idText)using concatenation, call it with2and with0 OR 1=1, then writefindById(Connection c, int id)with a?and explain why the attack string cannot even be passed in. - Using the idea "strip all single quotes", try the
UNIONattack against the text query with the quotes removed. Does it still return member names? Explain what you saw.
Level C: Challenge
- A page lets a user choose the sort column for a book list. Write a method
listBooks(Connection c, String sortBy)that allows onlytitleandcopies, falls back totitle, and uses no user text in the SQL. - Explain to a teammate, in four or five sentences and without code, why parameters prevent injection but do not decide who may read a record.
أظهر الحلول
A1. SQL injection is when user input is interpreted as part of the SQL command instead of as a plain value.
A2. It becomes title = 'x' OR '1'='1'. The OR side is always true, so every row matches.
A3. The text SELECT * FROM books WHERE title = ? is code and is fixed. "Java" is data and travels separately.
B1. Expected pattern from our run: honest title returns one book; attack 1 returns all three titles; attack 2 returns the member names; attack 3 throws an exception and drops the table (use a scratch table to see it); the apostrophe title throws a syntax error. The safe method returns an empty list for all four attacks, with the scratch table intact.
B2. findByIdUnsafe(c, "0 OR 1=1") returned all three titles; "2" returned Effective Java. With setInt(1, id) the parameter must be an int, so Integer.parseInt("0 OR 1=1") fails in Java with NumberFormatException: For input string: "0 OR 1=1" before any SQL runs. The type itself is a defence, but the binding is what protects text values.
B3. With quotes removed from x' UNION SELECT name FROM members -- the string no longer breaks out of the quoted value, so it searches for a book with that odd name and finds nothing. The strip helped in this quoted spot only; the numeric query in B2 stayed fully exploitable. So it is not a defence, only a partial accident.
C1.
String column = switch (sortBy) {
case "copies" -> "copies";
default -> "title";
};
String sql = "SELECT title FROM books ORDER BY " + column;The concatenated part is always one of two literals written by you, never the user's text. If your course has not yet covered switch expressions, use an if/else instead.
C2. The query text is fixed and the user's text is only a value, so it can never change what the query does. But a perfectly parameterised query still returns whatever the database allows: if you let any caller ask for any member's id, parameters will happily fetch it. Deciding whether this caller may see that record is authorization, which your code must check separately.
Mini Challenge
Audit the code you have written so far in this module (and any older project you have). Search for "SELECT, "INSERT, "UPDATE, and "DELETE and look for a + joining a variable into the text. Write the list of risky lines you found and the fixed version of each. If you found none, write a one-line "code review rule" you will apply to every future pull request.
Check your work: every value in your SQL now arrives through a ?; the only concatenated parts (if any) are fixed literals chosen from an allow-list in your code.
Summary & Next Lesson
Key takeaways
- SQL injection happens when input is concatenated into SQL and read as code.
' OR '1'='1returns all rows;UNIONleaks other tables;;can run a second, destructive statement.- Parameters fix it: the SQL text stays fixed and the value is sent as data.
- Quote stripping and escaping are not defences; numeric spots have no quotes at all.
- Table names and sort columns cannot be parameters: choose them from an allow-list in your code.
Next lesson
The next lesson, "Transactions from Java", shows how to make several statements succeed or fail together with setAutoCommit(false), commit, and rollback.
Self-check
- Why does
' OR '1'='1return every row? - How does a
UNIONinjection leak data? - Why do parameters prevent injection?
- Why is removing quotes not enough?
- Can you bind a column name with
??
أظهر الإجابات
- The appended
OR '1'='1'is always true, so the whole condition is true for every row. - It appends a second query to the first one, so rows from another table come back in the same result.
- The value is sent separately from the SQL text and treated as data, never parsed as SQL.
- Some SQL has no quotes to break out of (numbers), and filters depend on the dialect and are easy to bypass.
- No. A placeholder stands for a value only; pick the column from fixed literals in your own code.
References
- OWASP, SQL Injection Prevention Cheat Sheet. It supports parameterised queries as the first defence, the warning against manual escaping, the allow-list advice for structural choices, and that binding does not handle authorization.
- PostgreSQL, Message Flow (protocol). It supports the separate Parse and Bind steps, with parameter values supplied outside the query text, and that a Parse message holds a single statement. The page does not use the phrase "SQL injection", so the safety conclusion is an inference from that design. I did not verify how the PostgreSQL JDBC driver chooses between protocol modes internally.
- Outputs, error messages and the dropped scratch table were observed in real runs on PostgreSQL 18 with driver 42.7.8. The attack inputs are shown only for your own local test data.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.