들어가기 전에..
엘리스에서 배울 때 코치님께서 orm을 사용하지 않고 SQL문을 직접 써서 해보라고 하셨다.
typeORM을 쓸 줄 아는 수준으로는 mysql 쓴다고 보기가 어렵다고..
그리고 국비교육 수료한 이후 혼자 개발공부하면서 개인 프로젝트로 typeORM을 사용했는데,
솔직하게 SQL문에 대해 깊히 아는거보다 RDBMS 자체에 대해 공부하고 관계를 어떻게 설정하고,
효율성을 높힐 수 있는가가 중요한게 아닌가? 라는 생각을 했었다.
이번 주에 드디어 회사에 첫 출근을 하게 되면서 api를 구현하는데, 아무래도 신입이다보니
기존 다른 서비스에서 구현되어 있는 api를 그대로 똑같이 적용하는 부분을 맡겨주셨다.
모든 것이 처음이다보니 회사 깃을 받아서 하면 되는건지.. 내가 직접 구현을 해야하는건지
확실하지가 않아서 일단 내가 직접 짜면서 나중에 회사 깃을 받으면 바로 옮겨야겠다고 생각했고,
기존 서비스에 있는 api 명세서와 db를 대조하며 확인해서 구현했다.
발단
기능적인 부분은 금방 구현했고, 이후 인증과정이 필요한 기능에 대해서 확실히 테스트해야하는데
이미 로그인과정은 만들어져 있기에 내가 굳이 새로 구현할 필요는 없어보였다.
그래서 회사 깃을 부탁드려 받았더니 어색한 내용들이 정말 너무 많았다.
예를 들면 기존 하던 개인 프로젝트에 typeORM을 쓰기도 했고
나중에 nest.js로 마이그레이션하기 위해서 구조를 서비스단에서는 db에 대한 로직을 작성하고
sql관련된 부분은 별도의 리포지토리와 엔티티로 구분했는데
지금 받은 코드들은 sequelize를 쓰면서 서비스에 직접 db에 관한 로직과 같이 sql문이 같이 작성되어 있었다.
물론 개발자마다 선호하는 구조가 다를 수 있지만 엘리스에서 처음 프로젝트를 경험하고 난 이후
mvc패턴에 대해 익혀가며 db관련된 로직과 sql문을 분리해서 가독성에 대한 중요성을 알았는데,
처음 배우던 때로 돌아간 느낌이였다.
그래서 어느정도 다른 코드들에 대해 신뢰를 약간 잃은 상태다보니 아쉬운 점들이 보이기 시작했다.
왜 에러처리를 바로 throw하지 않고 try문 안에서 명시하고 컨트롤러로 미루지..?
왜 개발단계인데 error를 콘솔에서 디버깅하지 않는거지..?
winston을 써보지 않아서 서칭해보니 콘솔에서 확인할 수 있도록 세팅할 수도 있었다.
아니 적어도 console.error(error)를 하지 않는 것이 의아했다.
본론
슬슬 의심이 커져간다. 그리고 인증과정이 어떻게 진행되는지 확인하는데 이상한 점이 있었다.
sql문을 작성하면서 클라이언트에게 받은 uuid, email을 직접 입력해서 select하는 것이였다.
ex) SELECT * FROM USER ~~~ WHERE(${uuid} ~~ ) ~~
내가 처음부터 typeORM으로 하면서 sql문에 대해 몰랐다면 모르고 넘어갔을텐데
sql문을 익히도록 지도해주신 코치님께 정말 너무 감사하다.
user아니여도 모든 코드들에 전부 where, values 뒤에 템플릿 리터럴로 ${}가 보였다.
괄호 안에 ? 혹은 :params 값을 넣는 이유가 뭔지 처음 배울 때 인젝션공격에 당할 수 있다는데
그게 뭔지 ㅎㅎ 당연히 알리가 없었다. 사실 배우고도 그냥 그런갑다. 공격당한단다.
해커들이 대신 입력할만한 껀덕지를 준단다. 이렇게만 이해했다.
그런데 쿼리문을 보고 소름돋아서 설마 하고 uuid에 ' OR '1' = '1 을 입력하고 로그인했더니...
로그인이 되었다.... 이렇게 되면 로그인 뿐 아니라 저 뒤에 DROP TABLE이라던지
데이터를 아예 날려버리고자 한다면 충분히 하고도 남는 것이다.
아니, SELECT * FROM 을 해버리면 모든 데이터를 다 조회할 수도 있는 것이다.
해결방법
문제점은 무엇이였는가? SQL문에 입력값을 그대로 입력하는 것이다.
그렇기 때문에 입력값에 SQL문을 이용하면 명령어대로 다른 명령을 시도할 수 있는 것이다.
그렇다면 어떻게 해결해야겠는가? SQL문에 직접 SQL문을 넣을 수 없도록 해야 한다.
미리 쿼리문을 대신해서 채워놓아서 쿼리문을 입력하는 것이 아니라
입력값을 문자열로만 받도록 하는 것이다.
SELECT NAME, AGE FROM TABLE WHERE userID = ?
SELECT NAME, AGE FROM TABLE WHERE userID = :userID
여기서 궁금한 점은, ?이든 :에 변수를 채워넣든, 매개변수에 있는 값으로 대체하게 되면
결국엔 SQL문대로 입력하면 똑같은 것이 아닌가? 라는 점이다.
그렇지 않다. SQL문이 작동하는 방식은 간단하게는 다음과 같다.
Parse 구문 분석 - Execute 실행 - Fetch 인출
그리고 구문을 분석하고 실행하는 과정 중간에 sql 최적화, 코드로 포맷팅하는 과정이 있다.
구문을 분석하는 과정에서 ' or '1' = 1 이런식으로 논리를 대체한다던지,
테이블이나 특정 값을 알고 있다던지, 여러 경우를 통해서 parse이후의 내용을
공격자가 바꿔버리는 것이 지금의 내용이다.
그렇다면 parse 하는 과정에서 미리 값을 넣어놓고, 실행하기 전까지의 과정들을 처리하고
그저 입력값을 받으면 바로 실행할 수 있도록 대기하는 것이다.
이렇게 Prepared Statement를 넣어놓으면 SQL인젝션의 공격에도 방어할 수 있고,
쿼리의 성능을 개선하기 위해 미리 준비시킬 수도 있다.
'개발 > DB' 카테고리의 다른 글
[sequelize] Cannot delete property 'meta' of [object Array] (0) | 2023.10.29 |
---|---|
[mysql] view의 장단점, 성능에 대한 고찰 (0) | 2023.10.17 |
[sequelize] 서비스 - 모델 로직 분리, 리팩토링 과정 (0) | 2023.10.13 |
[sql] 다른 schema 간 DB table 복사방법 (0) | 2023.08.26 |
[TypeORM] Nest.JS 없이 Node.JS + TypeORM 기본 세팅 (0) | 2023.08.02 |