SignalR 보안 소개(SignalR 1.x)

작성자: Patrick Fletcher, Tom FitzMacken

경고

이 설명서는 최신 버전의 SignalR용이 아닙니다. ASP.NET Core SignalR을 살펴보세요.

이 문서에서는 SignalR 애플리케이션을 개발할 때 고려해야 하는 보안 문제에 대해 설명합니다.

개요

이 문서는 다음 섹션으로 구성됩니다.

SignalR 보안 개념

인증 및 권한 부여

SignalR은 애플리케이션의 기존 인증 구조에 통합되도록 설계되었습니다. 사용자를 인증하기 위한 기능은 제공하지 않습니다. 대신 애플리케이션에서 정상적으로 사용자를 인증한 다음 SignalR 코드에서 인증 결과를 사용합니다. 예를 들어 ASP.NET 양식 인증을 사용하여 사용자를 인증한 다음 허브에서 메서드를 호출할 권한이 있는 사용자 또는 역할을 적용할 수 있습니다. 허브에서 사용자 이름 또는 사용자가 역할에 속하는지 여부와 같은 인증 정보를 클라이언트에 전달할 수도 있습니다.

SignalR은 허브 또는 메서드에 액세스할 수 있는 사용자를 지정하는 Authorize 특성을 제공합니다. 허브 또는 허브의 특정 메서드에 Authorize 특성을 적용합니다. Authorize 특성이 없으면 허브에 연결된 클라이언트에서 허브의 모든 공용 메서드를 사용할 수 있습니다. 허브에 대한 자세한 내용은 SignalR Hubs에 대한 인증 및 권한 부여를 참조하세요.

특성은 Authorize 허브에서만 사용됩니다. 를 사용할 때 권한 부여 규칙을 적용하려면 메서드를 PersistentConnection 재정의 AuthorizeRequest 해야 합니다. 영구 연결에 대한 자세한 내용은 SignalR 영구 연결에 대한 인증 및 권한 부여를 참조하세요.

연결 토큰

SignalR은 보낸 사람의 ID의 유효성을 검사하여 악의적인 명령을 실행할 위험을 완화합니다. 인증된 사용자의 연결 ID와 사용자 이름을 포함하는 연결 토큰은 각 요청에 대해 클라이언트와 서버 간에 전달됩니다. 연결 ID는 새 연결을 만들 때 서버에서 임의로 생성되고 연결 기간 동안 유지되는 고유 식별자입니다. 사용자 이름은 웹 애플리케이션에 대한 인증 메커니즘에서 제공됩니다. 연결 토큰은 암호화 및 디지털 서명으로 보호됩니다.

클라이언트, 서버, 인증 시스템 및 연결 토큰 간의 관계를 보여 주는 연결 토큰 시스템 다이어그램

각 요청에 대해 서버는 토큰 내용의 유효성을 검사하여 지정된 사용자로부터 요청이 들어오는지 확인합니다. 사용자 이름은 연결 ID에 해당해야 합니다. SignalR은 연결 ID와 사용자 이름 모두의 유효성을 검사하여 악의적인 사용자가 다른 사용자를 쉽게 가장하지 못하도록 방지합니다. 서버가 연결 토큰의 유효성을 검사할 수 없는 경우 요청이 실패합니다.

클라이언트, 서버 및 저장된 토큰 간의 관계를 보여 주는 연결 토큰 시스템의 다이어그램

연결 ID는 확인 프로세스의 일부이므로 한 사용자의 연결 ID를 다른 사용자에게 표시하거나 쿠키와 같은 값을 클라이언트에 저장해서는 안 됩니다.

다시 연결할 때 그룹 다시 가입

기본적으로 SignalR 애플리케이션은 연결 시간이 초과되기 전에 연결이 끊어지고 다시 설정되는 경우와 같이 일시적인 중단에서 다시 연결할 때 사용자를 해당 그룹에 자동으로 다시 할당합니다. 다시 연결할 때 클라이언트는 연결 ID 및 할당된 그룹을 포함하는 그룹 토큰을 전달합니다. 그룹 토큰은 디지털 서명되고 암호화됩니다. 클라이언트는 다시 연결 후 동일한 연결 ID를 유지합니다. 따라서 다시 연결된 클라이언트에서 전달된 연결 ID는 클라이언트에서 사용한 이전 연결 ID와 일치해야 합니다. 이 확인은 악의적인 사용자가 다시 연결할 때 권한 없는 그룹에 가입하라는 요청을 전달하지 못하게 합니다.

