본문 바로가기
시큐어코딩/JAVA

[시큐어코딩]Java에서 SQL Injection 취약점 JPA,Mybatis,동적쿼리 완전정복

by ICT리더 리치 2025. 6. 13.

당신의 자바 애플리케이션은 해킹에 얼마나 안전한가요? 실수 한 줄로도 뚫릴 수 있는 SQL 인젝션, 지금 막아야 합니다.

안녕하세요, 시큐어코딩 및 보안컨설팅에 관심많은 ICT리더 리치입니다. 실무에서 가장 자주 발견되는 보안 취약점 중 하나가 바로 SQL Injection입니다. 특히 Spring Boot와 Java 11 환경에서는 ORM을 사용하더라도 방심하면 큰 문제가 생길 수 있습니다. 이 글에서는 순수 Java 기반의 방어 패턴부터 MyBatis Mapper, JPA ORM, 동적 쿼리 대응까지 모두 다룹니다. 코딩 실수 하나가 해킹을 불러오는 시대, 지금 제대로 된 보안 코드를 익혀보세요.

SQL injection 취약점 개요 및 방어기법에 대한 설명 이미지 (여성 강사가 노트북과 태블릿 앞에서 웃고 있음)
SQL Injection 개념과 방어 전략을 한눈에 설명하는 인포그래픽 – JPA, Mapper, 동적 SQL 대응

 

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 &gt;= #{startDate}
  </if>
  <if test="endDate != null">
    AND created_at &lt;= #{endDate}
  </if>
</select>

전문가스러운 여성 강사가 SQL injection 원인과 방어기법을 소개하는 강의 포스터 스타일 이미지
SQL Injection 방어 전략을 소개하는 전문강사의 모습 썸네일 – JPA Hibernate, SQL Mapper, Dynamic SQL 대응 포함

 

5. 코드 리뷰 체크리스트: SQL 보안 점검 포인트

코드 리뷰 시 아래 항목을 기준으로 점검하면 SQL Injection 취약점 발생 가능성을 상당 부분 줄일 수 있습니다.

점검 항목 필수 여부
PreparedStatement 사용 여부 (#{} 바인딩 포함) ✅ 필수
native query 사용 시 파라미터 바인딩 여부 ✅ 필수
${} 사용 금지 여부 ✅ 필수
입력값 유효성 검사 적용 여부 🔍 확인

6. 자주 묻는 질문 (FAQ)

Q ORM을 쓰면 무조건 안전한가요?

아닙니다. ORM은 기본적으로 PreparedStatement를 사용해 안전하지만, native query 또는 JPQL에서 문자열 연결을 하면 SQL Injection 위험이 있습니다. 반드시 파라미터 바인딩을 사용하세요.

Q native query를 써야 할 때, 안전하게 쿼리하는 방법은?

JPA에서는 createNativeQuery에 파라미터 바인딩을 사용하세요. MyBatis라면 #{} 문법을 활용해 PreparedStatement가 동작하도록 작성해야 합니다.

Q 동적 정렬 조건(order by)은 어떻게 보안 처리하나요?

order by는 보통 ${}로 작성해야 하므로 위험합니다. 이 경우 enum 또는 화이트리스트 기반의 정렬 컬럼 검증을 하고, 정해진 값만 쿼리에 반영되도록 처리해야 안전합니다.

Q ${}는 언제 써야 하나요? 꼭 피해야 하나요?

${}는 쿼리 문자열에 직접 삽입되기 때문에 SQL Injection에 매우 취약합니다. 필수로 써야 할 경우에는 입력값을 서버에서 필터링하거나 enum, 정규표현식 등을 통해 안전성을 확보해야 합니다.

Q 파라미터 바인딩 외에 추가로 체크해야 할 보안 항목은?

입력값의 유효성 검사는 항상 선행되어야 하며, 입력된 값이 기대한 형식과 길이를 벗어나지 않도록 제한하는 것도 중요합니다. 이와 함께 로깅, 예외처리, 결과 개수 제한 등의 방어 기법도 함께 사용하면 효과적입니다.

7. 마무리 요약

🔒 핵심 요약
  • 문자열 직접 조합된 쿼리는 반드시 피한다.
  • ORM 환경에서도 native query 시 바인딩 필수.
  • MyBatis의 ${} 사용은 극히 제한적으로, #{}로 대체.
  • 동적 쿼리 조립 시 enum 또는 whitelist 기반 처리.
  • 입력값 검증 및 유효성 필터는 기본 중의 기본.

SQL Injection은 개발자의 작은 부주의로도 심각한 보안사고로 이어질 수 있습니다. 특히 실무에서 바쁜 일정 속에 놓치기 쉬운 동적 쿼리와 파라미터 바인딩 실수는 반드시 리뷰 시점에서라도 점검해야 합니다. 이 글이 Spring Boot + Java 11 환경에서의 안전한 SQL 처리에 도움이 되셨다면, 다른 팀원들과도 꼭 공유해주세요. 💡 댓글로 실무 사례나 질문도 언제든지 환영합니다!

반응형