설명
SQL 인젝션은 사용자 입력이 파라미터 바인딩 없이 쿼리 문자열에 직접 결합될 때 발생합니다. DB는 해당 입력을 단순한 값이 아닌 SQL 구문으로 해석할 수 있어, WHERE 절 조작, UNION 주입, 서브쿼리/함수 호출 등이 가능해집니다. 공격자는 따옴표, OR, UNION, 주석(--) 등의 토큰을 섞어 인증을 우회하거나 데이터 조회·변조·삭제를 수행할 수 있습니다. Sequelize에서 raw query(sequelize.query)를 사용할 때 문자열 연결로 사용자 입력을 넣으면 쉽게 악용됩니다.
잠재적 영향
- 인증 우회: 로그인/권한 체크 쿼리를 조작해 비인가 사용자가 정상 사용자로 가장할 수 있습니다.
- 데이터 유출: UNION, 서브쿼리 등을 주입해 민감한 정보를 대량으로 조회할 수 있습니다.
- 데이터 변조/삭제: INSERT/UPDATE/DELETE, DROP 등을 유발하여 데이터 손실이나 위변조가 발생할 수 있습니다.
- 서비스 거부(DoS): 복잡한 쿼리나 대기 함수(SLEEP 등)를 주입해 DB/애플리케이션 자원을 고갈시킬 수 있습니다.
해결 방법
- 사용자 입력을 SQL 문자열에 직접 연결하지 말고
sequelize.query의replacements또는bind옵션으로 값을 별도로 전달하세요.replacements는 Sequelize가 값을 이스케이프해 쿼리에 삽입하고,bind는 쿼리 문자열과 별도로 DB에 전달합니다. - 테이블·컬럼명 같은 식별자에는 값 플레이스홀더를 사용할 수 없습니다. 서버가 정한 허용 목록에서 선택하세요.
- 가능한 경우
Model.findAll·findOne의attributes·where같은 구조화된 옵션을 사용하세요. ORM을 사용해도 직접 조립한 raw SQL이 안전해지지는 않습니다. - 길이와 형식도 검증하세요. 수동 이스케이프에 의존하거나 MySQL용
sqlstring을 PostgreSQL 쿼리에 사용하지 마세요.
예시
변경 전
javascript
const express = require("express");
const { Sequelize } = require("sequelize");
const app = express();
const sequelize = new Sequelize("postgres://user:pass@localhost:5432/appdb");
// 취약: 사용자 입력을 그대로 문자열 결합해 raw SQL 실행
app.get("/items", async (req, res) => {
const col = req.query.col; // 예: name
const term = req.query.term; // 예: a'
const sql = `SELECT ${col} FROM products WHERE name LIKE '%${term}%'`;
try {
const [rows] = await sequelize.query(sql);
res.json(rows);
} catch (e) {
res.status(500).send("error");
}
});
변경 후
javascript
const express = require("express");
const { Sequelize, QueryTypes } = require("sequelize");
const app = express();
const sequelize = new Sequelize("postgres://user:pass@localhost:5432/appdb");
// 변경 후: 값은 replacements, 식별자는 허용 목록으로 처리
app.get("/items", async (req, res) => {
const allowedCols = new Set(["name", "price", "created_at"]);
const requested = String(req.query.col || "name");
if (!allowedCols.has(requested)) {
return res.status(400).send("invalid column");
}
const term = String(req.query.term || "");
// 값은 SQL 문자열에 직접 연결하지 않고 별도로 전달
const sql = `SELECT ${requested} FROM products WHERE name ILIKE ?`;
try {
const rows = await sequelize.query(sql, {
replacements: [`%${term}%`],
type: QueryTypes.SELECT,
});
res.json(rows);
} catch (e) {
res.status(500).send("error");
}
});
// 대안: ORM API의 구조화된 쿼리 옵션 사용
// Product.findAll({ attributes: [requested], where: { name: { [Op.iLike]: `%${term}%` } } });
설명:
- 변경 전:
term과col을 SQL에 직접 삽입하므로 쿼리 조건이나 구조가 바뀔 수 있습니다. - 변경 후: 값은
replacements로 처리하고, 컬럼명은 고정된 허용 목록에서만 선택합니다.replacements와 DB 바인딩은 구현 방식이 다르지만, 모두 사용자 값을 SQL 문자열과 분리해 API에 전달합니다.