메뉴 건너뛰기

app

CWE-89: 데이터베이스를 위협하는 SQL 삽입(SQL Injection)

suritam92026.01.17 13:48조회 수 0댓글 0

    • 글자 크기

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 대응 및 통합 보안 적용 자바 코드 예시

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public class UserAuthenticator {
    private static final Logger logger = LoggerFactory.getLogger(UserAuthenticator.class);
 
    public boolean login(Connection conn, String userId, String userPw) {
        // [CWE-89 조치] ? 파라미터를 사용하는 Prepared Statement 구조 사용
        String sql = "SELECT user_name FROM users WHERE user_id = ? AND user_pw = ?";
        
        // [CWE-489 조치] 쿼리 실행 전후의 상태를 debug 레벨로 기록
        logger.debug("Attempting login for user: {}", userId);
 
        try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
            // 입력값을 바인딩하여 SQL 구문 조작을 원천 차단
            pstmt.setString(1, userId);
            pstmt.setString(2, userPw);
 
            try (ResultSet rs = pstmt.executeQuery()) {
                if (rs.next()) {
                    logger.debug("Login successful for: {}", userId);
                    return true;
                }
            }
        } catch (Exception e) {
            // [CWE-754 & CWE-489 조치] 예외 처리 세분화 및 정보 노출 방지
            // 운영 환경에서는 logger.debug를 통해 내부 상황만 기록
            logger.debug("Database error during login: {}", e.getMessage());
        }
        
        return false;
    }
}
 

코멘트: 데이터베이스 보안의 핵심은 사용자의 입력을 절대 신뢰하지 않는 것입니다. 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)

 
<select id="getUserUnsafe" resultType="User">
    SELECT * FROM users WHERE user_id = '${userId}' </select>
 
<select id="getUserSafe" resultType="User">
    SELECT * FROM users WHERE user_id = #{userId}
</select>
 
 

Java Service Layer

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
 
@Service
public class UserService {
    private static final Logger logger = LoggerFactory.getLogger(UserService.class);
    private final UserMapper userMapper;
 
    public User getUserInfo(String userId) {
        try {
            // [CWE-489] 개발 단계의 흐름 확인용 로그
            logger.debug("Retrieving information for user: {}", userId);
 
            // [CWE-89] # 파라미터를 사용한 안전한 쿼리 호출
            User user = userMapper.getUserSafe(userId);
 
            if (user != null) {
                logger.debug("User found: {}", user.getName());
            }
            return user;
 
        } catch (PersistenceException e) {
            // [CWE-754] DB 관련 예외 세분화 처리
            logger.debug("Database persistence error: {}", e.getMessage());
            return null;
            
        } catch (Exception e) {
            // [CWE-754] 광범위 예외 처리를 통한 가용성 확보
            logger.debug("Unexpected system error: {}", e.getMessage());
            return null;
        }
    }
}
 

실무 코멘트

가급적 모든 파라미터는 #을 사용하여 처리하십시오. 만약 정렬 순서(ORDER BY ${columnName})처럼 어쩔 수 없이 $를 사용해야 하는 경우에는, 입력값이 미리 정의된 안전한 컬럼명 리스트에 포함되는지 검증하는 로직(White-list validation)을 반드시 선행하여 보안 허점을 메워야 합니다.

 
    • 글자 크기
CWE-209: 시스템의 약점을 드러내는 오류 메시지 정보 노출 (by suritam9) CWE-489: 시스템 내부를 노출하는 제거되지 않은 디버그 코드 (by suritam9)

댓글 달기

첨부 (0)
위로