회사에서 프로시저를 만들다가 동적으로 테이블을 받고 싶어졌다.
왜냐하면 때에 따라 조회해야 할 테이블들이 달라지고 그 테이블의 갯수도 한번에 7-9개 정도로 많았다.
만약에 카테고리로만 나눈다면 나는 9개 테이블 x 3번이니 27개의 테이블을 프로시저 내에 박제해야하는 상태..
이게 최선인가? 하는 생각이 계속 들었고, 동적으로 받아와야겠다! 라고 결심을 한 상태
그런데 이전에 동적 쿼리를 이용하면 SQL 인젝션에 취약해진다고 들은 적이 있다.
그렇다면 그 인젝션을 방지할 수 있도록 짠다면 동적 쿼리를 사용해도 되지 않을까? 라는 생각에
SQL 인젝션에 대해 자세히 알아보고, 대응법이 있다면 그걸 최대한 활용해볼겸 포스팅하게 되었다.
- 보안상의 취약점을 이용하여, 임의의 SQL문을 주입하고 실행되게 하여 DB가 비정상적인 동작을 하도록 조작하는 행위
- OWASP TOP 10 중 첫 번째에 속해 있으며, 공격이 비교적 쉬운 편이고 공격에 성공할 경우 큰 피해를 입을 수 있다.
즉, 웹 페이지의 로그인 창 등에 SQL 구문을 넣어 데이터를 빼내거나 웹을 변조해 악성코드를 유포하는 해킹 수법
웹 어플리케이션은 User의 행동(클릭, 입력 등)으로 데이터를 변경한다.
사용자가 입력한 외부 입력값이 SQL 구문의 일부로 사용되는데
이때 입력된 데이터에 대한 유효성 검증을 하지 않아 발생하는 취약점이다.
필터링 과정없이 넘겨받아 동적 쿼리를 생성하여 개발자가 의도하지 않은 쿼리가 생성되게 된다.
동적으로 생성되는 쿼리에 SQL 구문의 주석을 의도적으로 삽입하여 WHERE 조건을 무력화시킬 수 있다
정상적인 로그인은 위에서 입력받은 값을 가져와 아래의 쿼리로 실행된다.
SELECT * FROM USER
WHERE ID = 'admin' AND PASSWORD = '보안365'
그렇다면, 아래와 같이 입력하게 되면 어떻게 될까?
SELECT * FROM USER
WHERE ID = 'admin'--AND PASSWORD = '보안365'
ID 조건을 강제로 닫아버리고, 그 이하의 내용이 주석으로 처리되어졌다.
이렇게 되면 admin이라는 ID가 실제로 있는 유저가 있을 시
비밀번호 확인 없이 로그인이 가능해져버렸다.
위와 같이 입력을 받게 된다면 웹 어플리케이션은 아래의 쿼리를 생성한다.
SELECT * FROM USER
WHERE (ID = '1' AND PASSWORD = '1') OR ('1' = '1')
놀랍게도 로그인이 된다.
SQL 구문에서 AND 연산이 OR연산보다 우선한다는 특성으로
OR 뒤 (1=1) 구문으로 해당 쿼리는 항상 참인 쿼리가 되어버렸다.
이렇게 된다면 이 서비스의 로그인은
ID와 비번을 아무거나 입력 후 로그인하면 모든 유저 정보가 조회되는 서비스가 되어버렸다.
로그인이 된다는 것도 문제인데, 이 간단한 쿼리로 해당 서비스의 모든 유저 개인 정보를 탈취할 수 있는 상황이 되어버렸다.
말 그대로 직접 데이터베이스를 조작하는 쿼리문을 삽입하여 의도하지 않은 명령어를 수행시키는 방법이다.
이게 무슨 말이지? 아래의 이미지를 보자.
INSERT INTO
BOARD(id, contents)
VALUES('ㄴㅇㄹㄴㅇㄹ', '''); DELETE FROM BOARD --)
위 쿼리처럼 '); 이부분으로 INSERT문을 종료하고
DELETE 문을 수행 후 필요없는 쿼리들은 맨 뒤 -- 주석으로 막아버렸다.
이렇게 되면 CONTENTS에는 '' 공색값이 들어간 후 BOARD 테이블에 있는 모든 데이터를 제거하게 된다. ㄷㄷ....
SQL 인젝션 ,,, 마냥 먼 얘기인 줄 알았는데 위 방법들을 보니까 남일 같지 않은 것 같다.
다시 한번 보안의 중요성을 또 깨달았다.
가장 기본적인 대응 전략이라고 한다.
사용자가 입력한 외부 입력값에 대하여 항상 Validation 체크를 진행하는 방법이 있다
어떻게?
1. SQL 쿼리의 구조를 변경시키는 문자나 키워드, 특수문자 제한
SQL 예약어인 UNION, GROUP BY, COUNT() 등의 함수명, 세미콜론(;), 주석(--) 등의 특수문자들을 제한하고,이러한 키워드가 있을 시 공백이나 다른 문자로 치환하여 방어한다.
2.이러한 경우도 생각해서 정교하게 입력값을 체크하자!
SESELECTLECT
중간 SELECT라는 단어를 체크해서 공백으로 치환하고 있다고 치자.
SESELECTLECT -> 좌우 문자들이 합쳐져서 다시 SELECT 키워드가 완성된다.
이런 경우도 체크해서 방지해야한다.
위와 반대로 허용된 문자를 제외하고는 모두 막는 방법이다.
블랙리스트 방식은 예상된 문자만 방지할 수 있는 반면에
화이트 리스트 방식은 예상치 못한 문자들도 모두 막을 수 있어 보안성 측면에서는 훨씬 더 강력한 방법이다.
prepared Statement를 활용하면 쿼리의 문법 처리 과정이 미리 컴파일 되어 있기 때문에
외부 입력값으로 SQL 관련 구문이나 특수문자가 들어와도 그것은 SQL 문법으로서 역할을 할 수 없게 된다.
SELECT * FROM USER
WHERE ID = ? AND PASSWORD = ?
위 쿼리와 같이 어떤 값이 들어갈지 물음표로 대체 되어있음
preparedStatemen는 이 부분을 미리 컴파일 하고 후에 이미 쿼리 부분은 컴파일 완료 후
데이터를 바인딩하기 때문에 데이터는 SQL 문법으로서의 역할을 할 수 없다.
그래서 추후 데이터에 쿼리 관련 구문이 들어와도 쿼리가 될 수 없다.
로그인 실패 시
'패스워드가 일치하지 않습니다' 의 문구는
다른 말로 ID는 일치한다는 말이 되어 악의적인 사용자에게 ID를 노출시키는 상황이 되어버린다.
위와 같이 구체적인 오류 문구 보다는 '로그인 실패하였습니다' 와 같은 추상적인 메세지가 안전하다.