가이드라인

SQL 인젝션

이제 SQL 인젝션을 살펴볼 차례입니다. 오랫동안 OWASP Top 10에서 부동의 1위를 차지했던 공격 기법이죠. 20년이 넘는 긴 역사를 가지고 있고, 최근 순위에서는 다소 밀려났지만 여전히 매우 대중적이고 위험한 취약점입니다.

웹 보안 취약점인 SQL 인젝션(SQLi)은 공격자가 데이터베이스를 조작하여 중요한 정보를 탈취할 수 있게 해주기 때문에 여전히 가장 흔하게 사용되는 '해킹' 기법 중 하나입니다. 더 심각한 점은 공격자가 데이터베이스 서버의 관리자 권한을 획득하여 데이터베이스 파괴, 트랜잭션 조작, 데이터 유출 등 치명적인 피해를 입히고 추가적인 공격에 취약하게 만들 수 있다는 것입니다.

어떻게 발생하는지 간단히 살펴보겠습니다.

SQL(Structured Query Language)은 관계형 데이터베이스와 통신하기 위한 언어입니다. 개발자, 데이터베이스 관리자, 애플리케이션이 매일 생성되는 방대한 양의 데이터를 관리하기 위해 사용하는 쿼리 언어죠.

애플리케이션 내부에는 데이터와 코드라는 두 가지 컨텍스트가 존재합니다. 코드 컨텍스트는 컴퓨터에 무엇을 실행할지 지시하며, 처리할 데이터와는 분리되어 있습니다. SQL 인젝션은 공격자가 입력한 데이터가 SQL 인터프리터에 의해 코드로 잘못 해석될 때 발생하며, 이를 통해 공격자는 애플리케이션에서 중요한 정보를 수집할 수 있게 됩니다.

SQL 인젝션 공격의 영향

SQL 인젝션은 모든 웹 애플리케이션에 극도로 해로울 수 있으며, 공격자에게 중요한 데이터에 대한 무단 액세스 권한을 제공하기 때문에 수많은 대규모 보안 사고의 원인이 되어 왔습니다. 공격자는 사용자 이름과 비밀번호부터 신용카드 정보, 주민등록번호에 이르기까지 방대한 정보를 탈취할 수 있습니다.

이 데이터에 접근한 공격자는 계정을 탈취하고, 비밀번호를 재설정하며, 무단으로 온라인 쇼핑을 즐기거나 훨씬 더 심각한 유형의 사기를 저지를 수 있습니다.

하지만 SQLi의 가장 무서운 점은 공격자가 탐지되지 않을 경우 시스템에 백도어를 설치하여 장기간 유지할 수 있다는 것입니다. 상상할 수 있듯이, 백도어가 열려 있는 동안에는 데이터 유출이 반복적으로 발생하게 됩니다. 정말 위험한 일이죠.

실제로 어떻게 나타나는지 몇 가지 예시를 통해 더 자세히 알아보겠습니다.

SQLi 예시

SQLi는 다양한 상황에 대응할 수 있는 여러 취약점 기법을 포함합니다. 아래는 가장 흔한 SQLi 예시들입니다.

Technique Description
Retrieving Hidden Data With this technique, attackers can modify any SQL query to gather more information from the database.
Data Examination Attackers can extract information about the version and structure of a database which helps them exploit further information. This technique may differ across databases.
Union Attacks Attackers can extract information about the version and structure of a database which helps them exploit further information. This technique may differ across databases.
Blind SQLi With Blind SQLi, attackers can implement the query on a database. The catch is that the attackers control this query, and it does not return any results in the application’s response.
Subverting Application Logic Attackers interfere with or manipulate a query to disrupt the application’s logic. To manipulate the query, attackers can combine the SQL comment sequence “--” and a WHERE clause.

‍

SQLi 유형

자, 이제 세 가지 SQLi 유형을 살펴보겠습니다.

인밴드(In-band) SQLi

이는 가장 흔하고 간단하며 효율적인 SQL 인젝션 유형 중 하나입니다. 이 공격 유형에서는 동일한 통신 채널을 사용하여 공격을 수행하고 그 결과값을 가져옵니다.

