The labs on this site need JavaScript to run. The same content is written out below.
Fundamentals › Security
What happens when text typed by a user is read as code?
SQL is a command sent to the database as text. To find a member by the name a user typed into an input box, that name has to go somewhere in the SQL, and Java code has two ways to do it. One is to attach the input to the SQL text ("… name = '" + input + "'"). The other is to leave a ? in the SQL where the value goes and pass the input separately through a PreparedStatement (a tool that takes values separately). When you attach it, characters in the input such as a quote also become part of the SQL text, so the database reads them as SQL syntax. Then the input is no longer a value but a command. This is SQL injection. When you pass it with ?, even a quote or OR in the input is only part of the value to look for. In this lab, you put into each of the two ways an ordinary name, a text with a quote and OR, and an ordinary name with a quote, and see the SQL the database received and the rows that came back. (The code on the screen is shown as if it were inside the main method, and the import lines and the main declaration are left out. There is no input box; the text is written in the input variable. Web attacks in general and JdbcTemplate are covered in the in-depth labs.)
Checked against real execution · Compiled six combinations on JDK 21 and H2 2.4.240 and ran them in new Java processes and checked them; the rules were checked against OWASP and the Java SE 21 API documentation
// The member table has three rows (name: kim, lee, O'Neil)
// Connection: connect to the database that the address points to
Connection conn = DriverManager.getConnection("jdbc:h2:mem:shop");
// Input: let us say this is the text a user typed into an input box
String input = "kim";
// SQL: leave the place for the value as ?
String sql = "SELECT name FROM member WHERE name = ?";
// Send: pass the SQL text and the value separately
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, input);
ResultSet rs = ps.executeQuery();
// Read: print the name of each returned row
while (rs.next()) {
System.out.println(rs.getString("name"));
}
We ran and checked this on JDK 21.0.12.1 and H2 2.4.240 (in-memory database), and we did not check by running on MySQL. For all six combinations, we put the code on the screen into Main.java, which has a main method (we added only import java.sql.* and the main declaration), compiled it with javax.tools, and ran each combination in a new Java process. Before the run, in the same process, we created the member table (id INT primary key, name VARCHAR(20)) and the three rows (1, kim), (2, lee), (3, O'Neil); the screen leaves this preparation code out. We did not make an input box and wrote the text in the input variable. What we checked against is the three items of the result table. The SQL the database received is the SELECT left in H2's INFORMATION_SCHEMA.QUERY_STATISTICS (the execution record), and for the combination that could not run because of a syntax error, we judged that the error came from the parser of the database engine and that the error message quoted that SQL (the execution record was empty). The returned rows are the names the code printed, and the way the input was handled was obtained from the actual values. If the text of input is in the SQL the database received, it is "part of the SQL text", and if it is not, it is "handled only as a value"; what is in the parentheses was decided by whether an error occurred and whether more rows than the rows whose name equals input came back. In the first item of the result table, the value of the combination passed with ? is the text passed with setString in the code. That this value is in ps separately from the SQL text was checked from the form in which H2 shows ps as a string (the SQL text followed by {1: value}). In this form, the single quote inside the value is written twice because that is how H2 shows a value as an SQL text value, and it does not mean the value was put into the SQL text. As facts that the step sentences rely on, we also checked by running that this SELECT is not in the execution record of the database at the point after prepareStatement and setString and at the point after createStatement, and that ps has 1 ? place while the attached SQL has 0 ? places (the execution record shows no new record once it has been read, so we created a separate database for each point and read it only once). The error of the combination that attached O'Neil is H2's SQLState 42000 (syntax error), the error position in the message was right after Neil, and the place where the error occurred was H2's reading of a string (Tokenizer.readCharacterString). The explanation on the screen ("the second quote ends the string, and the last quote opens a string that is not closed") is an interpretation fitted to this observation and to the H2 documentation that a quote inside a string must be written twice, and we did not trace directly how H2 splits the text. This error code is H2's, so it may differ on MySQL. How the SQL text is left in a real database log, and the order of the result rows (an order the database decides because there is no ORDER BY), are only what we saw in H2. We opened the documents directly. We followed the OWASP SQL Injection Prevention Cheat Sheet on these points: attaching strings and building queries from user input is dangerous (What Is a SQL Injection Attack?); with prepared statements the SQL is decided first and the values are passed later, and the database tells code and data apart whatever the input is (Defense Option 1); and the Java example uses ? in the SELECT and passes the value with setString, and an input such as tom' or '1'='1 only looks for a name equal to that whole text (Safe Java Prepared Statement Example). In PreparedStatement of the Java SE 21 API documentation, we checked the explanation that it is a precompiled SQL statement, that the setter methods set the value of a ? place (an IN parameter), and that the place numbers start from 1; and in Statement, that it is an object used for executing a static SQL statement and that executeQuery(String) returns one ResultSet. The PreparedStatement documentation does not say how setString handles quotes, so the fact that the value is handled regardless of quotes is not something the documentation decides but a behavior we observed on this H2. On the grammar page of the H2 documentation we checked that a string value starts and ends with a single quote and that a single quote inside it is written twice, and in String Literals of the MySQL 8.4 documentation we checked the same rule (MySQL also allows putting \ before the quote). We read all the opened documents through the summaries that the tool returned, and we did not read the whole original text. To keep the code on the screen short, it does not have the lines that close the connection and the ResultSet or the code that handles exceptions, and that the program stops at the line where the exception occurred and the lines below do not run was checked from the output being empty. This lab covers only SQL injection, and it does not cover input validation, escaping on output, permissions of the database account, other techniques used in real attacks, or cases where input is read as code in places other than SQL (HTML, commands, and so on). Web attacks in general (including XSS and CSRF) are covered in "Three web attacks", and JdbcTemplate, which reduces the repeated code of JDBC where you write SQL directly, in "JdbcTemplate and exception translation".
Struck-through text was removed from the first version of the code; highlighted text was added.