당신의 자바 애플리케이션은 해킹에 얼마나 안전한가요? 실수 한 줄로도 뚫릴 수 있는 SQL 인젝션, 지금 막아야 합니다.
안녕하세요, 시큐어코딩 및 보안컨설팅에 관심많은 ICT리더 리치입니다. 실무에서 가장 자주 발견되는 보안 취약점 중 하나가 바로 SQL Injection입니다. 특히 Spring Boot와 Java 11 환경에서는 ORM을 사용하더라도 방심하면 큰 문제가 생길 수 있습니다. 이 글에서는 순수 Java 기반의 방어 패턴부터 MyBatis Mapper, JPA ORM, 동적 쿼리 대응까지 모두 다룹니다. 코딩 실수 하나가 해킹을 불러오는 시대, 지금 제대로 된 보안 코드를 익혀보세요.
📌 바로가기 목차
1. SQL Injection 이란?
SQL Injection은 클라이언트로부터 입력받은 값을 검증 없이 SQL 쿼리에 직접 삽입할 경우 발생하는 보안 취약점입니다. 공격자는 입력값에 SQL 구문을 삽입해 데이터베이스를 조작하거나 민감한 정보를 탈취할 수 있습니다.
예를 들어 다음과 같은 코드를 보겠습니다.
// ❌ [취약] 사용자 입력을 직접 쿼리에 삽입 - SQL Injection 발생
String username = request.getParameter("username");
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
// ... Statement stmt = conn.createStatement(); stmt.executeQuery(sql);
사용자가 ' OR '1'='1
을 입력하면 전체 사용자 정보가 조회될 수 있습니다.
💥 실제 해킹 사례: 대형 커머스 기업의 SQL 인젝션 사고
2021년 한 유명 이커머스 플랫폼에서 SQL Injection 취약점을 통해 고객 정보 20만 건 이상이 유출되는 사고가 있었습니다. 공격자는 검색창 입력값을 조작해 관리자 권한을 획득했고, 이후 데이터베이스 전체에 접근해 대량의 개인정보를 탈취했습니다. 이 사건의 핵심 원인은 검색 조건을 직접 SQL에 문자열로 연결한 점이었습니다.
결과적으로 해당 기업은 약 6개월 간 보안점검 및 법적 대응에 수억 원의 비용을 지출했으며, 소비자 신뢰도에도 큰 타격을 입었습니다. SQL Injection 하나가 얼마나 큰 피해로 이어질 수 있는지를 보여주는 대표적인 사례입니다.
2. ORM(JPA) 기반 방어 방법
JPA와 같은 ORM은 기본적으로 SQL Injection에 강한 구조입니다. 하지만 동적 JPQL이나 native query를 사용할 경우 취약할 수 있으므로 파라미터 바인딩을 반드시 사용해야 합니다.
구현 방식 | 안전 여부 |
---|---|
JPQL + 파라미터 바인딩 | 안전 |
nativeQuery + 문자열 결합 | 취약 |
// ✅ [권장] @Query 파라미터 바인딩
@Query("SELECT u FROM User u WHERE u.username = :username")
User findByUsername(@Param("username") String username);
// ❌ [취약] 문자열 직접 삽입 (위험!)
@Query(value = "SELECT * FROM users WHERE username = ?1", nativeQuery = true)
User findByUsername(String username); // 이 방식도 바인딩을 권장
// ✅ [권장] 네이티브 쿼리 + 바인딩
@Query(value = "SELECT * FROM users WHERE username = :username", nativeQuery = true)
User findByUsername(@Param("username") String username);
3. MyBatis Mapper에서의 안전 코딩
MyBatis는 XML Mapper 방식이지만 #{} 바인딩 문법을 사용하면 SQL Injection을 방지할 수 있습니다. 단, ${}
는 SQL이 직접 조립되므로 주의해야 합니다.
SELECT id,username,email,created_at,status
FROM users
WHERE username = #{username}
AND status = 'ACTIVE'
ORDER BY created_at DESC
LIMIT 1
User selectUserByUsername(String username);
// ✅ [권장] #{} 사용 - PreparedStatement 바인딩
<select id="selectUser" resultType="User">
SELECT * FROM users WHERE username = #{username}
</select>
// ❌ [위험] ${} 사용 - 입력값이 그대로 삽입됨
<select id="selectUser" resultType="User">
SELECT * FROM users WHERE username = '${username}'
</select>
4. 동적 쿼리에서의 SQL Injection 방어 전략
동적 쿼리는 사용자의 요청에 따라 SQL 조건이 바뀌는 구조로, SQL Injection이 가장 많이 발생하는 부분입니다. 특히 문자열 결합 기반으로 쿼리를 생성하면 보안 리스크가 커집니다. MyBatis의 <if> 구문과 #{} 바인딩, 또는 JPA CriteriaBuilder를 통해 안전하게 처리해야 합니다.
// ✅ ${}이 아닌 #{} 방식으로 정적쿼리화하여 시큐어코딩을 적용
<select id="searchUser" resultType="User">
SELECT * FROM users
WHERE 1=1
<if test="username != null and username != ''">
AND username = #{username}
</if>
<if test="status != null and status != ''">
AND status = #{status}
</if>
<if test="startDate != null">
AND created_at >= #{startDate}
</if>
<if test="endDate != null">
AND created_at <= #{endDate}
</if>
</select>
5. 코드 리뷰 체크리스트: SQL 보안 점검 포인트
코드 리뷰 시 아래 항목을 기준으로 점검하면 SQL Injection 취약점 발생 가능성을 상당 부분 줄일 수 있습니다.
점검 항목 | 필수 여부 |
---|---|
PreparedStatement 사용 여부 (#{} 바인딩 포함) | ✅ 필수 |
native query 사용 시 파라미터 바인딩 여부 | ✅ 필수 |
${} 사용 금지 여부 | ✅ 필수 |
입력값 유효성 검사 적용 여부 | 🔍 확인 |
6. 자주 묻는 질문 (FAQ)
아닙니다. ORM은 기본적으로 PreparedStatement를 사용해 안전하지만, native query 또는 JPQL에서 문자열 연결을 하면 SQL Injection 위험이 있습니다. 반드시 파라미터 바인딩을 사용하세요.
JPA에서는 createNativeQuery
에 파라미터 바인딩을 사용하세요. MyBatis라면 #{} 문법을 활용해 PreparedStatement가 동작하도록 작성해야 합니다.
order by는 보통 ${}로 작성해야 하므로 위험합니다. 이 경우 enum 또는 화이트리스트 기반의 정렬 컬럼 검증을 하고, 정해진 값만 쿼리에 반영되도록 처리해야 안전합니다.
${}는 쿼리 문자열에 직접 삽입되기 때문에 SQL Injection에 매우 취약합니다. 필수로 써야 할 경우에는 입력값을 서버에서 필터링하거나 enum, 정규표현식 등을 통해 안전성을 확보해야 합니다.
입력값의 유효성 검사는 항상 선행되어야 하며, 입력된 값이 기대한 형식과 길이를 벗어나지 않도록 제한하는 것도 중요합니다. 이와 함께 로깅, 예외처리, 결과 개수 제한 등의 방어 기법도 함께 사용하면 효과적입니다.
7. 마무리 요약
- 문자열 직접 조합된 쿼리는 반드시 피한다.
- ORM 환경에서도 native query 시 바인딩 필수.
- MyBatis의 ${} 사용은 극히 제한적으로, #{}로 대체.
- 동적 쿼리 조립 시 enum 또는 whitelist 기반 처리.
- 입력값 검증 및 유효성 필터는 기본 중의 기본.
SQL Injection은 개발자의 작은 부주의로도 심각한 보안사고로 이어질 수 있습니다. 특히 실무에서 바쁜 일정 속에 놓치기 쉬운 동적 쿼리와 파라미터 바인딩 실수는 반드시 리뷰 시점에서라도 점검해야 합니다. 이 글이 Spring Boot + Java 11 환경에서의 안전한 SQL 처리에 도움이 되셨다면, 다른 팀원들과도 꼭 공유해주세요. 💡 댓글로 실무 사례나 질문도 언제든지 환영합니다!
'시큐어코딩 > JAVA' 카테고리의 다른 글
[시큐어코딩]Java에서 신뢰되지 않은 역직렬화 취약점, 왜 위험하고 어떻게 막을까? (1) | 2025.04.17 |
---|---|
[시큐어코딩]Java에서 XML 외부 개체(XXE) 취약점 안전하게 막는 방법 (1) | 2025.04.16 |
[시큐어코딩]Java에서 부적절한 인증서 유효성 검증 – 보안 연결의 허점을 막는 시큐어코딩 방법 (0) | 2025.04.16 |
[시큐어코딩]Java에서 전자서명 검증 실패가 초래하는 실무 위협과 대응 전략 완벽 가이드 (0) | 2025.04.10 |
[시큐어코딩]Java에서 부적절한 세션 종료 보안 이슈와 안전한 처리 방법4 (0) | 2025.04.10 |