내일배움캠프 TIL - SQL 인젝션(SQL Injection)

Jaden Lee·2025년 4월 17일
0

내일배움캠프

목록 보기
12/15

💥 SQL 인젝션(SQL Injection): 데이터베이스를 뚫는 가장 위험한 공격

🧠 1. SQL 인젝션이란?

SQL 인젝션은 웹, 애플리케이션이 사용자 입력을 제대로 필터링하지 않고 SQL 쿼리문에 포함시킬 때 발생하는 보안 취약점입니다.

공격자가 사용자 입력에 SQL 코드를 삽입(inject) 하면, 데이터베이스가 이를 정상적인 명령어로 오인하고 실행하게 됩니다.

이 공격은 웹 애플리케이션이 데이터베이스와 상호작용하는 거의 모든 곳(로그인, 검색창, 댓글 등)에 침투할 수 있어 위험도가 매우 높습니다.

🔍 2. SQL 인젝션 예시

예시 상황: 로그인 폼

사용자가 로그인 시 입력한 아이디와 비밀번호를 바탕으로 다음과 같은 SQL 쿼리가 만들어진다고 가정해봅시다:

SELECT * FROM users WHERE username = 'admin' AND password = '1234';

하지만 공격자가 입력칸에 이런 값을 입력하면?

username: ' OR '1'='1
password: 아무거나

SQL문은 이렇게 바뀝니다:

SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '...';

'1'='1'은 항상 참이므로 인증이 우회됩니다. 비밀번호가 틀려도 로그인 성공!
이렇게 공격자는 인증을 우회하거나, 특정 권한을 탈취할 수 있습니다.

⚠️ 3. SQL 인젝션의 유형과 피해

🧱 주요 유형

유형설명예시
인증 우회로그인 등에서 조건을 조작해 우회' OR '1'='1
데이터 유출모든 데이터 출력UNION SELECT 사용
데이터 변경 삭제데이터 수정, 삭제 명령 삽입'; DROP TABLE users; --
블라인드 인젝션결과를 직접 보지 못해도 참/거짓 유추참/거짓을 시간차로 확인
스토어드 프로시저 호출DB 내 내장 함수나 명령어 실행exec xp_cmdshell 'net user'

🔥 4. 실전 사례

🎯 4-1. Sony PlayStation Network 해킹 (2011)

  • 피해 규모: 약 7,700만 명의 개인정보 유출 (이름, 주소, 이메일, 신용카드 정보)
  • 원인: SQL 인젝션 취약점을 이용한 비인가 접근
  • 영향: PSN 서비스 일시 중단, 1억 달러 이상 피해 추정

🎯 4-2. Heartland Payment Systems (2008)

  • 피해 규모: 1억 3천만 건 이상의 신용카드 정보 유출
  • 방법: SQL 인젝션을 통한 내부망 침투 → 악성 코드 설치
  • 영향: PCI DSS 인증 박탈, 막대한 벌금 및 신뢰도 하락

🎯 4-3. 국내 쇼핑몰 사례 (가명처리)

  • 특정 중소 쇼핑몰에서 검색창에 ' OR 1=1 --를 입력한 결과, 관리자 페이지에 접근 가능
  • 고객 데이터 20만 건 유출, 개인정보보호법 위반으로 과징금 부과

🧪 5. 실제 악용 예시: 게시판 검색창

SELECT * FROM posts WHERE title LIKE '%$keyword%';

공격자가 아래와 같이 입력할 경우:

keyword = %' UNION SELECT credit_card_number, expiry FROM users --

전체 쿼리는 다음처럼 구성됩니다:

SELECT * FROM posts WHERE title LIKE '%%' UNION SELECT credit_card_number, expiry FROM users -- %';

👉 게시글 대신 사용자 카드 정보를 출력할 수 있게 됩니다.

🧷 6. SQL 인젝션을 막는 3가지 핵심 원칙

보안 전략설명
1. 파라미터 바인딩SQL 쿼리에 사용자의 입력값을 직접 연결하지 않고 안전하게 처리
2. ORM 사용SQLAlchemy, Prisma 등 ORM은 기본적으로 인젝션 방지 기능을 포함
3. 입력값 검증 & 필터링예상하지 못한 입력은 제한하고, 화이트리스트 기반 필터링 사용

🛡️ 7. 어떻게 방어할 수 있을까?