그러나 그룹 토큰이 만료되지 않는다는 점에 유의해야 합니다. 사용자가 과거에 그룹에 속했지만 해당 그룹에서 금지된 경우 해당 사용자는 금지된 그룹을 포함하는 그룹 토큰을 모방할 수 있습니다. 어떤 사용자가 어떤 그룹에 속하는지 안전하게 관리해야 하는 경우 데이터베이스와 같이 해당 데이터를 서버에 저장해야 합니다. 그런 다음, 사용자가 그룹에 속하는지 여부를 서버에서 확인하는 논리를 애플리케이션에 추가합니다. 그룹 멤버 자격을 확인하는 예제는 그룹 작업을 참조하세요.

그룹 자동 다시 가입은 일시적인 중단 후 연결이 다시 연결된 경우에만 적용됩니다. 사용자가 애플리케이션에서 벗어나거나 애플리케이션이 다시 시작되는 경우 애플리케이션은 해당 사용자를 올바른 그룹에 추가하는 방법을 처리해야 합니다. 자세한 내용은 그룹 작업을 참조하세요.

SignalR이 사이트 간 요청 위조를 방지하는 방법

CSRF(교차 사이트 요청 위조)는 악의적인 사이트가 사용자가 현재 로그인되어 있는 취약한 사이트에 요청을 보내는 공격입니다. SignalR은 악성 사이트에서 SignalR 애플리케이션에 대한 유효한 요청을 만들 가능성이 매우 낮아 CSRF를 방지합니다.

CSRF 공격에 대한 설명

다음은 CSRF 공격의 예입니다.

  1. 사용자가 양식 인증을 사용하여 에 www.example.com로그인합니다.

  2. 서버는 사용자를 인증합니다. 서버의 응답에는 인증 쿠키가 포함됩니다.

  3. 사용자가 로그아웃하지 않고 악의적인 웹 사이트를 방문합니다. 이 악성 사이트에는 다음 HTML 양식이 포함되어 있습니다.

    <h1>You Are a Winner!</h1>
    <form action="http://example.com/api/account" method="post">
        <input type="hidden" name="Transaction" value="withdraw" />
        <input type="hidden" name="Amount" value="1000000" />
        <input type="submit" value="Click Me"/>
    </form>
    

    양식 작업은 악성 사이트가 아닌 취약한 사이트에 게시합니다. CSRF의 “교차 사이트” 부분입니다.

  4. 사용자가 제출 단추를 클릭합니다. 브라우저에는 요청이 포함된 인증 쿠키가 포함됩니다.

  5. 요청은 사용자의 인증 컨텍스트를 사용하여 example.com 서버에서 실행되며 인증된 사용자가 수행할 수 있는 모든 작업을 수행할 수 있습니다.

이 예제에서는 사용자가 양식 단추를 클릭해야 하지만 악성 페이지는 AJAX 요청을 SignalR 애플리케이션으로 보내는 스크립트를 쉽게 실행할 수 있습니다. 또한 SSL을 사용하면 악의적인 사이트에서 "https://" 요청을 보낼 수 있으므로 CSRF 공격을 방지할 수 없습니다.

일반적으로 CSRF 공격은 브라우저가 모든 관련 쿠키를 대상 웹 사이트로 보내기 때문에 인증에 쿠키를 사용하는 웹 사이트에 대해 가능합니다. 그러나 CSRF 공격은 쿠키 악용에 국한되지 않습니다. 예를 들어, 기본 및 다이제스트 인증에도 취약해집니다. 사용자가 기본 또는 다이제스트 인증을 사용하여 로그인하면 브라우저는 세션이 끝날 때까지 자격 증명을 자동으로 보냅니다.

SignalR에서 수행한 CSRF 완화