인밴드 SQLi 공격의 두 가지 유형은 다음과 같습니다.

  • Union 기반 SQLi - Union 기반 공격은 UNION 연산자를 사용하여 SELECT 문과 같은 두 개 이상의 SQL 쿼리를 결합함으로써, 원하는 정보를 얻고 그 결과를 HTTP GET 응답으로 받아냅니다.
  • ‍Error 기반 SQLi - 공격자는 데이터베이스의 오류 메시지를 활용하여 데이터베이스 구조를 파악합니다. 이 공격에서 공격자는 서버가 오류 메시지를 표시하도록 유도하는 잘못된 요청을 보내거나 특정 동작을 수행하여 데이터베이스 정보를 얻어냅니다. 이것이 바로 개발자가 라이브 환경에서 오류나 로그 메시지를 그대로 노출하지 않아야 하는 이유입니다. 대신, 이러한 메시지는 접근이 제한된 곳에 안전하게 저장되어야 합니다.

추론적 SQL 인젝션

추론적(Inferential) 또는 블라인드 SQL 인젝션 공격은 더 복잡하며 공격을 수행하는 데 시간이 더 걸릴 수 있습니다. 게다가 공격자가 공격 결과를 즉시 확인할 수 없기 때문에 '블라인드' 공격이라고 불립니다.

공격자는 HTTP 요청을 통해 데이터베이스 서버로 페이로드를 전송하여 사용자의 데이터베이스 구조를 변경하려 시도하며, 이후 애플리케이션의 응답과 동작을 관찰하여 공격 성공 여부를 판단합니다.

추론적 SQL 인젝션 공격에는 다음과 같은 두 가지 유형이 있습니다.

  • 불리언 기반 블라인드 SQL 인젝션 - 이 공격은 데이터베이스에 쿼리를 보내 불리언(참 또는 거짓) 결과를 얻어내고, 공격자가 HTTP 응답을 관찰하여 해당 불리언 결과를 예측하는 방식입니다.
  • 시간 기반 블라인드 SQL 인젝션 - 이 공격은 공격자가 데이터베이스에 쿼리를 보내 응답을 보내기 전까지 몇 초간 대기하도록 만들고, HTTP 요청의 응답 시간을 통해 쿼리 결과를 판단하는 방식입니다.

대역 외(Out-of-band) SQL 인젝션

이는 데이터베이스 서버의 활성화된 기능에 의존하는 비교적 드문 유형의 SQL 인젝션 공격입니다. 다른 공격 유형을 사용할 수 없는 상황에서 발생합니다.

예를 들어, 인밴드(in-band) 공격을 위한 통신 채널을 사용할 수 없거나, HTTP 응답이 쿼리 결과를 파악하기에 충분하지 않은 경우 등이 있습니다.
또한, 이 공격은 데이터베이스 서버가 HTTP나 DNS 요청을 수행하여 공격자에게 필요한 데이터를 전송할 수 있어야 한다는 제약이 커서 흔하게 발생하지는 않습니다.

SQL 인젝션 방어 방법

다행히 SQL 인젝션은 오래되고 흔한 공격인 만큼 이를 방지할 수 있는 방법들이 잘 마련되어 있습니다. 이러한 예방 기술을 사용하는 것은 좋은 코딩 습관일 뿐만 아니라, 조직의 SQL 인젝션 보안을 크게 강화해 줍니다.

입력값 검증, 웹 애플리케이션 방화벽(WAF) 사용, 데이터베이스 보안 강화, 외부 보안 팀이나 시스템 도입, 그리고 완벽한 SQL 쿼리 작성 등 데이터베이스 서버를 이러한 공격으로부터 보호할 수 있는 다양한 방법이 있습니다.

앞서 언급한 보안 조치 중 하나를 적용하여 Python에서 SQL 인젝션을 방지하는 예시를 살펴보겠습니다.

Python 예제

이 예제에서는 공격자가 시스템에서 중요한 정보를 탈취하기 위해 불리언 기반 블라인드 SQL 인젝션을 사용하는 상황을 가정합니다.

Python: 취약한 코드

데이터베이스에 "sample_data"라는 테이블이 있다고 가정해 보겠습니다. 이 테이블은 애플리케이션 사용자의 이름과 비밀번호를 저장합니다.

이제 사용자가 다음 명령어를 통해 데이터베이스 테이블에서 값을 찾을 수 있도록 허용합니다:

