[Web Application-모의해킹] SQL Injection : 대응 방안 #3-1

2024. 4. 25. 16:32Project/Web Application-모의해킹

 

대응 방안 (feat.주요통신기반시설)

 

1) 점검 방법

 

  SQL Injection 취약점을 점검하는 방법은 다음과 같다.

 

  • 사용자 입력 값에 특수문자나 임의의 SQL 쿼리를 삽입하여 DB 에러 페이지가 반환되는지 확인
  • 사용자 입력 값에 임의의 SQL 참, 거짓 쿼리를 삽입하여 참, 거짓 쿼리에 따라 반환되는 페이지가 다른지 확인
  • 로그인 페이지에 참이 되는 쿼리를 전달하여 로그인되는지 확인

 

2) 보안설정방법

 

  SQL Injection 취약점에 대한 대응 방안은 다음과 같다.

 

  1. SQL 쿼리에 사용되는 문자열의 유효성 검증 로직 구현
  2. 시스템에서 제공하는 에러 메시지 및 DBMS에서 제공하는 에러 코드가 노출되지 않도록 예외 처리
  3. Prepared Statement

 

 

3) 대표적인 대응 방안

 

  • 사용자 입력 값 검증 로직 구현
  • 에러 메시지 출력 방지
  • Prepared Statement

 

 

사용자 입력 값 검증 로직 구현

 

  SQL Injection 취약점의 원리에 대해 알고 있다면 이를 방지하는 방법은 충분히 떠올릴 수 있다. SQL Injection 취약점은 사용자 입력 값에 임의의 SQL 쿼리를 삽입하여 공격자 의도에 맞는 SQL 쿼리 문이 완성되는 약점을 이용한 것이다. 이로 인해 개발자가 생각지 못한 SQL 쿼리문이 실행되어 DB를 비정상적으로 조작할 수 있게 된다. 이러한 SQL Injection 공격을 방지하기 위해서는 SQL 쿼리문의 변수가 되는 사용자 입력 값에 SQL 쿼리문을 구성하는 특수문자, SQL 구문, 함수 등의 문자를 검증하는 로직을 구현하면 된다.

 

1) SQL Injection 공격에 자주 사용되는 특수문자, SQL 구문, 함수

 

  SQL 쿼리문을 구성하는 특수문자, SQL 구문, 함수들은 다양하기도 하며 종류가 많지만 SQL 쿼리문을 성립하게 만드는 핵심적인 특수문자는 다음과 같다.

문자 설명
'(싱글 쿼터), "(더블 쿼터) 문자 데이터 구분기호
;(세미콜론) 쿼리 구분 기호
--, # 해당라인 주석 구분 기호
/* */ /*와 */ 사이 구문 주석

 

[1] '(싱글 쿼터), "(더블 쿼터)

 

  '(싱글 쿼터)와 "(더블 쿼터)는 SQL 쿼리문에서 문자 데이터를 구분하는 역할을 한다. 예시는 다음과 같다.

SELECT * FROM MEMBER WHERE NAME='김철수' OR NAME='홍길동'

 

  위 예시의 SQL 쿼리문을 보면 데이터 조회 시 조건에 해당하는 문자 데이터를 '(싱글 쿼터), "(더블 쿼터)로 구분을 한다. 그래서 SQL Injection 공격 구문 중 ' OR '1'='1으로 이루어진 구문을 자주 볼 수 있으며 이러한 구문이 적용된 SQL 쿼리문의 예시는 다음과 같다.

SELECT * FROM USER WHERE ID='XXX' OR '1'='1' AND PASS="XXX"

 

  사용자 입력 값 중 ID 파라미터에 XXX' OR '1'='1 구문을 입력하게 되면 개발자가 설정한 SQL 쿼리문과는 다르게 변조되어 비정상적인 작동을 한다. 

 

[2] ;(세미콜론)

 

  ;(세미콜론)은 SQL 쿼리문을 구분하는 기호로서 여러 개의 SQL 쿼리문을 사용할 때 주로 사용된다. 예시는 다음과 같다.