SQL 인젝션을 막는 핵심은 사용자 입력을 신뢰하지 않고 안전하게 처리하는 것입니다.

✅ 해결법 1: Python에서 서버가 DB에 접근하는 경우

❌ 취약한 코드

import sqlite3

def login(username, password):
    conn = sqlite3.connect("users.db")
    cursor = conn.cursor()
    
    # 사용자 입력 직접 문자열에 삽입 - 매우 위험!
    query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
    cursor.execute(query)
    
    result = cursor.fetchone()
    return result

✅ 안전한 코드: 파라미터 바인딩 사용

def login_secure(username, password):
    conn = sqlite3.connect("users.db")
    cursor = conn.cursor()
    
    # ?로 파라미터 바인딩 => 자동 이스케이프 처리
    cursor.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password))
    result = cursor.fetchone()
    return result

Tip: MySQL, PostgreSQL에서는 %s, $1, :param 같은 형식 사용 가능

✅ 해결법 2: Dart(Flutter) 앱에서 DB에 직접 접속하는 경우

Dart에서 postgres, mysql1, sqflite 같은 패키지를 사용해 클라이언트에서 직접 접속 가능하지만 보안상 매우 권장되지 않습니다.
(⚠️ 네트워크 노출, 인증정보 탈취 위험)

❌ 취약한 코드 예시 (Flutter + PostgreSQL)

final conn = PostgreSQLConnection('host', 5432, 'dbname', username: 'user', password: 'pass');
await conn.open();

// 위험! 입력값을 그대로 쿼리에 넣음
String query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
var result = await conn.query(query);

✅ 안전한 코드: 파라미터 바인딩

final conn = PostgreSQLConnection('host', 5432, 'dbname', username: 'user', password: 'pass');
await conn.open();

// 안전한 파라미터 바인딩 방식
String query = "SELECT * FROM users WHERE username = @username AND password = @password";
var result = await conn.query(
  query,
  substitutionValues: {
    'username': username,
    'password': password
  }
);

mysql1 또는 sqflite를 사용할 경우에도 비슷하게 바인딩 문법을 사용하세요!

✅ 해결법 3: Dart 클라이언트가 Python 서버로 요청 → 서버에서 DB 접속

가장 안전한 구조는 Flutter → FastAPI (Python) 서버 → DB 구조입니다.

📦 Dart (Flutter)에서 서버에 요청

// 서버에 POST 요청
final response = await http.post(
  Uri.parse("http://yourserver.com/login"),
  headers: {"Content-Type": "application/json"},
  body: jsonEncode({
    "username": "admin",
    "password": "1234"
  })
);

if (response.statusCode == 200) {
  print("Login success!");
} else {
  print("Login failed!");
}

🐍 FastAPI (Python) 서버 코드

from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel
import sqlite3

app = FastAPI()

class LoginRequest(BaseModel):
    username: str
    password: str

@app.post("/login")
def login(req: LoginRequest):
    conn = sqlite3.connect("users.db")
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users WHERE username = ? AND password = ?", (req.username, req.password))
    result = cursor.fetchone()
    
    if result:
        return {"message": "Login success!"}
    else:
        raise HTTPException(status_code=401, detail="Invalid credentials")

✅ 서버단에서 파라미터 바인딩과 인증/로그 관리가 가능하므로 훨씬 안전합니다.

🚧 참고 사항

  • ORM 사용 시 (예: SQLAlchemy, Prisma, Sequelize): 자동으로 SQL 인젝션 방지 처리됨 (단, Raw Query는 주의)

  • 입력값 검증도 필수: SQL 인젝션 외에도 XSS, CSRF 방어를 위해 Validation은 기본

🔍 7. NoSQL 인젝션이란?

NoSQL 인젝션은 MongoDB, CouchDB, Firebase, Redis 등의 NoSQL 데이터베이스에서 쿼리 파라미터를 악의적으로 조작하여 시스템에 비정상적인 동작을 유도하는 공격입니다.

⚠️ 왜 위험할까?

NoSQL DB는 구조가 유연하고 자바스크립트나 JSON 기반 쿼리를 많이 사용하기 때문에,
입력값을 객체나 배열로 조작하기 쉬워 공격자가 더 다양한 형태로 악용할 수 있습니다.

🔐 NoSQL 인젝션: Firebase vs MongoDB (Flutter 예제)

