Cert/소프트웨어(SW) 보안약점 진단원

구현단계 보안약점 기준 - SQL 삽입

브루노W 2024. 9. 9. 17:10
유형 입력데이터 검증 및 표현
보안약점 SQL 삽입
개요 데이터베이스(DB)와 연동된 웹 응용프로그램에서 입력된 데이터에 대한 유효성 검증을 하지 않을 경우, 공격자가 입력 폼 및 URL 입력란에 SQL 문을 삽입하여 DB로부터 정보를 열람하거나 조작할 수 있는 보안약점을 말한다.

취약한 웹 응용프로그램에서는 사용자로부터 입력된 값을 필터링 과정 없이 넘겨받아 동적쿼리(Dynamic Query)를 생성하기 때문에 개발자가 의도하지 않은 쿼리가 생성되어 정보유출에 악용될 수 있다.
보안대책 PreparedStatement 객체 등을 이용하여 DB에 컴파일된 쿼리문(상수)을 전달하는 방법을 사용한다.

PreparedStatement를 사용하는 경우에는  DB 쿼리에 사용되는 외부 입력값에 대하여 특수문자 및 쿼리 예약어를 필터링하고, 스트러츠(Struts), 스프링(Spring) 등과 같은 프레임워크를 사용하는 경우에는 외부 입력값 검증모듈 및 보안모듈을 상황에 맞추어 적절하게 사용한다.
진단방법 Statement 객체로 쿼리가 실행되는 부분을 확인하고 Statement 객체가 Pre-paredStatement 객체인지 확인한다.

PreparedStatement 객체를 사용하고 setString 등의 메소드로 외부 입력값을 설정하는 경우에는 기본적으로 안전하다고 판정하지만, 쿼리에 사용되는 변수가 외부 입력값인 경우엔 적절한 필터링 모듈이 존재하는지 추가로 확인한다.

즉, 쿼리 생성과 관련된 외부 입력값에 대한 필터링 모듈이 반드시 존재해야 하며, 그 외에도 관련 프레임워크에서 적절한 조치가 이루어질 경우에 안전하다고 판정한다.
연관된 설계단계 기준 DBMS 조회 및 결과 검증

 

코드예제

 

● 안전하지 않은 코드 예 (Java : JDBC API)

//외부로부터 입력받은 값을 검증 없이 사용할 경우 안전하지 않다.
String gubun = request.getParameter("gubun");

String sql = "SELECT * FROM board WHERE b_gubun = '" + gubun + "'";
Connection con = db.getConnection();
Statement stmt = con.createStatement();
//외부로부터 입력받은 값이 검증 또는 처리 없이 쿼리로 수행되어 안전하지 않다.
ResultSet rs = stmt.executeQuery(sql);

gubun 값으로 a' or 'a' = 'a 를 입력하면 조건절이 b_gubun = 'a' or 'a' = 'a'로 변경되어 board 테이블의 모든 내용이 조회된다.

 

● 안전한 코드 예 (Java : JDBC API)

String gubun = request.getParameter("gubun");

// 1. 사용자에 의해 외부로부터 입력받은 값은 안전하지 않을 수 있으므로, 
// PreparedStatement 사용을 위해 ?문자로 바인딩 변수를 사용한다.
String sql = "SELECT * FROM board WHERE b_gubun = ?";
Connection con = db.getConnection();

// 2. PreparedStatement 사용한다.
PreparedStatement pstmt = con.prepareStatement(sql);

// 3.PreparedStatement 객체를 상수 스트링으로 생성하고,
// 파라미터 부분을 setString등의 메소드로 설정하여 안전하다.
pstmt.setString(1, gubun);

ResultSet rs = pstmt.executeQuery();

PreparedStatement 객체를 상수 스트링으로 생성하고, 파라미터 부분을 setString, setParameter 등의 메소드로 설정한다.

 

 

● 안전하지 않은 코드 예 (Java : MyBatis)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN“
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<select id="boardSearch" parameterType="map" resultType="BoardDto">
// $기호를 사용하는 경우 외부에서 입력된 keyword값을 문자열에 결합한 형태로 쿼리에 반영되므로
// 안전하지 않다.
select * from tbl_board where title like '%${keyword}%' order by pos asc
</select>

외부에서 입력되는 값이 SQL 질의문에 $ 기호로 표현되어 ' '가 자동으로 붙지 않아 파라미터가 그대로 출력되어 SQL 삽입 취약점이 발생할 수 있다.

 

● 안전한 코드 예 (Java : MyBatis)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<select id="boardSearch" parameterType="map" resultType="BoardDto">
// $ 대신 #기호를 사용하여 변수가 쿼리맵에 바인딩 될 수 있도록 수정하는 것이 안전하다.
select * from tbl_board where title like '%'||#{keyword}||'%' order by pos asc
</select>

MyBatis 쿼리맵에 바인딩할 경우 $ 기호가 아닌 # 기호를 사용해야 한다.

 

 