SELECT * FROM MEMBER WHERE NAME='홍길동';
SELECT * FROM USER WHERE ID='ADMIN';

 

  위 예시에는 두 SQL 쿼리문이 존재한다. 먼저 첫 번째 SQL 쿼리문은 MEMBER 테이블에서 NAME이 홍길동인 데이터를 조회하며 두 번째 SQL 쿼리문은 USER 테이블에서 ID가 ADMIN인 데이터를 조회한다. 이때 각 SQL 쿼리문 끝에는 ;(세미콜론)이 입력되어 있기 때문에 각 쿼리문을 구분할 수 있으며 첫번째 SQL 쿼리문 작동 후 두번째 SQL 쿼리문이 작동한다.

 

[3] --, #, /* */

 

  --, #, /* */ 특수문자들은 SQL 쿼리문에서 SQL 쿼리를 수행할 시 필요 없는 부분을 구분하는 주석 역할을 한다. 주석 역할을 하는 특수문자는 사용하는 DBMS에 따라 다르지만 한 줄 주석은 대부분 -- 혹은 # 을 사용한다. 예시는 다음과 같다.

SELECT * FROM GAME WHERE NAME='지뢰찾기' #AND SCORE='100000';
SELECT * FROM GAME WHERE NAME='지뢰찾기' --AND SCORE='100000';

SELECT * FROM USER WHERE NAME='홍길동' /*AND AGE='20';*/

 

  첫 번째, 두 번째 SQL 쿼리문이 작동하면 GAME 테이블에서 NAME이 지뢰찾기이고 SCORE가 100000인 데이터를 조회하게 된다. 그러나 AND 앞에 --, #와 같은 주석기호가 입력되어 있어 기호 다음으로는 SQL 쿼리문에 영향을 주지 않는다. 그로 인해 SCORE가 100000인 조건은 영향을 끼치지 못한다.

 

  세 번째 SQL 쿼리문이 작동하면 USER 테이블에서 NAME이 홍길동이고 AGE가 20인 데이터를 조회하게 된다. 그러나 AND AGE='20'; 구문 양옆에 /* */이 입력되어 있어 그 사이에 있는 AGE가 20인 조건은 영향을 끼치지 못한다.

 

 

2) 필터링 방식

 

  사용자 입력 값 검증 로직은 문자를 필터링하는 과정을 거치며 필터링 방식은 두 종류로 나뉜다.

 

[1] 블랙 리스트 기반 필터링

 

  • 블랙 리스트 기반 필터링은 특정 문자, 키워드, 특수문자를 제한하는 방식이다.
  • SQL 쿼리문을 구성하는 특수문자들(', ;, --, #, /* */), SQL 예약어들(UNION, SELECT)이 주 대상이다.
  • 리스트 된 문자나 키워드가 사용자 입력 값에 존재하면 공백 등으로 치환한다.
  • 블랙 리스트 기반 필터링은 자주 사용되는 필터링 방식이지만 정교하게 입력 값을 검증하지 않을 시 우회의 가능성이 많아지므로 유의하며 사용해야 한다.
  • 예를 들어 '(싱글 쿼터)가 리스트에 포함되어 있다면 '(싱글 쿼터) 대신 URL 인코딩으로 표현한 '%27' 같이 다른 종류의 인코딩 된 형태로 표현 가능하다. 또는 'UNION'이라는 문자가 리스트에 포함되어 있다면 'UNIUNIONON' 형태로 필터링 과정을 거쳐 'UNION'이란 문자를 사용하도록 우회가 가능하다.

 

[2] 화이트 리스트 기반 필터링

 

  • 화이트 리스트 기반 필터링은 허용된 문자를 제외한 나머지를 허용하지 않는 방식이다.
  • 허용된 문자 외에 나머지를 허용하지 않으므로 블랙 리스트 기반 필터링 방식보다는 보안성 측면에서 훨씬 더 강력한 효과를 지닌다.
  • 그러나 웹 애플리케이션의 기능에 따라 화이트 리스트를 작성해야 하기 때문에 번거로움이 존재한다.

 

 

3) 결론

 

  SQL Injection 취약점의 대응 방안으로 SQL 쿼리에 사용되는 문자열의 유효성 검증 로직은 SQL 쿼리를 구성하는 특수문자, SQL 구문, 함수 등을 필터링하는 방식으로 이뤄진다. 필터링에는 블랙 리스트 기반 필터링과 화이트 리스트 기반 필터링 방식이 존재한다. 블랙 리스트 기반 필터링은 특정 문자, 키워드 등을 제한하는 방식으로서 자주 사용되지만 우회의 가능성이 높아 보안성 측면에서 약한 효과를 가진다. 화이트 리스트 기반 필터링은 허용된 문자를 제외한 나머지를 허용하지 않는 방식으로 블랙 리스트 기반 필터링보다 우회의 가능성이 적어 보안성 측면에서는 훨씬 더 강한 효과를 가진다. 그러나 웹 애플리케이션의 기능에 따라 번거로움이 존재한다. 보안적인 측면에 중점을 두고자 한다면 번거롭더라도 사용자 입력 값 검증 로직은 화이트 리스트 기반 필터링 방식을 사용하여 구현하도록 한다.

 

 