✅ 1. Firebase (Realtime Database / Firestore) in Flutter

❗️취약한 코드 예시 (Realtime Database):

final snapshot = await FirebaseDatabase.instance
  .ref("users")
  .orderByChild("username")
  .equalTo(userInput) // 👈 사용자 입력값 직접 사용
  .get();

😈 인젝션 시도 예:

만약 userInput에 JSON 형식의 이상한 문자열을 넣는다면?

String userInput = '{"\$ne": null}'; // 혹은 '"); DROP TABLE users; --'

서버에서는 인식이 안 될 수도 있지만,

클라이언트에서 저장된 값을 그대로 가져오거나 필터 없이 쿼리하면 예상치 못한 검색 결과가 나올 수 있음

✅ 해결법:

1. 타입과 길이 체크

bool isValidInput(String input) {
  final regex = RegExp(r'^[a-zA-Z0-9_]{3,20}$'); // 예: 3~20자의 알파벳, 숫자, _
  return regex.hasMatch(input);
}

if (!isValidInput(userInput)) {
  throw Exception("입력값이 유효하지 않습니다");
}

2. 값 escape 처리 또는 sanitize (Firebase는 대부분의 인젝션을 막지만, 안전하게 코딩하는 습관 필요)

✅ 2. MongoDB (Using mongo_dart in Flutter)

MongoDB는 NoSQL DB 중에서도 인젝션 시도가 실제로 많이 일어납니다.

❗️취약한 코드 예시:

import 'package:mongo_dart/mongo_dart.dart';

final db = await Db.create("mongodb://localhost:27017/mydb");
await db.open();

final userCollection = db.collection("users");

final result = await userCollection.findOne({
  'username': userInput,
  'password': passwordInput
});

😈 인젝션 공격 예:

String userInput = r"{ \$ne: null }";
→ username 조건이 null이 아닌 모든 값으로 바뀌어 인증 우회 가능.

✅ 해결법:

1. 값을 문자열로 강제 변환 (sanitize)

String sanitize(String input) {
  return input.replaceAll(RegExp(r'[^\w@.-]'), ''); // 알파벳, 숫자, @, -, .만 허용
}

2. ORM 또는 안전한 쿼리 조합 사용

final sanitizedUsername = sanitize(userInput);
final sanitizedPassword = sanitize(passwordInput);

final result = await userCollection.findOne({
  'username': sanitizedUsername,
  'password': sanitizedPassword
});

3. Password는 반드시 Hash로 비교

final hashed = sha256.convert(utf8.encode(sanitizedPassword)).toString();
final result = await userCollection.findOne({
  'username': sanitizedUsername,
  'password': hashed
});

💡 보안 요약 정리

접근 방식취약 여부해결책
문자열 직접 삽입❌ 매우 취약파라미터 바인딩
Dart에서 DB 직접 접근⚠️ 위험서버 중계 구조 사용 권장
Dart → Python API 서버✅ 안전API + DB 바인딩 방식 사용

💡 NoSQL 요약 정리

구분FirebaseMongoDB
기본 보안비교적 안전 (서버 제어 불가, Firestore는 Rules 적용)사용자 입력 직접 반영 시 위험
취약점 예시입력값에 JSON/SQL-like 문자열 넣기$ne, $gt, $or 같은 조건 조작
해결법입력값 검증 + Firestore Rule 적용입력값 sanitize + password hash + ORMs 사용

🔐 Firebase 보안 규칙 예 (Firestore)

match /users/{userId} {
  allow read, write: if request.auth.uid == userId;
  allow update: if request.resource.data.keys().hasOnly(['nickname', 'status']);
}

→ 이 규칙은 인증된 사용자만 자기 데이터만 접근 가능하고, 변경 가능한 필드도 제한합니다.

✨ 마무리

  • SQL 인젝션은 해킹 기술 중에서도 가장 고전적이면서도 파괴적인 공격입니다.
  • NoSQL도 인젝션 공격 대상입니다.
  • Firebase는 보안 규칙을 꼭 설정하고, MongoDB는 사용자 입력을 철저하게 필터링해야 합니다.
  • Flutter 클라이언트에서도 타입, 형식, 길이 제한 등 입력 유효성 검사가 핵심입니다.
  • 다행히도 파라미터 바인딩과 입력 검증만 잘 해도 99% 이상 방지할 수 있습니다.

0개의 댓글