import mysql.connector
db = mysql.connector.connect
#나쁜 관행입니다. 피하세요! 학습용으로만 작성되었습니다.
(host="localhost", user="newuser", passwd="pass", db="sample")
cur = db.cursor()
name = raw_input('Enter Name: ')
cur.execute("SELECT * FROM sample_data WHERE Name = '%s';" % name) for row in cur.fetchall(): print(row)
db.close()

SQL 인젝션

여기서 사용자가 검색창에 'Alicia'와 같은 이름을 입력하면 출력 결과에 아무런 문제가 없습니다.

하지만 사용자가 ' Alicia’; DROP TABLE sample_data; '와 같은 값을 입력하면 데이터베이스에 심각한 영향을 미치게 됩니다.

Python: 해결 방법

공격을 방지하려면 SQL 문을 다음과 같이 변경해야 합니다.

cur.execute("SELECT * FROM sample_data WHERE Name = %s;", (name,))

이제 시스템은 사용자가 SQL 쿼리를 삽입하려고 시도하더라도 사용자 입력을 문자열로 취급하며, 오직 이름 값으로만 처리합니다.

이 간단한 변경만으로도 향후 쿼리에서 악의적인 활동을 방지하고 사용자 입력 공격으로부터 시스템을 보호할 수 있습니다.

Java 예제

이 예제에서도 애플리케이션의 사용자 데이터를 저장하는 "sample_data"라는 데이터베이스 테이블을 사용합니다.

기본적인 로그인 페이지는 사용자 이름과 비밀번호를 입력받으며, 서블릿(LoginServlet)인 자바 파일이 데이터베이스를 통해 이를 검증하여 로그인 작업을 허용합니다.

Java: 취약한 예제

시스템은 데이터베이스의 "sample_data" 테이블을 사용하여 사용자의 자격 증명을 입력받아 로그인 작업을 수행합니다.

LoginServlet 파일에는 로그인 작업을 처리하기 위한 다음과 같은 쿼리가 포함되어 있습니다.

//잘못된 예시입니다. 문자열 연결을 사용하지 마세요.
String query = "select * from sample_data where username='" + username + "' and password = '" + password + "'";
        Connection conn = null;
        Statement stmt = null;
        try {
            conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/user", "root", "root");
            stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery(query);
            if (rs.next()) {
                // 일치하는 항목이 발견되면 로그인 성공
                success = true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                stmt.close();
                conn.close();
            } catch (Exception e) {}
        }
        if (success) {
            response.sendRedirect("home.html");
        } else {
            response.sendRedirect("login.html?error=1");
        }
    }

사용자 로그인을 위한 쿼리는 다음과 같습니다.

select * from sample_data where username='username' and password ='password'

SQL 인젝션

입력이 올바르면 시스템은 완벽하게 작동합니다. 예를 들어, 사용자 이름이 Alicia이고 비밀번호가 secret이라고 가정해 보겠습니다.

시스템은 해당 자격 증명을 가진 사용자의 데이터를 반환합니다. 하지만 공격자는 Postman과 cURL을 사용하여 사용자 요청을 조작함으로써 SQL 인젝션을 시도할 수 있습니다.

예를 들어, 해커는 더미 사용자 이름(Alicia)과 비밀번호 ‘or ‘1’=’1’을 보낼 수 있습니다.

이 경우 사용자 이름과 비밀번호는 일치하지 않지만, ‘1’=’1’ 조건은 항상 참이 되므로 로그인 작업이 성공하게 됩니다.

Java: 방지 방법

예방을 위해서는 LoginValidation 코드를 수정하여 쿼리 실행 시 Statement 대신 PreparedStatement를 사용해야 합니다. 이렇게 변경하면 쿼리 내에서 사용자 이름과 비밀번호를 연결(concatenation)하지 않고 세터(setter) 데이터로 처리하게 되어 SQL 인젝션을 방지할 수 있습니다.

수정된 LoginValidation 코드는 다음과 같습니다:

