1. SQL 삽입의 발생 원인과 위험성
SQL 삽입은 사용자로부터 입력받은 값이 필터링되지 않고 질의문(Query) 생성에 직접 사용될 때 발생합니다. 공격자가 입력창에 악의적인 SQL 구문(예: ' OR '1'='1)을 주입하면, 애플리케이션은 이를 정상적인 명령의 일부로 해석하여 실행하게 됩니다. 이 취약점이 노출되면 공격자는 인증을 우회하여 관리자 권한을 획득하거나, 데이터베이스에 저장된 민감 정보를 유출하고 테이블을 삭제하는 등 시스템 전체에 심각한 피해를 입힐 수 있습니다.
2. 흔히 발생하는 잘못된 패턴
가장 위험한 패턴은 문자열 더하기(+ 또는 append) 연산자를 사용하여 SQL 문을 동적으로 생성하는 방식입니다. 개발자가 디버깅을 위해 logger.debug()로 쿼리를 출력하며 확인하더라도, 근본적으로 파라미터가 쿼리 구조와 분리되지 않는다면 보안 결함을 피할 수 없습니다. 또한, 입력값에 대한 유효성 검증(CWE-112)이 누락되거나 에러 메시지(CWE-209)를 통해 쿼리 구조가 노출되는 환경에서는 공격 성공률이 더욱 높아집니다.
3. 실무적 대응: Prepared Statement 사용
SQL 삽입을 방어하는 가장 확실하고 표준적인 방법은 **Prepared Statement(매개변수화된 질의문)**를 사용하는 것입니다. 이 방식은 SQL 쿼리의 구조를 먼저 컴파일하고 사용자의 입력값은 나중에 바인딩하기 때문에, 입력값에 포함된 SQL 특수문자가 쿼리 구조를 변경하지 못하고 단순한 문자열 데이터로만 취급됩니다. 아울러 마이바티스(MyBatis)와 같은 프레임워크를 사용한다면 $ 변수 대신 # 변수를 사용하여 자동으로 매개변수화 처리가 되도록 강제해야 합니다.
4. CWE-89 대응 및 통합 보안 적용 자바 코드 예시
코멘트: 데이터베이스 보안의 핵심은 사용자의 입력을 절대 신뢰하지 않는 것입니다. SQL 삽입(CWE-89) 방어를 위해 Prepared Statement를 필수로 적용하되, 앞서 논의한 시큐어 코딩 규칙들(XXE 방어, 예외 처리 세분화, 디버그 코드 제거)을 함께 적용하면 데이터 유출부터 시스템 장애까지 아우르는 견고한 보안 체계를 완성할 수 있습니다. 운영 환경에서는 로깅 레벨 조절을 통해 디버깅 정보가 공격자에게 힌트가 되지 않도록 철저히 관리하십시오.
MyBatis에서 $ 대신 #을 사용해야 하는 가장 큰 이유는 SQL 삽입(SQL Injection) 공격 방어와 쿼리 성능 최적화 때문입니다.
실무적인 관점에서 두 방식의 차이점과 보안 코드를 정리해 드립니다.
1. # (Sharp)와 $ (Dollar)의 근본적인 차이
# (Sharp): Prepared Statement 방식
• 작동 원리: 사용자가 입력한 값을 ? (파라미터 홀더)로 치환한 뒤, 실행 시점에 값을 바인딩합니다.
• 보안성: 입력값에 SQL 특수문자가 포함되어도 단순한 문자로 처리되므로 SQL 삽입 공격이 불가능합니다.
• 성능: 쿼리 실행 계획(Execution Plan)을 재사용할 수 있어 성능상 유리합니다.
$ (Dollar): Statement 방식
• 작동 원리: 사용자가 입력한 값을 SQL 문장 안에 그대로 이어 붙입니다(String Concatenation).
• 보안성: 입력값이 쿼리의 구조를 바꿀 수 있어 SQL 삽입 공격에 매우 취약합니다.
• 용도: 테이블명이나 컬럼명처럼 ?를 쓸 수 없는 동적인 구문을 작성할 때만 예외적으로 사용합니다.
2. SQL 삽입 취약점 사례
만약 사용자 ID로 조회를 하는데 $를 사용한다면 다음과 같은 사고가 발생할 수 있습니다.
• MyBatis 설정: SELECT * FROM users WHERE user_id = '${userId}'
• 공격자 입력: admin' OR '1'='1
• 실제 실행되는 SQL: SELECT * FROM users WHERE user_id = 'admin' OR '1'='1'
• 결과: 조건절이 무력화되어 모든 사용자 정보가 유출됩니다. 반면 #을 사용하면 'admin'' OR ''1''=''1'이라는 이상한 이름의 사용자를 찾으려다 실패하므로 안전합니다.
3. 보안 적용 코드 예시 (CWE-89 대응)
블로그 포스팅에 바로 사용하실 수 있도록 logger.debug()와 세분화된 예외 처리를 포함한 코드를 작성해 드립니다.
Mapper XML (MyBatis)
Java Service Layer
실무 코멘트
가급적 모든 파라미터는 #을 사용하여 처리하십시오. 만약 정렬 순서(ORDER BY ${columnName})처럼 어쩔 수 없이 $를 사용해야 하는 경우에는, 입력값이 미리 정의된 안전한 컬럼명 리스트에 포함되는지 검증하는 로직(White-list validation)을 반드시 선행하여 보안 허점을 메워야 합니다.
댓글 달기