🔒 보안

SQL 인젝션

SQL Injection

악의적 SQL 코드를 삽입하는 공격. 데이터 유출, 변조 가능. Prepared Statement로 방어.

📖 상세 설명

SQL 인젝션은 사용자 입력을 통해 악의적인 SQL 코드를 삽입하는 웹 보안 공격입니다. OWASP Top 10에서 오랫동안 상위를 차지하는 치명적인 취약점으로, 데이터 유출, 변조, 삭제, 심지어 서버 장악까지 가능합니다. 2023년에도 여전히 많은 피해 사례가 보고되고 있습니다.

공격 원리는 간단합니다. 쿼리 문자열에 사용자 입력을 직접 연결하면, 공격자가 입력에 SQL 구문을 포함시켜 원래 쿼리의 의미를 바꿀 수 있습니다. 예: WHERE id = '1' OR '1'='1' 는 항상 참이 되어 모든 데이터가 노출됩니다. ' DROP TABLE users; -- 같은 입력으로 테이블을 삭제할 수도 있습니다.

SQL 인젝션 유형: 1) In-band SQLi - 결과가 직접 화면에 표시 (Error-based, UNION-based), 2) Blind SQLi - 결과가 보이지 않고 참/거짓 반응으로 추론 (Boolean-based, Time-based), 3) Out-of-band SQLi - DNS, HTTP 요청 등 외부 채널로 데이터 유출.

핵심 방어는 Prepared Statement(Parameterized Query)입니다. SQL 구조와 데이터를 분리하여 데이터베이스 드라이버가 값을 안전하게 이스케이프합니다. 추가로 입력 검증, 최소 권한 DB 계정 사용, WAF(Web Application Firewall), 에러 메시지 숨김 등 다층 방어가 필요합니다.

💻 코드 예제

// SQL 인젝션 취약 코드 vs 안전한 코드

// ❌ 취약한 코드 - 문자열 연결
async function getUser_VULNERABLE(username: string) {
  // 공격 예: username = "admin' OR '1'='1' --"
  // 결과 쿼리: SELECT * FROM users WHERE username = 'admin' OR '1'='1' --'
  const query = `SELECT * FROM users WHERE username = '${username}'`;
  return await db.query(query);  // 모든 사용자 정보 유출!
}

// ❌ 더 위험한 예 - DROP TABLE
// username = "'; DROP TABLE users; --"
// 결과: SELECT * FROM users WHERE username = ''; DROP TABLE users; --'

// ✅ 안전한 코드 - Prepared Statement (Node.js + PostgreSQL)
async function getUser_SAFE(username: string) {
  const query = 'SELECT * FROM users WHERE username = $1';
  // $1은 플레이스홀더, username은 별도 전달되어 이스케이프됨
  return await db.query(query, [username]);
}

// ✅ TypeORM - Query Builder
async function getUser_TypeORM(username: string) {
  return await userRepository
    .createQueryBuilder('user')
    .where('user.username = :username', { username })  // 파라미터 바인딩
    .getOne();
}

// ✅ Prisma - 기본적으로 안전
const user = await prisma.user.findUnique({
  where: { username }  // Prisma가 자동으로 이스케이프
});

// ⚠️ 주의: Raw Query 사용 시
// Prisma raw query - 안전한 방법
const result = await prisma.$queryRaw`
  SELECT * FROM users WHERE username = ${username}
`;  // Tagged template literal은 자동 이스케이프

// ❌ Prisma raw query - 위험한 방법
const dangerous = await prisma.$queryRawUnsafe(
  `SELECT * FROM users WHERE username = '${username}'`
);  // 절대 사용 금지!

// ✅ 동적 컬럼명/테이블명이 필요한 경우 - 화이트리스트
const ALLOWED_COLUMNS = ['id', 'name', 'email', 'created_at'];

function buildQuery(sortColumn: string, sortOrder: string) {
  // 입력값 검증 - 화이트리스트에 있는지 확인
  if (!ALLOWED_COLUMNS.includes(sortColumn)) {
    throw new Error('Invalid column name');
  }
  if (!['ASC', 'DESC'].includes(sortOrder.toUpperCase())) {
    throw new Error('Invalid sort order');
  }

  // 검증된 값만 쿼리에 포함
  return `SELECT * FROM users ORDER BY ${sortColumn} ${sortOrder}`;
}

// ✅ Python + psycopg2
import psycopg2

# 안전한 방법
cursor.execute(
    "SELECT * FROM users WHERE username = %s AND password = %s",
    (username, password)  # 튜플로 파라미터 전달
)

# ✅ Java + JDBC
String query = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement stmt = conn.prepareStatement(query);
stmt.setString(1, username);  // 인덱스 기반 바인딩
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();

🗣️ 실무 대화 예시

보안 감사에서:

"검색 기능에서 SQL 인젝션 취약점이 발견되었습니다. 사용자 입력을 직접 쿼리에 연결하고 있어요. ORM 사용하더라도 raw query 쓰는 부분은 반드시 파라미터 바인딩으로 수정해야 합니다."

기술 면접에서:

"SQL 인젝션 방어 방법은?" - "가장 중요한 건 Prepared Statement로 쿼리와 데이터를 분리하는 거예요. 추가로 입력 검증, 최소 권한 원칙, 에러 메시지 숨기기, WAF 적용 등 다층 방어를 해야 합니다."

코드 리뷰에서:

"이 부분에 template literal로 쿼리 만들고 있는데, 파라미터 바인딩으로 바꿔주세요. ORM 쓰면 대부분 안전하지만, ORDER BY나 테이블명 동적으로 넣는 건 화이트리스트 검증이 필요해요."

⚠️ 주의사항

🔗 관련 용어

XSS CSRF ORM WAF Input Validation

📚 더 배우기

📄 OWASP - SQL Injection 📄 OWASP SQL Injection Prevention Cheat Sheet 📄 PortSwigger - SQL Injection 실습