본문 바로가기

Web Security/Secure Coding

시큐어 코딩 (SQL Injection)

SQL Injection 공격이란? 

 데이터베이스(DB)와 연동된 웹 어플리케이션에서 입력된 데이터에 대한 유효성 검증을 하지 않을 경우, 공격자가 입력 폼 및 URL 입력란에 SQL 문을 삽입하여 DB 정보를 조회하거나 조작할 수 있는 공격이다. 

 

SQL Injection 공격 원리

 1. 공격자가 SQL Injection을 통해 조작된 Request를 보냄

 2. 웹 서버는 데이터베이스에 Query 문을 전달 

 3. 데이터베이스는 조작된 SQL Query 실행 

 4. 데이터베이스에서 실행된 결과를 웹서버에게 전달 

 5. 웹 서버는 실행된 결과(공격 결과)를 공격자에게 전달 

 

SQL Injection 예시

로그인하는 부분

예를 들어, admin 계정 (id: admin, pw: admin)이 존재한다고 가정해보자. 

만약 사용자가 아이디와 패스워드에 admin, admin 을 입력을 한 후, 로그인 버튼을 눌렀다고 해보자. 

그럼 GET 또는 POST 방식으로 입력한 값들이 서버로 전달된다.

그 때, 만일 서버에서 Query 문 형식이 아래와 같은 경우, SQL Injection 이 발생할 수 있게 된다. 

SELECT * FROM USER WHERE ID=[유저가 입력한 아이디] AND PW=[유저가 입력한 패스워드];

즉, 사용자로부터 입력된 값을 필터링 과정없이 넘겨받을 경우, 개발자가 의도하지 않은 쿼리가 생성되어 악용될 수 있다.

공격자가 되어 SQL Injection 을 시도한다고 가정해보자. (MySQL 기준) 

아이디에 admin' or 1=1# 이와 같은 입력을 주게 되면, 아래와 같이 Query 문이 형성 될 수 있다.

SELECT * FROM USER WHERE ID='admin' or 1=1# AND PW=[유저가 입력한 패스워드];

위와 같은 쿼리문이 실행되게 되면, # 으로 인해 AND 부분 부터 주석처리가 되어 admin 계정에 대한 Query 문이 올바르게 실행되어 admin 계정으로 로그인이 가능하게 된다. 

 

이와 같은 문제가 발생하는 것을 막기 위해, 사용자가 입력한 값에 대한 필터링을 진행해주어야 함을 알 수 있다. 

 

시큐어 코딩 보안 가이드에 나와있는 JAVA 코드를 예시로 알아보자. 

[안전하지 않은 코드]

public class SqlInjectionSample extends HttpServlet {
    private final String GET_USER_INFO_CMD = "get_user_info"; 
    private Connection con; 
    private Statement stmt; 
    private ResultSet rs;

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOExceptoin {
        String command = request.getParameter("command"); 
        if (command.equals(GET_USER_INFO_CMD)){ 
            Statement stmt = con.createStatement(); 
            String userId = request.getParameter("user_id"); 
            String password = request.getParameter("password"); 
            String query = "SELECT * FROM memebers WHERE username='" + userId + "' AND password='" + password + "'";
            rs = stmt.executeQuery(query);
            
            if(rs != null) { 
                 while(rs.next()) { 
                      // rs.getString("컬럼이름") 
                      // rs.getInt("컬럼이름") 
                      // 사용하여 처리 
                 }
            }else{
                 // 로그인 실패 
            }
        }
    }
}

시큐어 코딩에 나와있는 makeSecureString 함수를 구현하여, 안전한 문자열로 필터링하는 코드를 포함시켜보자. 

1. ID 와 암호 같은 인자의 길이의 제한을 두는 것 

2. 인자에 SQL문에서 쓰이는 예약어의 삽입을 제한하는 것 

3. 인자에 알파벳과 숫자를 제외한 문자의 사용을 제한하는 것 

 

[안전한 코드]