SignalR은 악의적인 사이트가 SignalR 애플리케이션에 유효한 요청을 만들지 못하도록 다음 단계를 수행합니다. 이러한 단계는 기본적으로 수행되며 코드에서 아무 작업도 필요하지 않습니다.

  • 도메인 간 요청 사용 안 함
    기본적으로 사용자가 외부 도메인에서 SignalR 엔드포인트를 호출하지 못하도록 하려면 SignalR 애플리케이션에서 도메인 간 요청을 사용하지 않도록 설정됩니다. 외부 도메인에서 가져온 모든 요청은 자동으로 유효하지 않은 것으로 간주되어 차단됩니다. 이 기본 동작을 유지하는 것이 좋습니다. 그렇지 않으면 악의적인 사이트가 사용자를 속여 사이트에 명령을 보낼 수 있습니다. 도메인 간 요청을 사용해야 하는 경우 도메인 간 연결을 설정하는 방법을 참조하세요.
  • 쿠키가 아닌 쿼리 문자열에 연결 토큰 전달
    SignalR은 연결 토큰을 쿠키가 아닌 쿼리 문자열 값으로 전달합니다. 연결 토큰을 쿠키로 저장하지 않으면 악성 코드가 발견되면 브라우저에서 연결 토큰을 실수로 전달하지 않습니다. 또한 연결 토큰은 현재 연결 이상으로 유지되지 않습니다. 따라서 악의적인 사용자는 다른 사용자의 인증 자격 증명으로 요청을 할 수 없습니다.
  • 연결 토큰 확인
    연결 토큰 섹션에 설명된 대로 서버는 인증된 각 사용자와 연결된 연결 ID를 알고 있습니다. 서버는 사용자 이름과 일치하지 않는 연결 ID의 요청을 처리하지 않습니다. 악의적인 사용자가 사용자 이름과 현재 임의로 생성된 연결 ID를 알고 있어야 하므로 악의적인 사용자가 유효한 요청을 추측할 가능성은 거의 없습니다. 연결이 종료되는 즉시 해당 연결 ID가 유효하지 않습니다. 익명 사용자는 중요한 정보에 액세스할 수 없어야 합니다.

SignalR 보안 권장 사항

SSL(Secure Socket Layers) 프로토콜

SSL 프로토콜은 암호화를 사용하여 클라이언트와 서버 간의 데이터 전송을 보호합니다. SignalR 애플리케이션이 클라이언트와 서버 간에 중요한 정보를 전송하는 경우 전송에 SSL을 사용합니다. SSL 설정에 대한 자세한 내용은 IIS 7에서 SSL을 설정하는 방법을 참조하세요.

그룹을 보안 메커니즘으로 사용하지 마세요.

그룹은 관련 사용자를 수집하는 편리한 방법이지만 중요한 정보에 대한 액세스를 제한하는 안전한 메커니즘은 아닙니다. 이는 특히 사용자가 다시 연결하는 동안 그룹에 자동으로 다시 참가할 수 있는 경우에 특히 그렇습니다. 대신 권한 있는 사용자를 역할에 추가하고 허브 메서드에 대한 액세스를 해당 역할의 멤버로만 제한하는 것이 좋습니다. 역할에 따라 액세스를 제한하는 예제는 SignalR Hubs에 대한 인증 및 권한 부여를 참조하세요. 다시 연결할 때 그룹에 대한 사용자 액세스를 확인하는 예제는 그룹 작업을 참조하세요.

클라이언트의 입력을 안전하게 처리

악의적인 사용자가 다른 사용자에게 스크립트를 보내지 않도록 다른 클라이언트로 브로드캐스트하기 위한 클라이언트의 모든 입력을 인코딩해야 합니다. SignalR 애플리케이션에는 다양한 유형의 클라이언트가 있을 수 있으므로 서버가 아닌 수신 클라이언트에서 메시지를 인코딩하는 것이 가장 좋습니다. 따라서 HTML 인코딩은 웹 클라이언트에서 작동하지만 다른 유형의 클라이언트에서는 작동하지 않습니다. 예를 들어 채팅 메시지를 표시하는 웹 클라이언트 메서드는 함수를 호출하여 사용자 이름과 메시지를 안전하게 처리합니다 html() .

