[Android/오르다 다이어리] 레거시 리팩토링 08 - SQL Injection 테스트 및 해결
지난 글: https://yujinius45.tistory.com/162
[Android/오르다 다이어리] 레거시 리팩토링 07 - SQLite, MapMemo 기능 완성
[Android/오르다 다이어리] 레거시 리팩토링 07 - SQLite, MapMemo 기능 완성지난 글: https://yujinius45.tistory.com/161 [Android/오르다 다이어리] 레거시 리팩토링 06 - DBHelper 싱글톤 패턴 적용[Android/오르다 다이
yujinius45.tistory.com
지난 번 MapMemo 기능을 완성 작업을 진행하면서 DBHelper를 보다가 문득 SQL Injection이 가능한 코드라는 생각이 들었다. 그래서 이에 대해 시도해보고 Injection을 방어할 수 있게 해결해보고자 한다.
💡 SQL Injection 테스트 및 해결 과정
기존 DBHelper 코드
현재 DBHelper 클래스의 insertTodo 메서드는 아래와 같이 구현되어 있다.
public void insertTodo(String _title, String _content, String _writeDate) {
SQLiteDatabase db = getWritableDatabase();
db.execSQL("INSERT INTO TODOLIST_TB (title, content, writeDate) VALUES ('" + _title + "','" + _content + "','" + _writeDate + "');");
}
이전 테스트 코드들을 작성하면서 SQL Injection이 실제로 가능한지 확인해보고 싶어졌다. Room DB와 같은 ORM을 사용하면 이러한 문제를 방지할 수 있지만, 현재 레거시 코드에서는 직접 SQL을 실행하기 때문에 보안 위험이 존재한다고 판단하여 이를 직접 테스트해보기로 했다.
SQL Injection 테스트 코드
아래와 같은 테스트 코드를 작성하여 SQL Injection의 가능성을 검증했다.
@Test
public void testSQLInjectionUpdateTodo() {
dbHelper.insertTodo("Todo A", "Content A", "2025-03-01");
dbHelper.insertTodo("Todo B", "Content B", "2025-03-01");
dbHelper.insertTodo("Todo C", "Content C", "2025-03-01");
ArrayList<TodoItem> todoList = dbHelper.getTodoList("2025-03-01");
assertEquals(3, todoList.size());
int targetTodoId = todoList.get(0).getId();
// SQL Injection 시도 - 모든 데이터를 업데이트하도록 WHERE 조건 변경
String injectionTitle = "Injected Title', writeDate='2025-03-02' WHERE id=id OR 1=1 --";
dbHelper.updateTodo(targetTodoId, injectionTitle, "Injected Content", "2025-03-02", "2025-03-01");
ArrayList<TodoItem> manipulatedList = dbHelper.getTodoList("2025-03-02");
// Injection이 성공했다면 모든 To-Do의 날짜가 변경됨 (원래 1개만 변경되어야 함)
assertTrue("SQL Injection should have updated multiple rows unexpectedly!", manipulatedList.size() > 1);
}
테스트 결과
3개의 값을 넣어두고 의도대로라면 1개만 변경되어야 하지만, WHERE 1=1을 추가하는 Injection을 시도하여 모든 행이 업데이트되는 결과가 발생했다.
테스트 결과 SQL Injection이 성공하여 1개가 아니라 모든 행이 다 업데이트 되어 버렸다.
▼ SQL Injection 테스트 코드 통과 캡쳐

실제 앱에서 실행한 결과
실제 앱에서도 동일한 Injection을 수행했을 때, 앱이 크래시하는 문제가 발생했다.
▼ 할 일 추가 제목에 Injection 시도 → 결과: 앱 크래시 종료

