1. SQL Injection 취약점이란?
사용자 입력 데이터 값에 대해 검증 절차 미흡으로 인해 특정 SQL 명령어를 삽입하거나
변조할 경우 Database 내의 data를 조회할 수 있는 취약점을 말한다.
2. 취약점 예시
OWASP Juice Shop으로 로그인 후, 메인 페이지를 접근하면
특정 URL 뒤에 [q] 파라미터를 전달하는 요청 패킷이 history에 남는 것을 볼 수 있다.

해당 파라미터에 임의의 값을 넣어보기도 하고 Jusice Shop에서 판매하는 품목 이름을
무작위로 입력해보니 응답 패킷이 돌아오는 것을 볼 수 있었다.

추가적으로 임의의 특수문자 값을 삽입하니 SQLite error 구문이 반환되는 것을 통해
해당 파라미터 내에 SQL injection을 구현하면 공격 가능한 부분이 있을 것 같아보인다.

현재 [q] 파라미터 내에 입력된 값이 SQL query 문의 name 이라는 column 내에서 값을 받고 있으며
이 때 like 문을 통해 해당 값이 들어간 모든 구문에 대해 검색이 가능한 query문이 들어간 것을 볼 수 있다.
그렇다면 UNION SQL injection 구문을 통해 SELECT query문 뒤에 추가적인 구문을 이어넣어보도록 하자
위의 SQLite error 구문을 통해 사용하고 있는 DB table 이름이 Products 인 것을 알 수 있고
현재 table 내 column의 개수가 몇개인지 파악되지 않은 상태이므로 UNION SELECT 구문 내에
column 값을 무작위로 삽입하여 응답 패킷의 결과를 확인해본다.
apple'))UNION%20SELECT%201,2,3,4,5,6,7,8,9%20FROM%20Products--

위의 예시를 바탕으로 현재 연결된 데이터베이스에 존재하는 모든 table, index, view에 대한 정보를
하나의 특수한 시스템 테이블에 저장하는 sqlite_matser table의 정보를 확인해보도록 한다.
[sqlite_master 주요 column]
- type: 저장된 객체의 종류
- name: 객체의 이름
- tbl_name: 객체가 속한 테이블 이름
- sql: 객체 생성 시 사용된 CREATE 문
apple'))UNION%20SELECT%20sql,type,name,tbl_name,5,6,7,8,9%20FROM%20sqlite_master--

위에서 노출된 정보들을 바탕으로 사용자 table에 저장되 사용자 목록 정보들을 열람할 수 있는 것을 확인했다.

3. 조치방안
취약한 code 확인
export function searchProducts () {
return (req: Request, res: Response, next: NextFunction) => {
let criteria: any = req.query.q === 'undefined' ? '' : req.query.q ?? ''
criteria = (criteria.length <= 200) ? criteria : criteria.substring(0, 200)
models.sequelize.query(`SELECT * FROM Products WHERE ((name LIKE '%${criteria}%' OR description LIKE '%${criteria}%') AND deletedAt IS NULL) ORDER BY name`)
.then(([products]: any) => {
const dataString = JSON.stringify(products)
for (let i = 0; i < products.length; i++) {
products[i].name = req.__(products[i].name)
products[i].description = req.__(products[i].description)
}
res.json(utils.queryResultToJson(products))
}).catch((error: ErrorWithParent) => {
next(error.parent)
})
}
}
현재 취약한 부분은 SELECT query문 내에 '%${criteria}%' 부분이라는 것을 알 수 있다.
최종 조치 code
export function searchProducts () {
return (req: Request, res: Response, next: NextFunction) => {
let criteria: any = req.query.q === 'undefined' ? '' : req.query.q ?? ''
criteria = (criteria.length <= 200) ? criteria : criteria.substring(0, 200)
models.sequelize.query(
`SELECT * FROM Products WHERE ((name LIKE '%:criteria%' OR description LIKE '%:criteria%') AND deletedAt IS NULL) ORDER BY name`,
{ replacements: { criteria } }
).then(([products]: any) => {
const dataString = JSON.stringify(products)
for (let i = 0; i < products.length; i++) {
products[i].name = req.__(products[i].name)
products[i].description = req.__(products[i].description)
}
res.json(utils.queryResultToJson(products))
}).catch((error: ErrorWithParent) => {
next(error.parent)
})
}
}
위의 코드를 보면 Sequelize의 내장된 바인딩 메커니즘을 사용하여, DB에 query를 실행하기 전에
query 구조를 미리 템플릿처럼 정의하여 전송할 수 있게 된다.
다시 말해, 입력 받는 파라미터 내에 입력 구조를 선제적으로 구조화하는 것을 뜻한다. (Prepared Statement)
위의 {replacements: { criteria }} 라고 추가된 code를 통해 Sequelize는 이 입력값을 SQL 구문이 아닌,
순수 data로만 처리하도록 DB에게 전달하므로 사용자의 입력 내 SQL query문이 전달되는 경우를 차단할 수 있다.
'취약점 진단' 카테고리의 다른 글
| OWASP Juice Shop Mass Assignment (0) | 2025.12.11 |
|---|---|
| OWASP Juice Shop [Redirect 취약점] (0) | 2025.12.11 |
| OWASP Juice Shop [정보누출 취약점] (0) | 2025.12.11 |
| Docker 사용법 (0) | 2025.12.11 |
