이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
기초개념 › 보안
사용자가 입력한 글이 코드로 읽히면 무슨 일이 생기는가
SQL은 DB에 글자로 보내는 명령입니다. 사용자가 입력창에 쓴 이름으로 회원을 찾으려면 그 이름이 SQL 어딘가에 들어가야 하는데, 자바 코드에는 방법이 두 가지 있습니다. 하나는 SQL 글자에 입력을 이어 붙이는 것("… name = '" + input + "'")이고, 하나는 SQL에 ?로 값 자리를 비워 두고 입력은 PreparedStatement(값을 따로 받는 도구)로 따로 넘기는 것입니다. 이어 붙이면 입력 안에 든 따옴표 같은 글자도 SQL 글자의 일부가 되어서, DB가 그것을 SQL 문법으로 읽습니다. 그러면 입력이 값이 아니라 명령이 됩니다. 이것이 SQL 인젝션입니다. ?로 넘기면 입력 안의 따옴표나 OR도 찾을 값의 일부일 뿐입니다. 이 실습은 두 방법에 평범한 이름, 따옴표와 OR가 든 글, 따옴표가 든 평범한 이름을 하나씩 넣어, DB가 받은 SQL과 돌아온 행을 봅니다. (화면의 코드는 main 메서드 안에 있다고 보고 import 줄과 main 선언은 줄였습니다. 입력창은 만들지 않고 input 변수에 글을 적어 두었습니다. 웹 공격 전반과 JdbcTemplate은 세부 동작 실습에서 다룹니다.)
실행으로 대조 · JDK 21과 H2 2.4.240에서 여섯 가지 조합을 컴파일해 새 자바 프로세스로 실행해 대조, 규칙은 OWASP와 Java SE 21 API 문서와 대조
// member 테이블에는 세 행(name: kim, lee, O'Neil)이 들어 있다
// 연결: 주소가 가리키는 DB와 연결을 맺는다
Connection conn = DriverManager.getConnection("jdbc:h2:mem:shop");
// 입력: 사용자가 입력창에 쓴 글이라고 하자
String input = "kim";
// SQL: 값이 들어갈 자리를 ?로 비워 둔다
String sql = "SELECT name FROM member WHERE name = ?";
// 보내기: SQL 글자와 값을 따로 넘긴다
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, input);
ResultSet rs = ps.executeQuery();
// 읽기: 돌아온 행마다 name을 출력한다
while (rs.next()) {
System.out.println(rs.getString("name"));
}
JDK 21.0.12.1과 H2 2.4.240(메모리 DB)에서 실행해 대조했고, MySQL에서의 실행 대조는 하지 않았습니다. 여섯 가지 조합을 모두, 화면의 코드를 main 메서드가 든 Main.java에 넣어(import java.sql.*와 main 선언만 더했습니다) javax.tools로 컴파일하고, 조합마다 새 자바 프로세스에서 실행했습니다. 실행 전에 같은 프로세스에서 member 테이블(id INT 기본 키, name VARCHAR(20))과 세 행 (1, kim), (2, lee), (3, O'Neil)을 만들어 두었는데, 화면에서는 이 준비 코드를 줄였습니다. 입력창은 만들지 않고 input 변수에 글을 적어 두었습니다. 대조한 것은 결과 표의 세 항목입니다. DB가 받은 SQL은 H2의 INFORMATION_SCHEMA.QUERY_STATISTICS(실행 기록)에 남은 SELECT이고, 문법 오류로 실행되지 못한 조합은 오류가 DB 엔진의 파서에서 났고 오류 메시지가 그 SQL을 인용한 것으로 판정했습니다(실행 기록은 비어 있었습니다). 돌아온 행은 코드가 출력한 name이고, 입력이 다뤄진 방식은 실제 값에서 얻었습니다. DB가 받은 SQL에 input의 글이 들어 있으면 "SQL 글자의 일부", 들어 있지 않으면 "값으로만 다뤄짐"이고, 괄호 안은 오류가 났는지, 이름이 input과 같은 행보다 많은 행이 돌아왔는지로 정했습니다. 결과 표의 첫 항목에서 ?로 넘긴 조합의 값은 코드에서 setString으로 넘긴 글입니다. 이 값이 ps에 SQL 글자와 따로 들어 있는 것은 H2가 ps를 문자열로 보여 주는 모양(SQL 글자 뒤에 {1: 값})으로 확인했습니다. 이 모양에서 값 안의 작은따옴표가 두 번 쓰이는 것은 H2가 값을 SQL 글자 값으로 보여 주는 표기이고, 값이 SQL 글자에 끼워졌다는 뜻이 아닙니다. 단계 문장이 기대는 사실로, prepareStatement와 setString까지 한 시점과 createStatement까지 한 시점에는 DB의 실행 기록에 이 SELECT가 없는 것, ps의 ? 자리가 1개이고 이어 붙인 SQL의 ? 자리는 0개인 것도 실행으로 확인했습니다(실행 기록은 한 번 읽으면 새 기록이 보이지 않아 시점마다 DB를 따로 만들어 한 번씩만 읽었습니다). O'Neil을 이어 붙인 조합의 오류는 H2의 SQLState 42000(문법 오류)이고, 메시지의 오류 위치는 Neil 바로 뒤였으며 오류가 난 곳은 H2의 문자열 읽기(Tokenizer.readCharacterString)였습니다. 화면의 설명("두 번째 따옴표가 문자열을 끝내고 마지막 따옴표는 닫히지 않은 문자열을 연다")은 이 관찰과 문자열 안의 따옴표를 두 번 써야 한다는 H2 문서에 맞춘 풀이이고, H2가 글자를 어떻게 나누는지는 직접 추적하지 않았습니다. 이 오류 코드는 H2의 것이어서 MySQL에서는 다를 수 있습니다. 실제 DB 로그에 SQL 글자가 어떻게 남는지와 결과 행의 순서(ORDER BY가 없어 DB가 정하는 순서)는 H2에서 본 것일 뿐입니다. 문서는 직접 열어 확인했습니다. OWASP SQL Injection Prevention Cheat Sheet에서 문자열 이어 붙이기와 사용자 입력으로 만든 질의가 위험하다는 것(What Is a SQL Injection Attack?), 준비된 문장에서는 SQL을 먼저 정하고 값을 나중에 넘기며 DB가 입력이 무엇이든 코드와 데이터를 구별한다는 것(Defense Option 1), Java 예제가 SELECT에 ?를 쓰고 setString으로 값을 넘기며 tom' or '1'='1 같은 입력도 그 글 전체와 같은 이름을 찾을 뿐이라는 것(Safe Java Prepared Statement Example)을 따랐습니다. Java SE 21 API 문서의 PreparedStatement에서는 미리 컴파일된 SQL 문장이라는 설명, 값을 넣는 setter가 ? 자리(IN 파라미터)의 값을 정한다는 것, 자리 번호가 1부터라는 것을, Statement에서는 정해진(static) SQL 문장을 실행하는 객체이고 executeQuery(String)이 ResultSet 하나를 돌려준다는 것을 확인했습니다. PreparedStatement 문서는 setString이 따옴표를 어떻게 다루는지는 적고 있지 않아서, 값이 따옴표와 상관없이 다뤄지는 것은 문서가 정한 것이 아니라 이 H2에서 관찰한 동작입니다. H2 문서 grammar 페이지에서 문자열 값이 작은따옴표로 시작하고 끝나며 안에 작은따옴표를 넣으려면 두 번 쓴다는 것을, MySQL 8.4 문서의 String Literals에서 같은 규칙(MySQL은 \를 앞에 붙이는 방법도 허용)을 확인했습니다. 열린 문서는 모두 도구가 돌려준 요약으로 읽었고 원문 전체를 읽지는 않았습니다. 화면의 코드는 짧게 하려고 연결과 ResultSet을 닫는 줄과 예외를 처리하는 코드를 적지 않았고, 예외가 난 줄에서 프로그램이 멈추고 아래 줄이 실행되지 않는 것은 출력이 비어 있는 것으로 확인했습니다. 이 실습은 SQL 인젝션 하나만 다루며, 입력 검사, 출력할 때의 이스케이프, DB 계정 권한, 실제 공격에서 쓰는 다른 수법, 입력이 SQL이 아닌 곳(HTML, 명령어 등)에서 코드로 읽히는 경우는 다루지 않습니다. 웹 공격 전반(XSS, CSRF 포함)은 "웹 공격 세 가지", SQL을 직접 쓰는 JDBC의 반복되는 코드를 줄여 주는 JdbcTemplate은 "JdbcTemplate과 예외 변환"에서 다룹니다.
앞 실습: 출처(origin) · 다음 실습: 프로세스
줄을 그은 곳은 처음 코드에서 지운 것이고, 색을 칠한 곳은 새로 쓴 것입니다.