회사에서 프로젝트를 진행하면서 SQL Injection 이라는 공격에 처음으로 당해보았다.
다행히도 아직 오픈 전이라 십년감수했다.. ^ 3^
그래서 이번 주제는 SQL Injection은 뭔지? 그리고 MyBatis, 쿼리 문에서 발생할 수 있는 Injection 사례를 알아보고 해결방안을 제시하려고 한다.
SQL Injection?
SQL Injection하면 엄청나게 유명한 짤이 있다.
이 만화 컷 하나로 Injection의 모든 것을 이해할 수 있다.
요 만화는 대충 이렇다. 어머니가 사랑스러운 아들 이름을 Robert'); DROP TABLE Students;-- 라고 지었다.
학교 측에서는 신입생의 이름을 바탕으로 Students Table에 추가하려고 한다.
1
|
INSERT INTO Students (name) VALUES ('student name');
|
그렇다면 Robert'); DROP TABLE Students;-- 이라는 킹받는 이름을 VALUES에 넣어보자
1
|
INSERT INTO Students (이름) VALUES ('Robert');DROP TABLE Students;--');
|
이런식으로 쿼리 문이 구성될 것이다. Robert 이름 하나를 넣고 Students 테이블을 날려 모든 학생 정보를 파기해버렸다.
학교는 과연 로버트드랍테이블스튜던트의 어머니한테 손해배상을 청구할 수 있을까?? (당연히 불가능)
SQL Injection을 한 줄로 정의하자면, 클라이언트의 입력 값을 조작하여 서버의 데이터베이스를 공격하는 방식을 의미한다.
한국에서 이 공격이 유명해진 이유는 '여기어때'에서 Injection 방식을 통해 회원들의 개인정보를 탈취당했기 때문이다. (여기어때 안티아닙니다❤️, 참고기사: https://www.hankyung.com/article/2017042738721)
MyBatis에서 발생할 수 있는 Injection
현재 프로젝트에서는 SqlSessionFactory에서 SqlSession을 열어서 사용하는 방식이 아니라, xml을 사용하는 방식으로 프로젝트를 진행하고 있다. 그래서 변수를 주입할 때, $ 또는 # 기호를 많이 사용하고 있다.
$의 경우 문자 그대로가 삽입되는 Direct Variable Injection 방식으로, 테이블 명이나 테이블 column을 동적으로 가져올 때 많이 사용한다. 예를 들어
1
2
3
4
5
6
7
8
9
|
SELECT id
, name
FROM ${tableName}
WHERE id = 3;
SELECT id
, name
FROM Stduents
WHERE ${column} IS NOT NULL;
|
이렇게 사용할 수 있다. $방식의 경우, #과 다르게 문자열('')로 감싸지 않아 강력하지만 Injection에 취약한 방식이라 할 수 있다.
#의 경우 문자열('')로 감싸서 사용하므로 Prepared Statement 방식이라고 불린다. 이는 SQL 쿼리에 안전하게 값을 바인딩하기 위한 방법이다. 예를 들어
1
2
3
4
|
SELECT id
, name
FROM Students
WHERE id = #{stduentId}
|
이렇게 사용할 수 있다. $방식과 달리, 문자열로 감싸기 때문에 Injection 방어가 가능하다.
실제로 $를 사용하여 문제가 발생했던 쿼리문을 소개하고 이를 어떻게 개선했는지 설명하겠다!
1
2
3
4
5
6
|
SELECT shipping_product_id
, customer_name
, quantity
FROM shipping_products AS s
JOIN products AS p ON s.product_id = p.product_id
WHERE p.product_name = ${productName} AND p.product_status = 1
|
이런 SQL 구문이 있었다. product_name을 바탕으로 출고상품을 조회하는 것인데, 이 때 가장 중요한 부분은 지금 사용 가능한 즉, 활성화 된 상품만 검색 가능해야 한다.
예를 들어, 상품 이름을 위 이미지처럼 '' OR 1=1 -- 이라고 검색하면 어떻게 될까?
1
2
3
4
5
6
|
SELECT shipping_product_id
, customer_name
, quantity
FROM shipping_products AS s
JOIN products AS p ON s.product_id = p.product_id
WHERE p.product_name = '' OR 1=1 -- AND p.product_status = 1
|
활성화된 상품 뿐만 아니라 비활성화된 상품 모두를 확인할 수 있다. 이를 통해, 사용자는 비활성화된 상품으로 작업을 수행할 수 있다.
이를 해결하기 위해서는 심플하다. ${productName} -> #{productName} 으로 변경만하면 된다.
LIKE SQL Injection
사실 앞에 예제들은 이제는 일상속에서 많이 발견할 수 없다. MyBatis 공식문서에서도 $의 사용에 대한 위험성을 알려주고 있고 코드 리뷰를 통해 발견할 수 있을 문제이다.
근데 이번건 좀 신기하다. 품질테스트 팀에서 발견해준 이슈인데, 아래 예제를 통해 설명하겠다.
이런 화면이 있고 가정하겠다. 전체검색 결과 총 4개의 상품을 확인할 수 있다. 품질 팀에서 킹명주 원빈 확률 100% 상품을 검색하기 위해 검색창에 %를 입력했다.
품질팀: 킹명주님 %를 검색했는데, 전체가 검색되어요.. 제가 잘못한건가요?
킹명주: 흠.. 그럴리가 없는데,, 쿼리를 살펴볼게요.
하고 쿼리문을 살펴보았다.
1
2
3
4
5
6
7
8
9
10
11
|
SELECT product_id
, product_name
, supply_name
, quantity
, expired_date
, shipping_status
FROM products AS p
WHERE ...
<if test = "productName != null and productName != ''">
AND product_name LIKE CONCAT('%', #{productName}, '%')
</if>
|
이렇게 되어있었다. 그런데 생각해보니 %로 검색하는 경우product_name LIKE '%%%' 꼴이 되는 것이였다.
즉, LIKE의 와일드카드(wildcard)인 % 그리고 _ 에서 문제가 발생했다.
(**참고: %는 0개 이상의 임의의 문자를 나타내는 것이고 _는 정확히 1개의 임의의 문자를 나타낸다.)
즉 위 예제에서 다른 와일드카드인 _ 검색을 해도
실제 쿼리에서는 product_name LIKE '%_%'로, 하나 이상의 문자가 포함되어 있는 결과 전체를 보여줄 것이다.
이를 완전히 막기 위해서는 service layer에서 처리하거나, 쿼리문에 ESCAPE 문자열을 지정하는 방법이 있다.
내용이 너무 길것 같아서 구체적인 해결방법에 대해서는 다음 글에 나타내려고 한다. 이렇게만 끝내면 독자들의 원성이 자자하니 우리회사는 과연 LIKE Injection에 잘 대처했을까? 라는 도파민 중독 컨텐츠를 준비해보았다.
우리 회사의 서비스는 과연?
회사에서 검색엔진이 중요한 서비스들을 위주로 테스트를 진행해보았다.
첫 번째로 대표 서비스인 그룹웨어 서비스에서 메일 검색을 해보았다.
확실히 전통있는 기업이라 문제의 원인을 없애는 군대식 일처리를 수행했다. 이유가 궁금하지만 어찌되었든 Injection 방어 성공~
두번째는 우리 회사의 대표적인 쿠폰 서비스에서 상품 검색을 해보자!
전체 검색 결과가 3086 건이다.
그만 알아보자.
오늘의 결론
오늘은 2가지 Injection에 대해 알아보았다.
다음 글은 LIKE SQL Injection을 킹명주는 어떻게 막았는지? 여러가지 방법을 소개하려고 한다.
엄청나게 유용하기 때문에 꼭 재방문 부탁드리면서 글을 마무리하겠다.
'ETC' 카테고리의 다른 글
[정적 코드 분석] sonarQube 사용해보기 (4) | 2023.08.01 |
---|---|
소프트웨어 마에스트로 13기 회고 (11) | 2023.01.30 |
킹명주의 일기장을 개발했다리.. (2) | 2022.12.28 |