nodejs express 공부를 하고 있는데, 쿼리를 작성하던 중 문득 생각이 들었다.
`
SELECT * FROM users
WHERE user_id = ?
`
mariadb 모듈에는 query 함수가 있는데,
첫 번째 인자로 쿼리 문자열 받고,
두 번째 인자로 쿼리의 플레이스홀더(? 부분)에 해당하는 부분을 배열 형태로 받는다.
세 번째 인자로 콜백함수를 받는다.
여기서 ${} 형태로 작성하면 가독성과 유지보수 측면에서 더 좋지 않나? 라는 생각이 들었다.
`
SELECT * FROM users
WHERE user_id = ${userId}
`
이런 식으로 작성하고 두 번째 인자를 생략하면 정상적으로 작동된다.
하지만 또 한편으로 드는 생각이 굳이 첫 번째 방식으로 작성하고 두 번째 인자로 배열을 받는 이유가 있지 않을까? 라는 생각이 들었다.
그래서 검색을 좀 해보니 두번째 방식으로 작성을 하게 되면 SQL Injection 공격에 취약해진다고 한다.
역시 개발자들은 항상 코드에 이유가 있다.
그래서 테스트를 해보았다.
1. ${변수}
// 소스코드
login1 = (email, password) => {
return new Promise((resolve, reject) => {
const query = `
SELECT * FROM users u
WHERE email = '${email}' AND password = '${password}';
`;
db.query(query, (error, data) => {
if (error) reject(error);
else resolve(data[0]);
});
});
}
// email과 password에 아래와 같이 입력
login1("test1", "' OR '1' = '1")
.then(result => console.log(result));
/* 결과
{
"user_id": 1,
"email": "test1",
"name": "네임",
"password": "$2b$10$qJ3/h/W4yiLLygW6.1xn8uI6xpbLY1/h6EpX./PkydaZ6tBd3AakC",
"created_at": "2024-01-21T13:04:43.000Z",
"modified_at": "2024-01-31T02:56:35.000Z"
}
*/
위 코드를 실행하면 데이터베이스에서는 아래와 같은 쿼리가 실행된다.
-- 실제 데이터베이스에서 실행되는 쿼리
SELECT * FROM users u
WHERE email = 'test1' AND password = '' OR '1' = '1';
-- 위 쿼리는 결국 아래 쿼리와 같다
SELECT * FROM users u
2. 플레이스 홀더(공식 권장 방식)
// 소스코드
const login2 = (email, password) => {
return new Promise((resolve, reject) => {
const query = `
SELECT * FROM users u
WHERE email = ? AND password = ?;
`;
db.query(query, [email, password], (error, data) => {
if (error) reject(error);
else resolve(data[0]);
});
});
}
// email과 password에 아래와 같이 입력
login2("test1", "' OR '1' = '1")
.then(result => console.log(result));
/* 결과
undefined
*/
위 코드를 실행하면 데이터베이스에서는 아래와 같은 쿼리가 실행된다.
-- 실제 데이터베이스에서 실행되는 쿼리
SELECT * FROM users u
WHERE email = 'test1' AND password = "'' OR '1' = '1'";
SQL 인젝션을 위해 입력한 password부분이 하나의 문자열로 데이터베이스에서 실행이 된다.
그러므로 조건에 맞는 데이터가 없기 때문에 빈 배열을 가져오게 되고, 빈 배열의 첫 번째 데이터를 리턴하기 때문에 undefined가 반환된다.
mariadb의 공식문서를 살펴보면
첫번째 줄을 살펴보면 To avoid SQL Injection attacks, 라고 되어 있는 것을 볼 수 있다.
SQL Injection attacks를 피하기 위해 라고 친절하게 적어놓았다.
결론
물론, 비밀번호의 경우 데이터베이스에 암호화해서 저장을 하고, 암호화 모듈에서 제공하는 함수를 이용해서 비교를 하기 때문에 위 쿼리처럼 WHERE절에 email AND password를 사용해서 로그인을 진행하는 경우는 드물다.
그렇다면 첫번째 방법을 사용해도 되는 거냐?
당연히 절대 안 된다.
위에서는 password에 조건문만 집어넣었지만 DELETE, DROP TABLE, UPDATE 등 다양한 쿼리를 사용해서 공격을 시도할 수 있다.
(이 경우에도 생각해야 할 것들이 있지만 주제와 맞지 않으므로 pass)
어쨌든 해커가 공격할 여지를 남겨주는 것은 좋은 방법이 아니다.
권장하는 방법을 쓰자.
전체 코드
const mariadb = require('mariadb/callback');
const DB_HOST = '호스트';
const DB_PORT = '포트';
const DB_USER = '유저';
const DB_PASSWORD = '비밀번호';
const DB_DATABASE = '데이터베이스명';
const db = mariadb.createConnection({
host: DB_HOST,
port: DB_PORT,
user: DB_USER,
password: DB_PASSWORD,
database: DB_DATABASE,
});
// ${} 방식
const login1 = (email, password) => {
return new Promise((resolve, reject) => {
const query = `
SELECT * FROM users u
WHERE email = '${email}' AND password = '${password}';
`;
db.query(query, (error, data) => {
if (error) reject(error);
else resolve(data[0]);
});
});
}
login1("test1", "' OR '1' = '1")
.then(result => console.log(result));
// ? 방식
const login2 = (email, password) => {
return new Promise((resolve, reject) => {
const query = `
SELECT * FROM users u
WHERE email = ? AND password = ?;
`;
db.query(query, [email, password], (error, data) => {
if (error) reject(error);
else resolve(data[0]);
});
});
}
login2("test1", "' OR '1' = '1")
.then(result => console.log(result));
'개발일지 > Node.js' 카테고리의 다른 글
Prisma vs TypeORM 벤치마크 (0) | 2024.04.09 |
---|---|
TypeORM + MySQL8 AsText does not exist (0) | 2024.03.12 |
Nestjs - Shared modules : 의존성 주입을 통하여 모듈 공유하기 (0) | 2024.02.29 |
TypeORM - @Column({ unique : true }) vs @Unique() (0) | 2024.02.23 |