sql injection

목표

MyBatis에서 사용하는 #과 $의 차이에 대해서 배워 봅시다.

SQL Injection

#과 $의 차이

<!-- 페이지 적용 -->
<select id="pageList" resultType="hashmap" parameterType="hashmap">
<![CDATA[
      SELECT b.num, b.title, u.name, date_format(b.date, '%Y-%m-%d') as date, b.count, b.id
      FROM b_board as b
      INNER join b_users as u
      ON (b.id = u.id)
      WHERE del_chk = 'N'
      ORDER BY num DESC
      LIMIT #{pageStart}, #{perPageNum}
  ]]>
</select>

위 코드를 보면 #{…} 방식으로 값을 받아와서 사용합니다. 실무에서는 오직 #만을 사용합니다.
그 이유는 아래 특징들을 보면 알 수 있습니다.

a6

  • # { }
    • 파라미터가 String 형태로 들어와 자동적으로 파라미터 형태가 됩니다.
    • EX) #{id}의 id값이 xyz일 경우, Query문의 형태는 id = ‘xyz’ 형태가 됩니다.
    • 쿼리 주입을 예방할 수 있어 보안측면에 유리합니다.

SQL Injection(쿼리 주입)

SQL 인젝션(SQL 삽입, SQL 주입으로도 불린다)은 코드 인젝션의 한 기법으로 클라이언트의 입력값을 조작하여 서버의 데이터베이스를 공격할 수 있는 공격방식을 말한다. 주로 사용자가 입력한 데이터를 제대로 필터링, 이스케이핑하지 못했을 경우에 발생한다. 공격이 쉬운 데 비해 파괴력이 어마어마하기 때문에 시큐어 코딩을 하는 개발자라면 가장 먼저 배우게 되는 내용이다. 이러한 injection 계열의 취약점들은 테스트를 통해 발견하기는 힘들지만 스캐닝툴이나 코드 검증절차를 거치면 보통 쉽게 발견되기 때문에 탐지하기는 쉬운 편이다. 보안회사 Imperva가 2012년에 발표한 보고서에 따르면 월평균 4회가량의 SQL 인젝션 공격이 일어난다고 한다. OWASP에서도 수년 동안 인젝션 기법이 보안 위협 1순위로 분류되는 만큼 보안에 각별한 주의가 필요하다.

  • $ { }
    • 파라미터가 바로 출력된다.
    • 해당 컬럼의 자료형에 맞추어 파라미터의 자료형이 변경됩니다.
    • 쿼리 주입을 예방할 수 없어 보안측면에 불리하다. 따라서 사용자 입력을 전달해야 하는 경우에는 사용하지 않는게 좋습니다.
    • 테이블이나 컬럼명을 파라미터로 전달하고 싶을 때 사용합니다.
    • #{ }는 자동으로 ' '가 붙어서 이 경우에는 사용할 수 없습니다.

공격 방법 1

데이터베이스에 값을 조회하기 위해 사용되는 언어를 SQL이라고 하며 다음과 같이 생겼다고 가정합니다.

   SELECT user FROM user_table WHERE id='입력한 아이디' AND password='입력한 비밀번호';

일반적인 유저라면 id : 닌자 pw : 거북이 이런 방식으로 로그인 할 것입니다.

   SELECT user FROM user_table WHERE id='닌자' AND password='거북이';

이때 악의적인 유저가 다음과 같이 입력했습니다.

   SELECT user FROM user_table WHERE id='admin' AND password=' ' OR '1' = '1';

비밀번호 입력값과 마지막 구문을 자세히 살펴봅시다. 따옴표를 올바르게 닫으며 password=’ ‘를 만들어 버림과 동시에 SQL 구문 뒤에 OR ‘1’ = ‘1’을 붙였다. WHERE 뒤에 있는 구문을 간단히 축약하면 false AND false OR true로 정리할 수 있는데, 논리학에 따르면 AND 연산은 OR보다 연산 우선순위가 빠르기 때문에 해당 구문 전체는 true 가 되어 올바른 값으로 판단하고 실행하게 됩니다. 즉 아이디와 비번을 제대로 매치하지 못했어도 OR ‘1’ = ‘1 구문 때문에 로그인에 성공하게 되는 것입니다.