String query = "select * from sample_data where username=? and password = ?";
Connection conn = null;
PreparedStatement stmt = null;
try {
    conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/user", "root", "root");
    stmt = conn.prepareStatement(query);
    stmt.setString(1, username);
    stmt.setString(2, password);
    ResultSet rs = stmt.executeQuery();
    if (rs.next()) {
       success = true;
       }
      rs.close();
      } catch (Exception e) {
         e.printStackTrace();
         } finally {
             try {
                 stmt.close();
                 conn.close();
            } catch (Exception e) {
            }
         }

이 경우 PreparedStatement와 세터, 그리고 기반이 되는 JDBC API가 사용자 입력을 처리하여 SQL 인젝션을 방지합니다.

예시

이제 다양한 언어에서의 예시를 통해 이것이 실제로 어떻게 구현되는지 더 자세히 살펴보겠습니다.

C# - 보안 취약

이 예시는 `FromRawSql`을 사용하기 때문에 보안에 취약합니다. 이 메서드는 매개변수를 바인딩하거나 이스케이프 처리를 시도하지 않습니다. 따라서 이 메서드는 어떤 상황에서도 사용을 피해야 합니다.

var blogs = context.Posts
    .FromRawSql("SELECT * FROM Posts WHERE state = {0} AND author = {1}", state, author)
    .ToList();

C# - 보안 안전

이 예시는 보간된 값을 가져와 매개변수화하는 `FromSqlInterpolated`를 사용하므로 안전합니다.

일반적으로는 안전하지만, 보안에 취약한 `FromRawSql`과 매우 유사하게 보일 위험이 있습니다.

var blogs = context.Posts
    .FromSqlInterpolated($"SELECT * FROM Posts WHERE state = {state} AND author = {author}")
.ToList();

Java - 보안 안전: Hibernate - Named Query + Native Query

Hibernate는 `Native Query`와 `Named Query`를 통해 안전하게 쿼리를 작성하는 두 가지 방법을 제공합니다. 두 방법 모두 매개변수 위치를 지정할 수 있습니다.

@NamedNativeQuery(
        name = "find_post_by_state_and_author",
        query =
        "SELECT * " +
                "FROM Post " +
                "WHERE state = :state" + 
         " AND author = :author",
        resultClass = Post.class)

java
List<Post> posts = session.createNativeQuery(
        "SELECT * " +
        "FROM Post " +
        "WHERE state = :state" +
        " AND author = :author" )
        .addEntity(Post.class)
        .setParameter("state", state)
        .setParameter("author", author)
        .list();

Java - 보안 안전: jplq

jplq 리포지토리 인터페이스에 `Query` 속성을 주석으로 추가하면 다양한 형태의 쿼리를 사용할 수 있으며, 모두 매개변수화됩니다.

@Query("SELECT p FROM Post p WHERE u.state = ?1 and u.author = ?2")
Post findPostByStateAndAuthor(String state, int author);
@Query("SELECT p FROM Post p WHERE u.state = :state and u.author = :author")
User findPostByStateAndAuthor(@Param("state") String state, @Param("author") int author);

Javascript - 보안: pg

`pg` 라이브러리를 사용할 때, `query` 메서드의 두 번째 인자로 매개변수 값을 전달하여 쿼리를 매개변수화할 수 있습니다.

const { posts } = await db.query('SELECT * FROM Post WHERE state = $1 AND author = $2', [state, author])

Javascript - 보안: Sequelize

`sequelize` 라이브러리는 쿼리 설정을 담은 두 번째 인자를 통해 쿼리를 매개변수화하는 방법을 제공합니다. 여기에는 이름이나 인덱스를 사용하여 쿼리에 바인딩할 값의 목록이 포함됩니다.

await sequelize.query(
    'SELECT * FROM Post WHERE state = $state AND author = $author',
    {
        bind: { state: state, author: author},
        type: QueryTypes.SELECT
    }
);
await sequelize.query(
    'SELECT * FROM Post WHERE state = $1 AND author = $2',
    {
        bind: [state, author],
        type: QueryTypes.SELECT
    }
);
태그라인

실무에 바로 적용하는 보안 코딩

OWASP Top 10과 같은 소프트웨어 취약점을 이해하고 완화하기 위한 보안 코딩 가이드라인을 살펴보고, 실제 앱 시뮬레이션을 통해 실습할 수 있는 가이드형 교육 미션을 시작해 보세요.

추가 가이드라인
데모 예약하기