Injection 실전 가이드
Injection의 핵심 개념과 실무 활용
학습 항목
이미지 로딩 중...
SQL Injection 방어 완벽 가이드
웹 애플리케이션의 가장 위험한 보안 취약점 중 하나인 SQL Injection을 방어하는 다양한 기법을 학습합니다. Prepared Statement, ORM, 입력 검증 등 실전에서 바로 적용 가능한 방어 기법을 다룹니다.
들어가며
이 글에서는 SQL Injection 방어 완벽 가이드에 대해 상세히 알아보겠습니다. 총 12가지 주요 개념을 다루며, 각각의 개념에 대한 설명과 실제 코드 예제를 함께 제공합니다.
목차
- Prepared_Statement_기본
- Named_Parameters_사용
- ORM을_통한_안전한_쿼리
- 입력_검증_화이트리스트
- 입력_이스케이프_처리
- 저장_프로시저_활용
- 최소_권한_원칙_적용
- 에러_메시지_숨김
- WAF_활용
- 입력_타입_검증
- 쿼리_빌더_안전_사용
- 정기_보안_테스트
1. Prepared_Statement_기본
개요
Prepared Statement는 SQL 쿼리와 데이터를 분리하여 SQL Injection을 근본적으로 차단하는 가장 효과적인 방법입니다.
코드 예제
const mysql = require('mysql2/promise');
async function getUserById(userId) {
const connection = await mysql.createConnection(config);
const [rows] = await connection.execute(
'SELECT * FROM users WHERE id = ?',
[userId]
);
return rows;
}
설명
'?' 플레이스홀더를 사용하여 SQL 쿼리와 사용자 입력을 분리합니다. 데이터베이스 드라이버가 자동으로 입력값을 이스케이프 처리하여 악의적인 SQL 코드 실행을 방지합니다.
2. Named_Parameters_사용
개요
Named Parameter를 사용하면 복잡한 쿼리에서도 가독성을 유지하면서 안전하게 데이터를 바인딩할 수 있습니다.
코드 예제
async function searchUsers(username, email) {
const [rows] = await connection.execute(
'SELECT * FROM users WHERE username = :username AND email = :email',
{ username, email }
);
return rows;
}
설명
:username, :email과 같은 명명된 파라미터를 사용하여 여러 값을 안전하게 바인딩합니다. 쿼리의 가독성이 높아지고 파라미터 순서 오류를 방지할 수 있습니다.
3. ORM을_통한_안전한_쿼리
개요
Sequelize, TypeORM 같은 ORM을 사용하면 SQL을 직접 작성하지 않고도 안전한 데이터베이스 작업이 가능합니다.
코드 예제
const { Sequelize, DataTypes } = require('sequelize');
const User = sequelize.define('User', {
username: DataTypes.STRING,
email: DataTypes.STRING
});
async function findUser(username) {
return await User.findOne({ where: { username } });
}
설명
ORM은 내부적으로 Prepared Statement를 사용하여 모든 쿼리를 자동으로 파라미터화합니다. 개발자가 SQL Injection을 신경 쓰지 않아도 안전한 코드를 작성할 수 있습니다.
4. 입력_검증_화이트리스트
개요
사용자 입력을 받을 때 허용된 값만 통과시키는 화이트리스트 방식으로 검증하여 공격 벡터를 원천 차단합니다.
코드 예제
function getSortColumn(userInput) {
const allowedColumns = ['id', 'name', 'created_at'];
if (!allowedColumns.includes(userInput)) {
throw new Error('Invalid sort column');
}
return userInput;
}
설명
동적 테이블명, 컬럼명, ORDER BY 절 등 Prepared Statement로 처리할 수 없는 경우 화이트리스트로 검증합니다. 허용된 값만 통과시켜 공격 가능성을 완전히 제거합니다.
5. 입력_이스케이프_처리
개요
불가피하게 동적 SQL을 사용해야 할 때는 데이터베이스별 이스케이프 함수를 사용하여 특수문자를 무력화합니다.
코드 예제
const mysql = require('mysql2');
function searchByPattern(pattern) {
const escaped = mysql.escapeId(pattern);
const query = `SELECT * FROM ${escaped}`;
return connection.query(query);
}
설명
mysql.escape()는 값을, mysql.escapeId()는 식별자(테이블명, 컬럼명)를 이스케이프합니다. 하지만 이 방법은 최후의 수단이며, 가능하면 Prepared Statement를 사용해야 합니다.
6. 저장_프로시저_활용
개요
저장 프로시저를 사용하면 SQL 로직을 데이터베이스 내부에 캡슐화하여 애플리케이션 레이어의 SQL Injection 위험을 줄일 수 있습니다.
코드 예제
async function registerUser(username, email) {
await connection.execute(
'CALL sp_register_user(?, ?)',
[username, email]
);
}
// DB: CREATE PROCEDURE sp_register_user(
// IN p_username VARCHAR(50), IN p_email VARCHAR(100))
설명
저장 프로시저는 파라미터화된 방식으로 실행되며, 복잡한 비즈니스 로직을 안전하게 처리할 수 있습니다. 애플리케이션 코드에서는 프로시저를 호출만 하면 됩니다.
7. 최소_권한_원칙_적용
개요
데이터베이스 사용자 계정에 필요한 최소한의 권한만 부여하여 SQL Injection 공격의 피해 범위를 제한합니다.
코드 예제
// DB 사용자 권한 설정 예시
// CREATE USER 'app_user'@'localhost'
// IDENTIFIED BY 'password';
// GRANT SELECT, INSERT, UPDATE ON mydb.users
// TO 'app_user'@'localhost';
const config = {
user: 'app_user', // DROP, ALTER 권한 없음
password: 'password',
database: 'mydb'
};
설명
애플리케이션 DB 계정에 DROP, ALTER 등 위험한 권한을 부여하지 않습니다. SQL Injection이 발생해도 데이터 조회/수정만 가능하고 테이블 삭제 등은 불가능하게 만듭니다.
8. 에러_메시지_숨김
개요
데이터베이스 에러 메시지를 사용자에게 노출하지 않아 공격자가 데이터베이스 구조를 파악하지 못하도록 합니다.
코드 예제
async function handleQuery(userId) {
try {
const [rows] = await connection.execute(
'SELECT * FROM users WHERE id = ?', [userId]
);
return rows;
} catch (error) {
console.error('DB Error:', error);
throw new Error('Database operation failed');
}
}
설명
상세한 SQL 에러 메시지는 로그에만 기록하고, 사용자에게는 일반적인 에러 메시지만 반환합니다. 공격자가 테이블명, 컬럼명 등의 정보를 얻지 못하게 합니다.
9. WAF_활용
개요
Web Application Firewall을 사용하여 SQL Injection 패턴을 자동으로 탐지하고 차단하는 추가 보안 계층을 구축합니다.
코드 예제
const helmet = require('helmet');
const expressSanitizer = require('express-sanitizer');
app.use(helmet());
app.use(expressSanitizer());
app.post('/search', (req, res) => {
const sanitized = req.sanitize(req.body.query);
// 추가 검증 및 처리
});
설명
미들웨어 레벨에서 위험한 패턴을 필터링합니다. UNION, OR 1=1, DROP TABLE 등의 SQL Injection 시도를 사전에 차단할 수 있습니다.
10. 입력_타입_검증
개요
TypeScript나 스키마 검증 라이브러리를 활용하여 입력 데이터의 타입과 형식을 엄격하게 검증합니다.
코드 예제
const Joi = require('joi');
const userSchema = Joi.object({
id: Joi.number().integer().positive().required(),
username: Joi.string().alphanum().min(3).max(30),
email: Joi.string().email()
});
const { error, value } = userSchema.validate(req.body);
설명
Joi, Zod 같은 스키마 검증 라이브러리로 입력값의 타입, 길이, 형식을 검증합니다. SQL Injection에 사용되는 특수문자나 예상 밖의 입력을 사전에 차단합니다.
11. 쿼리_빌더_안전_사용
개요
Knex.js 같은 쿼리 빌더를 사용하면 동적 쿼리를 안전하게 작성할 수 있으며, 자동으로 파라미터 바인딩이 적용됩니다.
코드 예제
const knex = require('knex')({ client: 'mysql2' });
async function searchUsers(filters) {
return await knex('users')
.where('status', filters.status)
.andWhere('age', '>', filters.minAge)
.select('*');
}
설명
쿼리 빌더는 메서드 체이닝으로 쿼리를 구성하며, 모든 값은 자동으로 파라미터화됩니다. SQL 문자열을 직접 작성하지 않아 안전성이 높습니다.
12. 정기_보안_테스트
개요
OWASP ZAP, SQLMap 같은 도구로 정기적으로 SQL Injection 취약점을 스캔하여 보안 상태를 점검합니다.
코드 예제
// package.json 스크립트
{
"scripts": {
"security:scan": "npm audit && snyk test",
"pentest:sql": "sqlmap -u 'http://localhost/api' --batch --risk=3"
}
}
// CI/CD 파이프라인에 통합
설명
자동화된 보안 스캔을 CI/CD 파이프라인에 통합하여 배포 전에 SQL Injection 취약점을 발견합니다. 지속적인 모니터링으로 새로운 취약점을 조기에 탐지할 수 있습니다.
마치며
이번 글에서는 SQL Injection 방어 완벽 가이드에 대해 알아보았습니다. 총 12가지 개념을 다루었으며, 각각의 사용법과 예제를 살펴보았습니다.
관련 태그
#JavaScript #SQLInjection #Security #PreparedStatement #InputValidation