리소스 인젝션

C# 데이터베이스 리소스 인젝션(CWE-99)

설명

리소스 인젝션은 신뢰할 수 없는 값이 애플리케이션이 사용할 리소스의 식별자나 접근 설명자를 결정할 때 발생합니다. .NET 데이터 접근 코드에서는 연결 문자열이나 그 구성요소가 다음과 같은 보안 결정을 포함할 수 있습니다.

  • SQL Server·PostgreSQL·MySQL 서버 또는 네트워크 주소
  • 데이터베이스나 카탈로그 이름
  • SQLite 파일 경로
  • ODBC DSN·드라이버 또는 OLE DB 공급자
  • 사용자 이름, 비밀번호, 인증 방식과 같은 인증 컨텍스트

공격자가 이 값을 제어하면 SQL 문을 변경하지 않더라도 애플리케이션을 의도하지 않은 서버, 데이터베이스 또는 로컬 파일로 연결할 수 있습니다. 따라서 이 문제는 쿼리 구문을 조작하는 SQL 인젝션과 구분됩니다. 실제 피해는 구성된 연결이나 데이터 소스가 열리거나 사용될 때 발생합니다.

SqlConnectionStringBuilder, NpgsqlConnectionStringBuilder 같은 연결 문자열 빌더는 구성요소 값의 따옴표와 구분자 구문을 안전하게 직렬화합니다. 그러나 빌더는 사용자가 지정한 호스트, 데이터베이스, 파일 또는 자격 증명에 대한 권한을 확인하지 않습니다. 예를 들어 DataSource = userInput은 구분자 인젝션은 막더라도 공격자가 선택한 서버에 연결하는 문제를 해결하지 못합니다.

잠재적 영향

  • 공격자가 운영 데이터베이스 대신 자신이 제어하는 서버로 연결을 유도해 쿼리, 데이터 또는 자격 증명을 노출할 수 있습니다.
  • 더 높은 권한의 데이터베이스나 다른 테넌트의 저장소를 선택해 접근 통제를 우회할 수 있습니다.
  • SQLite 파일 경로나 DSN·드라이버를 조작해 의도하지 않은 로컬 파일 또는 공급자를 사용하게 할 수 있습니다.
  • 연결 실패, 장시간 대기 또는 연결 풀 고갈을 반복시켜 서비스 가용성을 저하시킬 수 있습니다.

해결 방법

연결 정보를 서버 측에서 관리

완전한 연결 문자열은 서버가 제어하는 구성이나 비밀 저장소에서 읽습니다. 비밀번호와 토큰을 소스 코드, URL, 로그에 넣지 마세요. 배포 환경 변수를 사용하는 경우 해당 값을 변경할 수 있는 주체를 배포 권한으로 제한하고 비밀 관리 정책을 적용해야 합니다.

사용자 입력을 그대로 구성 키로 사용하지 마세요. 예를 들어 configuration[userInput]은 값 자체가 비밀 저장소에 있더라도 사용자가 예상하지 않은 키를 선택하게 할 수 있습니다.

논리 식별자를 권한 검사 후 매핑

사용자가 여러 리소스 중 하나를 선택해야 한다면 primary, reporting처럼 의미가 제한된 논리 식별자만 받습니다. 요청 주체가 해당 식별자를 사용할 권한이 있는지 확인하고, 코드가 소유한 고정 매핑을 통해 신뢰할 수 있는 구성 이름이나 연결 설명자로 변환합니다. 매핑 내용과 변경 권한도 애플리케이션이 통제해야 합니다.

개별 연결 구성요소를 동적으로 지정해야 한다면 각 값을 비즈니스 규칙에 따라 별도로 검증한 후 공급자별 ConnectionStringBuilder에 할당합니다. 빌더는 검증 뒤에 구문을 안전하게 구성하는 용도이며, 검증이나 권한 부여를 대신하지 않습니다. 이름이 Sanitize, Encrypt, Hash인 헬퍼도 실제 계약을 확인하지 않고 안전하다고 간주하면 안 됩니다.

현재 공급자와 안전한 인증 사용

  • SQL Server 신규 코드에는 사용 중단된 System.Data.SqlClient 대신 Microsoft.Data.SqlClient를 사용합니다.
  • 지원되는 환경에서는 비밀번호보다 관리 ID, 통합 인증 또는 단기 액세스 토큰을 우선합니다.
  • 데이터베이스 계정에 최소 권한을 부여하고 테넌트·업무별 접근 경계를 별도로 적용합니다.
  • 전송 암호화와 서버 인증서 검증을 활성화합니다. 연결 문자열 빌더를 사용했다는 사실만으로 TLS가 보장되지는 않습니다.
  • SQLite 파일을 선택해야 한다면 파일 이름을 고정 매핑하고 정규화된 경로가 전용 기준 디렉터리 안에 있는지 별도로 검증합니다.

예시

변경 전

csharp
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;

[ApiController]
[Route("database")]
public sealed class VulnerableDatabaseController : ControllerBase
{
    [HttpPost("connect")]
    public IActionResult Connect([FromQuery] string connectionString)
    {
        using var connection = new SqlConnection(connectionString);
        connection.Open();
        return Ok();
    }
}

요청자가 전체 연결 문자열을 지정하므로 서버, 데이터베이스, 인증 컨텍스트를 함께 바꿀 수 있습니다.

변경 후

csharp
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;

[ApiController]
[Route("database")]
public sealed class SafeDatabaseController : ControllerBase
{
    private readonly IConfiguration _configuration;

    public SafeDatabaseController(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    [HttpPost("connect")]
    public IActionResult Connect([FromQuery] string databaseId)
    {
        string? connectionName = databaseId switch
        {
            "primary" => "PrimaryDatabase",
            "reporting" when User.IsInRole("ReportReader") => "ReportingDatabase",
            _ => null
        };
        if (connectionName is null)
        {
            return Forbid();
        }

        string? connectionString =
            _configuration.GetConnectionString(connectionName);
        if (string.IsNullOrWhiteSpace(connectionString))
        {
            return Problem("Database configuration is unavailable.");
        }

        using var connection = new SqlConnection(connectionString);
        connection.Open();
        return Ok();
    }
}

안전한 예시는 요청값을 고정된 논리 식별자로 제한하고 reporting 선택에는 역할 검사를 추가합니다. 요청값으로 구성 키를 직접 조회하지 않으며, 실제 연결 문자열은 서버 측 구성에서만 가져옵니다. 프로덕션에서는 연결을 시험하는 엔드포인트를 공개하지 말고 실제 업무 작업에 같은 선택·권한 정책을 적용하세요.

참조