공격 방법 2

로그인 폼도 결국엔 서버에 요청을 해서 받는 것입니다. HTTP 헤더를 보면 응답 헤더에 서버의 종류와 버전이 나옵니다. Apache 서버는 MySQL 서버, IIS는 MS SQL 같은 방식으로 데이터베이스의 종류를 추측할 수 있습니다. DB엔진을 알아내서 해당 시스템에 맞는 명령어를 이용해 데이터를 뽑아내거나 할 수 있습니다.

블라인드 SQL 인젝션

에러 메시지가 정보가 아무런 도움이 되지 않거나 아예 에러 페이지를 보여주지 않을 때 사용합니다. 대표적인 기술로는 시간 지연 공격이 있습니다. 간단하게 몇 초 정도의 time for delay를 이용해 원하는 시간 만큼 데이터베이스가 움직여 준다면 취약하다고 볼 수 있습니다.

방어 방법

아마도 XSS와 상당 부분 겹치겠지만 기본적으로 유저에게 받은 값을 직접 SQL로 넘기면 안 됩니다. 요즘에 쓰이는 거의 모든 데이터베이스 엔진은 유저 입력이 의도치 않은 동작을 하는 걸 방지하는 escape 함수와 prepared statement를 제공합니다. prepared statement 자체 내에 escape가 내장돼서 한 겹 감싸진 형태를 말합니다.

또한 DB에 유저별로 접근 권한과 사용 가능한 명령어를 설정하면 최악의 경우에 SQL injection에 성공하였다고 하더라도 그나마 피해를 최소화할 수 있습니다. 혹자는 SQL injection은 데이터베이스 스키마를 알아야 가능한 공격기법이라고 하지만 스키마 구조를 몰라도 SQL injection을 사용하면 스키마 구조를 알아낼 수가 있습니다. 그리고 데이터베이스를 변조하려는 게 아니라 파괴하려는 거라면 와일드카드 문자( * )를 사용해서 그냥 싹 다 지워버리는 공격이 가능합니다.

DB엔진별로 문법이 다 다르기 때문에 개발자가 그걸 다 고려해서 코딩하는 방법은 매우 비추천. 엔진에서 제공하는 prepared statement를 사용하는 게 최선. escape_string 같은 함수를 사용하면 몇몇 군데에서 빼먹거나 하는 실수로 보안 구멍이 생길 수 있다. 그리고 prepared statement는 사용 전에 일부 컴파일돼서 DB쿼리를 가속시켜주므로 적극적으로 사용하는게 좋습니다. 다만 컴파일하는 시간이 있다 보니 변수만 다른 같은 쿼리를 반복적으로 하는 작업에서야 유의미한 속도 향상이 있습니다. 프로시저(Procedure)라는 쿼리 캡슐화 기능도 쓰면 좋습니다.

추천되는 방어법은 클라이언트 측의 입력을 받을 웹 사이트에서 자바스크립트로 폼 입력값을 한 번 검증하고, 서버 측은 클라이언트 측의 자바스크립트 필터가 없다고 가정하고 한 번 더 입력값을 필터합니다. 이때 정규표현식으로 필터하는게 가장 강력하고 좋습니다. 그다음 SQL 쿼리로 넘길 때 해당 파라미터를 prepared statement로 입력받고 이를 프로시저로 처리합니다. 다음, 쿼리의 출력값을 한 번 더 필터하고(XSS 공격 방어의 목적이 강하다) 유저에게 전송합니다. 이렇게 하면 해당 폼에 대해서는 SQL injection 공격이 완전히 차단됩니다. 물론 이것이 공격 기법의 전부가 아니므로(스푸핑이나 맨 인 더 미들 공격, 키 로거 등) 정보 유출에 민감한 사이트를 운영할 생각이라면 보안 회사에 컨설팅을 꼭 받아야 합니다.

참조

https://namu.wiki/w/SQL%20injection