설명
SQL 인젝션은 신뢰할 수 없는 값을 SQL 명령 텍스트에 직접 결합해, 값이 아니라 SQL 구문으로 해석되게 할 때 발생합니다. 공격자는 조건식, 추가 쿼리 또는 주석 구문 등을 삽입해 인증을 우회하거나 허용되지 않은 데이터를 조회·변경·삭제할 수 있습니다.
입력 검증과 SQL 이스케이프는 파라미터 바인딩을 대신하지 않습니다. 특히 데이터베이스마다 문자열 리터럴 규칙과 문자 인코딩이 다르므로, 일반적인 sanitize 또는 escape 도우미의 이름만으로 안전하다고 판단해서는 안 됩니다.
해결 방법
SQL 구조를 고정하고 신뢰할 수 없는 모든 값은 SQL 텍스트와 분리해 바인딩합니다.
- ADO.NET에서는 공급자에 맞는
DbParameter를 추가하고 가능한 경우 데이터 형식과 크기를 명시합니다. - Dapper에서는 익명 객체 또는
DynamicParameters로 값을 전달합니다. - EF Core에서는 보간식을 안전하게 파라미터화하는
FromSql,ExecuteSql,SqlQuery를 우선 사용합니다.*RawAPI가 필요한 경우 SQL은 고정하고 자리표시자와 값을 별도 인자로 전달합니다. - 테이블명, 열 이름, 정렬 방향 같은 SQL 식별자는 파라미터로 바인딩할 수 없습니다. 사용자가 선택할 수 있는 값을 작은 허용 집합으로 제한하고 각 선택을 개발자가 작성한 고정 리터럴에 매핑합니다.
- 데이터베이스 계정에는 필요한 작업만 허용하는 최소 권한을 적용합니다.
예시
변경 전
다음 코드는 요청 값을 SQL 텍스트에 직접 삽입합니다.
csharp
using System.Data;
using Dapper;
using Microsoft.AspNetCore.Mvc;
public sealed class UsersController
{
public object Search(
[FromQuery] string username,
IDbConnection dapperConnection)
{
var sql = $"SELECT * FROM Users WHERE Username = '{username}'";
return dapperConnection.QueryFirstOrDefault(sql);
}
}
변경 후
ADO.NET과 Dapper에서는 SQL과 값을 분리합니다. AddWithValue도 값을 명령 텍스트와 분리하지만, 형식 추론의 모호성과 성능 문제를 줄이기 위해 이 예시는 형식과 크기를 명시합니다.
csharp
using System.Data;
using Dapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;
public sealed class UsersController
{
public object Search(
[FromQuery] string username,
SqlConnection sqlConnection,
IDbConnection dapperConnection)
{
const string sql =
"SELECT * FROM Users WHERE Username = @username";
using var command = new SqlCommand(sql, sqlConnection);
command.Parameters
.Add("@username", SqlDbType.NVarChar, 128)
.Value = username;
using var reader = command.ExecuteReader();
return dapperConnection.QueryFirstOrDefault(
sql,
new { username });
}
}
EF Core의 보간 안전 API는 보간된 값을 자동으로 파라미터로 변환합니다. 원시 API를 사용하더라도 값은 별도 인자로 전달할 수 있습니다.
csharp
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
public sealed class User
{
public int Id { get; set; }
public string Username { get; set; } = "";
}
public sealed class UsersController
{
public object Search(
[FromQuery] string username,
DbSet<User> users)
{
var parameterized = users.FromSql(
$"SELECT * FROM Users WHERE Username = {username}");
var rawWithSeparateValue = users.FromSqlRaw(
"SELECT * FROM Users WHERE Username = {0}",
username);
return new { parameterized, rawWithSeparateValue };
}
}
동적 SQL 식별자가 필요하면 입력을 그대로 결합하지 않고 고정 리터럴에 매핑합니다.
csharp
using System.Data;
using Dapper;
using Microsoft.AspNetCore.Mvc;
public sealed class ReportsController
{
public object List(
[FromQuery] string sort,
IDbConnection connection)
{
var orderBy = sort switch
{
"name" => "Name",
"created" => "CreatedAt",
_ => "Id"
};
var sql = "SELECT * FROM Reports ORDER BY " + orderBy;
return connection.Query(sql);
}
}
참조
- OWASP SQL Injection Prevention Cheat Sheet
- OWASP Query Parameterization Cheat Sheet
- Entity Framework Core: SQL Queries
- ADO.NET: Configuring Parameters and Parameter Data Types
- Dapper 공식 문서
- OWASP ASVS 5.0.0, V1.2.4
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command
- OWASP Top 10:2025 A05 - Injection