에러 메시지 출력 방지

 

  SQL Injection 공격 중 Error Based SQL Injection 공격이 존재한다. 해당 공격은 DBMS 에러를 의도적으로 유발해 출력되는 DBMS 에러 메시지 내에 원하는 SQL 쿼리의 결과를 출력하는 방식이다. 이를 방지하기 위해서는 에러 발생 시 출력되는 에러 메시지를 출력하지 않도록 설정해야 한다.

 

 

Prepared Statement

 

  SQL 쿼리는 DB로 전달되어 처리될 때 DBMS 내부적으로 몇 단계 과정을 거치게 되는데 이는 전달되는 SQL 쿼리의 종류에 따라 다르다. 이 중에서 SELECT 구문의 처리 과정에 대해서 알아보도록 하자.

 

 

1) SELECT 구문 처리 과정

 

  SELECT 구문은 DB로 전달된 후 Parse, Bind, Execute, Fetch 이렇게 네 과정을 거쳐 처리된다.

 

SQL 쿼리 처리 과정

 

  SQL 쿼리가 DB에 전달되면 먼저 Parse 단계를 거치게 된다. Parse 단계에서는 전달된 SQL 쿼리가 이상이 없는지 확인하고 메모리에 저장하는 과정이다. Bind 단계에서는 SQL 쿼리에 변수가 존재하지 않으면 바로 Execute 단계로 넘어가고 변수가 존재하면 변수에 실제 값을 바인딩해주는 단계이다. Execute 단계에서는 SQL 쿼리를 실행시켜 데이터를 조회하고 마지막으로 Fetch 단계에서는 실행시킨 SQL 쿼리의 데이터를 정렬한 후 반환하는 단계이다.

 

 

2) Parsing 단계

 

  Parsing 단계는 문법 검사, 의미 검사, 권한 검사, 실행 계획 순서로 진행된다. 문법 검사는 SQL 쿼리가 문법적으로 오류가 없는지 확인하는 작업이며, 의미 검사는 SQL 쿼리에서 사용된 테이블, 컬럼이 존재하는지 확인한다. 권한 검사는 SQL 쿼리를 전달한 사용자가 쿼리에서 사용된 테이블과 컬럼을 사용할 수 있는 권한이 존재하는지 확인하는 작업이며, 실행 계획은 해당 SQL 쿼리가 DBMS의 공유 풀 내 라이브러리 캐시에 저장되어 있는지 확인하여 존재하면 다음 단계로 넘어가고 존재하지 않으면 SQL Processing 작업 진행 후 재사용을 대비하여 해당 SQL 쿼리를 공유풀 내 라이브러리 캐시에 저장한다.(이 작업을 최적화 단계라고도 한다.) 이때 SQL Processing 작업을 진행하지 않고 다음 단계로 넘어가는 것을 Soft Parsing이라 하며 SQL Processing 작업까지 다 하는 것을 Hard Parsing이라 한다.

 