chat.client.addMessageToPage = function (name, message) {
    // Html encode display name and message. 
    var encodedName = $('<div />').text(name).html();
    var encodedMsg = $('<div />').text(message).html();
    // Add the message to the page. 
    $('#discussion').append('<li><strong>' + encodedName
        + '</strong>:  ' + encodedMsg + '</li>');
};

활성 연결을 사용하여 사용자 상태 변경 내용 조정

활성 연결이 있는 동안 사용자의 인증 상태 변경되면 사용자에게 "활성 SignalR 연결 중에 사용자 ID를 변경할 수 없습니다."라는 오류가 표시됩니다. 이 경우 애플리케이션이 서버에 다시 연결하여 연결 ID와 사용자 이름이 조정되었는지 확인해야 합니다. 예를 들어 애플리케이션에서 활성 연결이 있는 동안 사용자가 로그아웃할 수 있도록 허용하는 경우 연결의 사용자 이름이 다음 요청에 대해 전달된 이름과 더 이상 일치하지 않습니다. 사용자가 로그아웃하기 전에 연결을 중지한 다음 다시 시작하려고 합니다.

그러나 대부분의 애플리케이션은 수동으로 연결을 중지하고 시작할 필요가 없다는 점에 유의해야 합니다. 애플리케이션이 로그아웃 후 사용자를 별도의 페이지(예: Web Forms 애플리케이션 또는 MVC 애플리케이션의 기본 동작)로 리디렉션하거나 로그아웃 후 현재 페이지를 새로 고치면 활성 연결이 자동으로 끊어지고 추가 작업이 필요하지 않습니다.

다음 예제에서는 사용자 상태 변경된 경우 연결을 중지하고 시작하는 방법을 보여줍니다.

<script type="text/javascript">
    $(function () {
        var chat = $.connection.sampleHub;
        $.connection.hub.start().done(function () {
            $('#logoutbutton').click(function () {
                chat.connection.stop();
                $.ajax({
                    url: "Services/SampleWebService.svc/LogOut",
                    type: "POST"
                }).done(function () {
                    chat.connection.start();
                });
            });
        });
    });
</script>

또는 사이트에서 Forms 인증으로 슬라이딩 만료를 사용하고 인증 쿠키를 유효하게 유지하는 활동이 없는 경우 사용자의 인증 상태 변경됩니다. 이 경우 사용자가 로그아웃되고 사용자 이름이 연결 토큰의 사용자 이름과 더 이상 일치하지 않습니다. 인증 쿠키를 유효하게 유지하기 위해 주기적으로 웹 서버에서 리소스를 요청하는 일부 스크립트를 추가하여 이 문제를 해결할 수 있습니다. 다음 예제에서는 30분마다 리소스를 요청하는 방법을 보여 줍니다.

$(function () {
    setInterval(function() {
        $.ajax({
            url: "Ping.aspx",
            cache: false
        });
    }, 1800000);
});

자동으로 생성된 JavaScript 프록시 파일

각 사용자에 대한 JavaScript 프록시 파일에 모든 허브 및 메서드를 포함하지 않으려면 파일의 자동 생성을 사용하지 않도록 설정할 수 있습니다. 여러 허브 및 메서드가 있지만 모든 사용자가 모든 메서드를 인식하지 않도록 하려면 이 옵션을 선택할 수 있습니다. EnableJavaScriptProxiesfalse로 설정하여 자동 생성을 사용하지 않도록 설정합니다.

var hubConfiguration = new HubConfiguration();
hubConfiguration.EnableJavaScriptProxies = false;
RouteTable.Routes.MapHubs("/signalr", hubConfiguration);

JavaScript 프록시 파일에 대한 자세한 내용은 생성된 프록시 및 이 프록시가 수행하는 작업을 참조하세요.

예외

개체가 클라이언트에 중요한 정보를 노출할 수 있으므로 예외 개체를 클라이언트에 전달하지 않아야 합니다. 대신 클라이언트에서 관련 오류 메시지를 표시하는 메서드를 호출합니다.

public Task SampleMethod()
{
    try
    { 
        // code that can throw an exception
    }
    catch(Exception e)
    {
        // add code to log exception and take remedial steps

        return Clients.Caller.DisplayError("Sorry, the request could not be processed.");
    }
}