public class SqlInjectionSample extends HttpServlet {
    private final String GET_USER_INFO_CMD = "get_user_info"; 
    private Connection con; 
    private Statement stmt; 
    private ResultSet rs;

    // Secure Coding 
    private final static int MAX_USER_ID_LENGTH = 8 ; 
    private final static int MAX_PASSWORD_LENGTH = 16 ;
    private final static String UNSECURED_CHAR_REGULAR_EXPRESSION ="[^\\{Alnum}]|select|delete|update|insert|create|drop|alter";
    private Pattern unsecuredCharPattern; 
    
    // 정규식을 초기화 
    public void initialize() {
        unsecuredCharPattern = Pattern.compile(UNSECURED_CHAR_REGULAR_EXPRESSION, Pattern.CASE_INSENSITIVE);
    }
    
    // 입력값을 정규식을 이용하여 필터링
    // [^\\p{Alnum}] : 알파벳과 숫자를 의미한 나머지 문자를 의미 
    private String makeSecureString(final String str, int maxLength) { 
        String secureStr = str.substring(0, maxLength); 
        Matcher matcher = unsecuredCharPattern.matcher(secureStr); 
        return matcher.replaceAll(""); 
    }
    
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOExceptoin {
        String command = request.getParameter("command"); 
        if (command.equals(GET_USER_INFO_CMD)){ 
            Statement stmt = con.createStatement(); 
            String userId = request.getParameter("user_id"); 
            String password = request.getParameter("password"); 
            // Secure Coding
            String query = "SELECT * FROM memebers WHERE username='" + makeSecureString(userId, MAX_USER_ID_LENGTH) + "' AND password='" + makeSecureString(password,MAX_PASSWORD_LENGTH)+ "'";
            rs = stmt.executeQuery(query);
            
            if(rs != null) { 
                 while(rs.next()) { 
                      // rs.getString("컬럼이름") 
                      // rs.getInt("컬럼이름") 
                      // 사용하여 처리 
                 }
            }else{
                 // 로그인 실패 
            }
        }
    }
}

위와 같이, 블랙리스트에 의거한 필터링 방식을 사용할 때에는 다양한 형태의 SQL 함수를 이용한 공격방법들을 참고하여 리스트를 만들어야한다. Blind SQL Injection 에서는 if, union 등의 구문을 이용한 방법, 각종 SQL 함수에 대한 필터링이 필요하다. 또는, 외부로부터 인자를 받는 preparedStatement 객체를 상수 스트링으로 생성하고, 인자 부분을 stmt.setString(), stmt.setInt() 등의 메소드를 활용하여, 외부의 입력이 쿼리문의 구조를 바꾸는 것을 방지하는 것이 필요하다. 

 

PreparedStatement Example

```
   String sql = "SELECT * FROM USER WHERE id=? AND pw=?"; 
   PreparedStatement pstmt = conn.preparedStatement(sql); 
   pstmt.setString(1, userid); 
   pstmt.setString(2, userpw); 
   ResultSet result = pstmt.executeQuery(); 
```

이와 같이, Statement 방식이 아닌 PreparedStatemet 를 사용하면, SQL Injection 공격을 막을 수 있다. 그 이유는 preparedStatement를 사용할 경우, conn.preparedStatement(sql) 부분에서 이미 SQL 구문에 대한 컴파일이 진행되기 때문에 set[타입] 메소드를 이용하여 인자를 전달해주게 되면, 입력 값이 SQL 문법이 아닌 일반 문자열로 인식되기 때문이다. 

즉, 쿼리는 이미 Binding 되어있기에, 더 이상 쿼리에 대한 Compilation 이 진행되지 않으므로 SQL Injection 을 막을 수 있게 된다.

 

자세한 이해를 위해선 Statement 와 PreparedStatement 의 동작 원리를 이해하는 것이 중요하다. 

 

참고 문헌

[1] 시큐어 코딩 보안 가이드 https://www.kisa.or.kr/public/laws/laws3_View.jsp?mode=view