● 안전하지 않은 코드 예 (Java : Hibernate)

import org.hibernate.Query
import org.hibernate.Session

// 외부로부터 입력받은 값을 검증 없이 사용할 경우 안전하지 않다.
String name = request.getParameter("name");
// Hiberate는 기본으로 PreparedStatement를 사용하지만,
// 파라미터 바인딩 없이 사용 할 경우 안전하지 않다.
Query query = session.createQuery("from Student where studentName = '" + name + "' ");

Hibernate는 기본으로 PreparedStatement를 사용하지만, 파라미터 바인딩 없이 사용할 경우 외부 입력값에 의해 쿼리 구조가 변경될 수 있음

 

● 안전한 코드 예 (Java : Hibernate)

import org.hibernate.Query
import org.hibernate.Session

String name = request.getParameter("name");
// 1. 파라미터 바인딩을 위해 ?를 사용한다.
Query query = session.createQuery("from Student where studentName = ? ");
// 2. 파라미터 바인딩을 사용하여 외부 입력값에 의해 쿼리 구조 변경을 못하게 사용하였다.
query.setString(0, name);
import org.hibernate.Query
import org.hibernate.Session

String name = request.getParameter("name");
// 1. 파라미터 바인딩을 위해 명명된 파라미터 변수를 사용한다.
Query query = session.createQuery("from Student where studentName = :name ");
// 2. 파라미터 바인딩을 사용하여 외부 입력값에 의해 쿼리 구조 변경을 못하게 사용하였다.
query.setParameter("name", name);

외부 입력값이 위치하는 부분을 ? 또는 : 명명된 파라미터 변수로 설정한다.

 

 

● 안전하지 않은 코드 예 (C#)

public void ButtonClickBad(object sender, EventArgs e)
{
    string connect = "MyConnString";
    string usrinput = Request["ID"];
   
   // 외부로부터 입력받은 값을 SQL 쿼리에 직접 사용하는 것은 안전하지 않다.
    string query = "Select * From Products Where ProductID = " + usrinput;
    using (var conn = new SqlConnection(connect))
    {
        using (var cmd = new SqlCommand(query, conn))
        {
            conn.Open();
            cmd.ExecuteReader(); /* BUG */
        }
    }
}

외부 입력값 usrinput을 SQL 쿼리에 직접 사용하고 있다.

 

● 안전한 코드 예 (C#)

void ButtonClickGood(object sender, EventArgs e)
{
    string connect = "MyConnString";
    string usrinput = Request["ID"];

    // 파라미터 바인딩을 위해 @을 사용합니다. 외부입력 값에 의해 쿼리 구조 변경을 할 수 없습니다.
    string query = "Select * From Products Where ProductID = @ProductID";
    using (var conn = new SqlConnection(connect))
    {
        using (var cmd = new SqlCommand(query, conn))
        {
            cmd.Parameters.AddWithValue("@ProductID", Convert.ToInt32(Request["ProductID"]);
            conn.Open();
            cmd.ExecuteReader();
        }
    }
}

ProductID를 파라미터 바인딩으로 처리하고 있다.

 

진단방법

 

 

Statement 객체로 쿼리가 실행되는 부분을 확인하고(①) Statement 객체가 Pre-paredStatement 객체인지 확인한다.(②)

PreparedStatement 객체를 사용하고 setString 등의 메소드로 외부 입력값을 설정하는 경우에는 기본적으로 안전하다고 판정하지만, 쿼리에 사용되는 변수가 외부 입력값인 경우엔 적절한 필터링 모듈이 존재하는지 추가로 확인한다.(③)


즉, 쿼리 생성과 관련된 외부 입력값에 대한 필터링 모듈이 반드시 존재해야 하며, 그 외에도 관련 프레임워크에서 적절한 조치가 이루어질 경우에 안전하다고 판정한다.

 

● 일반적인 진단의 예

class Login {
    public Connection getConnection() throws SQLException {
        DriverManager.registerDriver(new com.microsoft.sqlserver.jdbc.SQLServerDriver());
        String dbConnection = PropertyManager.getProperty(“db.connection”);
        return DriverManager.getConnection(dbConnection);
    }
    
    // 외부입력 값(username, password)을 제공받음
    public void doPrivilegedAction(String username, char[] password) throws SQLException {
        Connection connection = getConnection();
        if (connection == null) {
        // handle error
    	}
        try {
            String pwd = HashUtil.hashPassword(password);
            String sqlString = “SELECT * FROM db_user WHERE username = ‘”+ username
            "' AND password = '" + pwd + "'"; ················································③
            Statement stmt = connection.createStatement(); ···································②
            ResultSet rs = stmt.executeQuery(sqlString); ·····································①
            if (!rs.next()) {
            throw new SecurityException(“User name or password incorrect”);
            }
        	// Authenticated; proceed
        } finally {
        	// …
        }
    }
}