SQL 인젝션은 웹, 애플리케이션이 사용자 입력을 제대로 필터링하지 않고 SQL 쿼리문에 포함시킬 때 발생하는 보안 취약점입니다.
공격자가 사용자 입력에 SQL 코드를 삽입(inject) 하면, 데이터베이스가 이를 정상적인 명령어로 오인하고 실행하게 됩니다.
이 공격은 웹 애플리케이션이 데이터베이스와 상호작용하는 거의 모든 곳(로그인, 검색창, 댓글 등)에 침투할 수 있어 위험도가 매우 높습니다.
사용자가 로그인 시 입력한 아이디와 비밀번호를 바탕으로 다음과 같은 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'은 항상 참이므로 인증이 우회됩니다. 비밀번호가 틀려도 로그인 성공!
이렇게 공격자는 인증을 우회하거나, 특정 권한을 탈취할 수 있습니다.
유형 | 설명 | 예시 |
---|---|---|
인증 우회 | 로그인 등에서 조건을 조작해 우회 | ' OR '1'='1 |
데이터 유출 | 모든 데이터 출력 | UNION SELECT 사용 |
데이터 변경 삭제 | 데이터 수정, 삭제 명령 삽입 | '; DROP TABLE users; -- |
블라인드 인젝션 | 결과를 직접 보지 못해도 참/거짓 유추 | 참/거짓을 시간차로 확인 |
스토어드 프로시저 호출 | DB 내 내장 함수나 명령어 실행 | exec xp_cmdshell 'net user' |
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 -- %';
보안 전략 | 설명 |
---|---|
1. 파라미터 바인딩 | SQL 쿼리에 사용자의 입력값을 직접 연결하지 않고 안전하게 처리 |
2. ORM 사용 | SQLAlchemy, Prisma 등 ORM은 기본적으로 인젝션 방지 기능을 포함 |
3. 입력값 검증 & 필터링 | 예상하지 못한 입력은 제한하고, 화이트리스트 기반 필터링 사용 |
SQL 인젝션을 막는 핵심은 사용자 입력을 신뢰하지 않고 안전하게 처리하는 것입니다.
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 같은 형식 사용 가능
Dart에서 postgres, mysql1, sqflite 같은 패키지를 사용해 클라이언트에서 직접 접속 가능하지만 보안상 매우 권장되지 않습니다.
(⚠️ 네트워크 노출, 인증정보 탈취 위험)
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를 사용할 경우에도 비슷하게 바인딩 문법을 사용하세요!
가장 안전한 구조는 Flutter → FastAPI (Python) 서버 → DB 구조입니다.
// 서버에 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!");
}
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은 기본
NoSQL 인젝션은 MongoDB, CouchDB, Firebase, Redis 등의 NoSQL 데이터베이스에서 쿼리 파라미터를 악의적으로 조작하여 시스템에 비정상적인 동작을 유도하는 공격입니다.
NoSQL DB는 구조가 유연하고 자바스크립트나 JSON 기반 쿼리를 많이 사용하기 때문에,
입력값을 객체나 배열로 조작하기 쉬워 공격자가 더 다양한 형태로 악용할 수 있습니다.
final snapshot = await FirebaseDatabase.instance
.ref("users")
.orderByChild("username")
.equalTo(userInput) // 👈 사용자 입력값 직접 사용
.get();
만약 userInput에 JSON 형식의 이상한 문자열을 넣는다면?
String userInput = '{"\$ne": null}'; // 혹은 '"); DROP TABLE users; --'
서버에서는 인식이 안 될 수도 있지만,
클라이언트에서 저장된 값을 그대로 가져오거나 필터 없이 쿼리하면 예상치 못한 검색 결과가 나올 수 있음
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("입력값이 유효하지 않습니다");
}
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이 아닌 모든 값으로 바뀌어 인증 우회 가능.
String sanitize(String input) {
return input.replaceAll(RegExp(r'[^\w@.-]'), ''); // 알파벳, 숫자, @, -, .만 허용
}
final sanitizedUsername = sanitize(userInput);
final sanitizedPassword = sanitize(passwordInput);
final result = await userCollection.findOne({
'username': sanitizedUsername,
'password': sanitizedPassword
});
final hashed = sha256.convert(utf8.encode(sanitizedPassword)).toString();
final result = await userCollection.findOne({
'username': sanitizedUsername,
'password': hashed
});
접근 방식 | 취약 여부 | 해결책 |
---|---|---|
문자열 직접 삽입 | ❌ 매우 취약 | 파라미터 바인딩 |
Dart에서 DB 직접 접근 | ⚠️ 위험 | 서버 중계 구조 사용 권장 |
Dart → Python API 서버 | ✅ 안전 | API + DB 바인딩 방식 사용 |
구분 | Firebase | MongoDB |
---|---|---|
기본 보안 | 비교적 안전 (서버 제어 불가, Firestore는 Rules 적용) | 사용자 입력 직접 반영 시 위험 |
취약점 예시 | 입력값에 JSON/SQL-like 문자열 넣기 | $ne, $gt, $or 같은 조건 조작 |
해결법 | 입력값 검증 + Firestore Rule 적용 | 입력값 sanitize + password hash + ORMs 사용 |
match /users/{userId} {
allow read, write: if request.auth.uid == userId;
allow update: if request.resource.data.keys().hasOnly(['nickname', 'status']);
}
→ 이 규칙은 인증된 사용자만 자기 데이터만 접근 가능하고, 변경 가능한 필드도 제한합니다.