SQL 인젝션

C# SQL 인젝션

설명

SQL 인젝션은 신뢰할 수 없는 값을 SQL 명령 텍스트에 직접 결합해, 값이 아니라 SQL 구문으로 해석되게 할 때 발생합니다. 공격자는 조건식, 추가 쿼리 또는 주석 구문 등을 삽입해 인증을 우회하거나 허용되지 않은 데이터를 조회·변경·삭제할 수 있습니다.

입력 검증과 SQL 이스케이프는 파라미터 바인딩을 대신하지 않습니다. 특히 데이터베이스마다 문자열 리터럴 규칙과 문자 인코딩이 다르므로, 일반적인 sanitize 또는 escape 도우미의 이름만으로 안전하다고 판단해서는 안 됩니다.

해결 방법

SQL 구조를 고정하고 신뢰할 수 없는 모든 값은 SQL 텍스트와 분리해 바인딩합니다.

  • ADO.NET에서는 공급자에 맞는 DbParameter를 추가하고 가능한 경우 데이터 형식과 크기를 명시합니다.
  • Dapper에서는 익명 객체 또는 DynamicParameters로 값을 전달합니다.
  • EF Core에서는 보간식을 안전하게 파라미터화하는 FromSql, ExecuteSql, SqlQuery를 우선 사용합니다. *Raw API가 필요한 경우 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);
    }
}

참조