이처럼 SQL 쿼리에 사용자 입력값을 직접 포함하면 발생하는 문제다. SQL Injection에 대해 이론적으로만 알고 있었는데, 직접 테스트를 통해 실제로 발생할 수 있는 보안 문제임을 확인할 수 있었다.
✅ 잠깐, SQL Injection이란?
SQL Injection은 사용자 입력값이 SQL 쿼리에 직접 삽입될 때 발생하는 보안 취약점으로, 악의적인 사용자가 SQL 문을 변조하여 데이터베이스의 내용을 무단 조회, 수정, 삭제하는 공격 기법이다.
SQL Injection의 위험성
- 데이터 변조:
WHERE 1=1을 추가하여 모든 데이터를 변경할 수 있음 - 데이터 유출:
UNION SELECT등을 사용하여 테이블의 모든 데이터 조회 가능 - 데이터 삭제:
DROP TABLE명령을 삽입하여 테이블 삭제 가능
💡 SQL Injection 방지 방법 (Prepared Statement 사용)
SQLite를 사용하는 현 상황에서 SQL Injection을 방지하기 위해 Prepared Statement를 사용할 수 있다.
✅ Prepared Statement란?
- Prepared Statement는 SQL 쿼리를 실행할 때 미리 컴파일된 SQL 문을 사용하는 방식이다. 즉, 애플리케이션이 SQL 문을 실행할 때마다 전체 SQL 문을 다시 해석(파싱)하고 실행하는 것이 아니라, 미리 컴파일된 SQL 문을 재사용하는 방식이다.
이 방식은 SQL Injection 공격을 방지하고, 성능을 최적화하는 데 도움을 준다.
✅ Prepared Statement의 동작 과정
- SQL 템플릿을 미리 컴파일→
?(바인딩 변수)를 포함한 SQL 문을 먼저 데이터베이스에 전달하여 컴파일함. - 사용자 입력 값과 SQL을 분리→
?자리에 들어갈 실제 값을 나중에 바인딩(대입)하여 실행. - 실제 값이 SQL과 함께 실행됨→ 입력 값은 데이터베이스 엔진에서 문자열 값 그대로 처리되며, 실행 계획을 변경하지 않음.
✅ Prepared Statement 예제 (SQLite 사용)
SQLiteDatabase db = getWritableDatabase();
String sql = "INSERT INTO TODOLIST_TB (title, content, writeDate) VALUES (?, ?, ?)";
SQLiteStatement stmt = db.compileStatement(sql);
stmt.bindString(1, title);
stmt.bindString(2, content);
stmt.bindString(3, writeDate);
stmt.executeInsert();
- 여기서
?에 들어갈 값을bindString()으로 바인딩(대입)하고 실행하는 방식 - 이렇게 하면 사용자가 입력한 값이 SQL 문 자체에 포함되지 않고 독립적인 데이터 값으로 처리됨
✅ ? 바인딩이 Escape 처리를 한다는 의미
SQL Injection의 핵심은 사용자의 입력값이 SQL 문법과 결합되어 의도치 않은 실행을 유도하는 것이다.
예를 들어, 기존 코드에서는 다음과 같이 문자열을 직접 SQL 문에 삽입하고 있었다
String query = "SELECT * FROM TODOLIST_TB WHERE title = '" + userInput + "'";
위 코드에서 userInput에 만약 ' OR 1=1 -- 같은 값이 들어가면?
SELECT * FROM TODOLIST_TB WHERE title = '' OR 1=1 --'
1=1은 항상 참(True)이므로 모든 데이터가 반환됨.-이후는 주석 처리되어 SQL 구문이 변조됨.
하지만 Prepared Statement에서는 ? 바인딩을 사용하여 Escape 처리를 자동으로 수행한다.
String query = "SELECT * FROM TODOLIST_TB WHERE title = ?";
Cursor cursor = db.rawQuery(query, new String[]{userInput});
여기서 ? 자리에 입력값이 들어갈 때, 자동으로 Escape 처리되어 문자열 그대로 사용된다.
즉,
- 사용자가
' OR 1=1 --같은 값을 입력하더라도 - 데이터베이스 엔진은 이를 문자열 값 그대로 해석하고,
- SQL 문법과 결합되지 않도록 안전하게 처리하게 되는 것이다.
✅ Escape 처리란?
Escape 처리는 특수 문자(', ", ;, -- 등)가 SQL 문법으로 해석되지 않도록 보호하는 방법이다.
- Prepared Statement에서 자동으로 Escape 처리를 수행하므로,
- 개발자가 따로
replace("'", "\\'")같은 조작을 할 필요가 없다.
다시 돌아와서 수정해보자.
☠️ 기존의 취약한 코드 (❌ 위험한 방식)
public void updateTodo(int _id, String _title, String _content, String _writeDate) {
SQLiteDatabase db = getWritableDatabase();
db.execSQL("UPDATE TODOLIST_TB SET title='" + _title + "', content='" + _content + "', writeDate='" + _writeDate + "' WHERE id=" + _id);
}
문제점:
- 사용자 입력값이 직접 SQL 문에 삽입됨 → SQL Injection 위험
😃 개선된 안전한 코드 (✅ Prepared Statement 사용)
public void updateTodoSecure(int _id, String _title, String _content, String _writeDate) {
SQLiteDatabase db = getWritableDatabase();
String query = "UPDATE TODOLIST_TB SET title = ?, content = ?, writeDate = ? WHERE id = ?";
db.execSQL(query, new Object[]{_title, _content, _writeDate, _id});
}
✔ ? 바인딩을 사용하면 SQL Injection을 방어할 수 있음
✔ 입력값을 자동으로 Escape 처리하여 악의적인 SQL 코드 실행 방지
✔ SQL 문법이 깨지지 않아 앱이 크래시하는 문제도 해결
💡 개선된 코드 테스트
위의 방법으로 DBHelper를 수정하고 다시 테스트 코드를 실행한 결과, Injection이 차단되어 더 이상 여러 개의 행이 변경되지 않음을 확인할 수 있었다.
▼ SQL Injection 테스트 코드가 통과되지 못하는 것 캡쳐 및 앱이 크래시 되지 않은 상태 캡쳐

✅ Injection 시도 후에도 1개의 행만 업데이트됨
✅ 앱 크래시 문제 해결
✅ Injection 방어 성공
⭐ DBHelper에 Prepared Statement를 적용하기 전 테스트 코드를 작성해두자.
- 변경이 원래 기능에 영향이 미치지 않는지 확인용으로 테스트 코드를 작성해두고자 한다.
- 또한, 위의 Injection 테스트 코드를 아래와 같이 변경하여 Injection 방어가 되었다면 테스트가 통과 되도록 변경했다.
// Injection이 성공했다면 모든 To-Do의 날짜가 변경됨 (원래 1개만 변경되어야 함)
//assertTrue("SQL Injection should have updated multiple rows unexpectedly!", manipulatedList.size() > 1);
// Injection 방어 코드 작성 후 False가 되어야 테스트 코드 통과
assertFalse("SQL Injection should have updated multiple rows unexpectedly!", manipulatedList.size() > 1);
아래와 같이 코드 변경 전, 후로 동일하게 테스트 코드가 통과되는 것을 확인했다.
자세히 보면 Prepared Statement 적용 후 실행 시간이 살짝 증가한 것을 볼 수 있다. 이는 테스트 환경에서 Prepared Statement가 처음 실행되어 SQL 파싱, 컴파일, 실행 준비 과정으로 인해 증가한 것으로 볼 수 있다. 이후 실제 앱 사용에서 같은 SQL을 다시 실행할 때는 이미 준비된 SQL을 활용해서 파싱/컴파일 과정 없이 값만 바인딩해서 바로 실행되므로 시간이 절약될 것이다.

⭐ SQL Injection 테스트 및 해결 과정 결론
이번 테스트를 통해 레거시 코드에서 SQL Injection이 발생할 수 있음을 확인했고, 이를 방지하기 위해 Prepared Statement를 사용한 안전한 SQL 실행 방식을 적용했다.
☺️ 후기
- SQL Injection을 직접 테스트하면서 배운 점
- 예전에는 이론적으로만 알던 SQL Injection이 실제로 앱에서 어떻게 발생하는지 경험하면서 더 확실히 이해할 수 있었다.
- Prepared Statement를 적용하고 나서 Injection이 방어되는 걸 직접 테스트로 확인하니 보안적인 개선이 체감되었다.
- 앱 보안을 고려한 SQL 실행 방식이 얼마나 중요한지 다시 한번 깨달았다.
- 향후 개선하고 싶은 부분
- SQLite → Room으로 마이그레이션 하는 것을 고려하고 있다.
- SQLite를 사용해도 충분히 구현 가능하지만 버전 관리 및 유지보수 편리성으로 Room으로 마이그레이션 하는 것이 좋을 것 같다는 생각을 했다. 추가 리팩토링이나 기능 추가가 된다면 그때 Room으로 변경해보는 것을 고려해봐야겠다.
'Android > 오르다 다이어리' 카테고리의 다른 글
| [Android/오르다 다이어리] 레거시 리팩토링 10 - Release 2 (v2.0.0) 완료 및 후기 (0) | 2025.03.02 |
|---|---|
| [Android/오르다 다이어리] 레거시 리팩토링 09 - HomeFragment 애니메이션 적용 & UI 개선 (0) | 2025.03.02 |
| [Android/오르다 다이어리] 레거시 리팩토링 07 - SQLite, MapMemo 기능 완성 (1) | 2025.03.02 |
| [Android/오르다 다이어리] 레거시 리팩토링 06 - DBHelper 싱글톤 패턴 적용 (0) | 2025.03.01 |
| [Android/오르다 다이어리] 레거시 리팩토링 05 - Configuration Change 대응 (ViewModel 적용) (0) | 2025.03.01 |