Parsing 단계 순서

 

  SQL 처리 과정은 기본적으로 Hard Parsing까지 진행이 된다. 그러나 매번 Hard Parsing까지 진행을 하게 되면 리소스를 많이 사용하게 되어 결과적으로 비효율적인 성능을 가지게 된다. 그래서 새로운 SQL 쿼리를 처리할 때마다 Parsing 단계 마지막에 공유풀 내 라이브러리 캐시에 저장을 하고 동일한 SQL 쿼리가 요청될 때마다 해당 쿼리가 라이브러리 캐시에 존재하는지 확인하여 존재하면 SQL Processing 작업을 스킵하여 불필요한 작업을 하지 않도록 한다. 이때 SQL 쿼리 처리 과정 중 Parsing 단계를 Hard Parsing까지 진행을 하면 Statement, Soft Parsing까지 진행을 하면 Prepared Statement라고 한다.

 

 

3) Statement VS Prepared Statement

 

  Statement 작동 방식과 Prepared Statement 작동 방식은 SQL 쿼리를 요청했을 때 공유풀 내 라이브러리 캐시에 동일한 SQL 쿼리 존재 여부에 따라 달라진다. DBMS의 성능 향상을 위해서라면 어떻게든 Prepared Statement 작동 방식으로 처리하는 것이 좋지만 이는 SQL 쿼리가 어떻게 구성되어 있는지에 따라 다르다.

 

  일반적으로 SQL 쿼리는 변수를 포함하며 해당 변수는 SQL 쿼리가 DBMS에 전달되기 전에 결정된다. 예시는 다음과 같다.

 

SQL 쿼리 유형 (1)

 

  SQL 쿼리가 예시와 같이 구성되어 있다면 동일한 SQL 쿼리라도 $userID 변수에 따라 DBMS에 전달되는 SQL 쿼리가 매번 달라져서 Hard Parsing까지 진행하는 Statement 작동 방식으로 처리가 된다. 이와 같이 SQL 쿼리에 변수가 필요하지만 DBMS에 동일한 SQL 쿼리를 전달하여 Prepared Statement 작동 방식으로 처리하기 위한 방법으로 변수 자리에 바인드 변수인 '?'를 사용하도록 한다. 그렇다면 바인드 변수에는 사용자 입력 값이 언제 포함될까? 바로 Parsing 단계가 모두 끝난 다음 단계인 Bind 단계에서 포함된다. 예시는 다음과 같다.

 

SQL 쿼리 형식 (2)

 

  SQL 쿼리가 예시와 같이 구성되어있다면 SQL 쿼리를 DBMS로 전달할 때 항상 'SELECT * FROM USER WHERE ID = ?' 형식으로 전달되기 때문에 Soft Parsing까지 진행하는 Prepared Statement 작동 방식으로 처리가 되고 Parsing 단계 후에 Bind 단계에서 바인드 변수 '?'에 실제 값이 바인딩된다. 이와 같이 Prepared Statement 작동 방식을 통해서 DBMS 성능 향상을 원한다면 SQL 쿼리 내 변수 자리에 바인드 변수인 '?'를 사용하여 SQL 쿼리를 구성해야 한다.

 

 

4) SQL Injection 대응 방안

 

  SQL 쿼리 처리 과정 중 Parsing 단계를 거치게 되면 SQL 쿼리는 컴파일된 상태이기 때문에 이후 Bind 단계에서 바인딩되는 사용자 입력 값에 SQL 구문이 있더라도 이는 문법적인 의미를 가지지 못하여 SQL 쿼리를 변조시킬 수 없다. 그래서 SQL Injection 공격을 방지할 수 있는 것이다. 성능 향상을 위한 목적으로 개발된 Prepared Statement 작동 방식이 의도치 않은 곳에서 활약을 하게 되었으며 이는 SQL Injection 공격을 방지하기 위한 최선의 방법으로 알려지게 되었다.