ASP.NET Core XSS(교차 사이트 스크립팅) 방지

작성자: Rick Anderson

XSS(교차 사이트 스크립팅)는 공격자가 클라이언트 쪽 스크립트(일반적으로 JavaScript)를 웹 페이지에 배치할 수 있도록 하는 보안 취약성입니다. 다른 사용자가 영향을 받는 페이지를 로드하면 공격자의 스크립트가 실행되어 공격자가 S 및 세션 토큰을 도용 cookie하거나, DOM 조작을 통해 웹 페이지의 콘텐츠를 변경하거나, 브라우저를 다른 페이지로 리디렉션할 수 있습니다. XSS 취약성은 일반적으로 애플리케이션이 사용자 입력을 가져와 유효성 검사, 인코딩 또는 이스케이프하지 않고 페이지에 출력할 때 발생합니다.

이 문서는 주로 XSS에 취약할 수 있는 HTML을 반환하는 보기, Razor 페이지 및 기타 앱이 있는 ASP.NET Core MVC에 적용됩니다. HTML, XML 또는 JSON 형식으로 데이터를 반환하는 웹 API는 API에서 클라이언트 앱이 얼마나 신뢰하는지에 따라 사용자 입력을 제대로 삭제하지 않으면 클라이언트 앱에서 XSS 공격을 트리거할 수 있습니다. 예를 들어 API가 사용자 생성 콘텐츠를 수락하고 HTML 응답에서 반환하는 경우 공격자는 응답이 사용자의 브라우저에서 렌더링될 때 실행되는 콘텐츠에 악성 스크립트를 삽입할 수 있습니다.

XSS 공격을 방지하기 위해 웹 API는 입력 유효성 검사 및 출력 인코딩을 구현해야 합니다. 입력 유효성 검사를 통해 사용자 입력이 예상 조건을 충족하고 악성 코드를 포함하지 않습니다. 출력 인코딩을 사용하면 API에서 반환된 모든 데이터가 제대로 삭제되므로 사용자의 브라우저에서 코드로 실행할 수 없습니다. 자세한 내용은 GitHub 이슈를 참조하세요.

XSS로부터 애플리케이션 보호

기본 수준에서 XSS는 애플리케이션을 속여 렌더링된 페이지에 태그를 <script> 삽입하거나 요소에 이벤트를 삽입하여 On* 작동합니다. 개발자는 애플리케이션에 XSS를 도입하지 않도록 다음 방지 단계를 사용해야 합니다.

  1. 아래의 나머지 단계를 수행하지 않는 한 신뢰할 수 없는 데이터를 HTML 입력에 넣지 마세요. 신뢰할 수 없는 데이터는 공격자가 애플리케이션을 위반할 수 없더라도 데이터베이스를 위반할 수 있으므로 HTML 양식 입력, 쿼리 문자열, HTTP 헤더 또는 데이터베이스에서 원본 데이터와 같이 공격자가 제어할 수 있는 모든 데이터입니다.

  2. 신뢰할 수 없는 데이터를 HTML 요소 내에 배치하기 전에 HTML로 인코딩되었는지 확인합니다. HTML 인코딩은 문자와 같은 < 문자를 가져와서 안전한 형식(예: <)으로 변경합니다.

  3. 신뢰할 수 없는 데이터를 HTML 특성에 넣기 전에 HTML로 인코딩되었는지 확인합니다. HTML 특성 인코딩은 HTML 인코딩의 상위 집합이며 " 및 "와 같은 추가 문자를 인코딩합니다.

  4. 신뢰할 수 없는 데이터를 JavaScript에 넣기 전에 런타임에 검색하는 콘텐츠가 있는 HTML 요소에 데이터를 배치합니다. 가능하지 않은 경우 데이터가 JavaScript로 인코딩되었는지 확인합니다. JavaScript 인코딩은 JavaScript에 대해 위험한 문자를 사용하고 16진수로 바꿉니다. 예를 들어 < 로 인코딩 \u003C됩니다.

  5. 신뢰할 수 없는 데이터를 URL 쿼리 문자열에 배치하기 전에 URL이 인코딩되었는지 확인합니다.

Razor를 사용한 HTML 인코딩

MVC에서 사용되는 Razor 엔진은 실제로 그렇게 하지 않도록 하기 위해 실제로 노력하지 않는 한 변수에서 가져온 모든 출력을 자동으로 인코딩합니다. @ 지시문을 사용할 때마다 HTML 특성 인코딩 규칙을 사용합니다. HTML 특성 인코딩은 HTML 인코딩의 상위 집합이기 때문에 HTML 인코딩 또는 HTML 특성 인코딩을 사용해야 하는지 여부에 대해 염려할 필요가 없습니다. 신뢰할 수 없는 입력을 JavaScript에 직접 삽입하려는 경우가 아니라면 HTML 컨텍스트에서만 @를 사용해야 합니다. 태그 도우미는 태그 매개 변수에 사용하는 입력도 인코딩합니다.

다음 Razor 보기를 수행합니다.

@{
    var untrustedInput = "<\"123\">";
}

@untrustedInput

이 보기는 untrustedInput 변수의 내용을 출력합니다. 이 변수에는 XSS 공격에 사용되는 일부 문자(즉, <, " 및 >)가 포함됩니다. 원본을 검사하면 다음과 같이 인코딩되는 렌더링된 출력이 표시됩니다.

&lt;&quot;123&quot;&gt;

Warning

ASP.NET Core MVC는 출력 시 자동으로 인코딩되지 않는 HtmlString 클래스를 제공합니다. XSS 취약성이 노출되기 때문에 신뢰할 수 없는 입력과 함께 사용해서는 안 됩니다.

Razor를 사용한 JavaScript 인코딩

보기에서 처리하기 위해 JavaScript에 값을 삽입하려는 경우가 있습니다. 두 가지 방법으로 이 작업을 수행할 수 있습니다. 값을 삽입하는 가장 안전한 방법은 태그의 데이터 특성에 값을 배치하고 JavaScript에서 검색하는 것입니다. 예시:

@{
    var untrustedInput = "<script>alert(1)</script>";
}

<div id="injectedData"
     data-untrustedinput="@untrustedInput" />

<div id="scriptedWrite" />
<div id="scriptedWrite-html5" />

<script>
    var injectedData = document.getElementById("injectedData");

    // All clients
    var clientSideUntrustedInputOldStyle =
        injectedData.getAttribute("data-untrustedinput");

    // HTML 5 clients only
    var clientSideUntrustedInputHtml5 =
        injectedData.dataset.untrustedinput;

    // Put the injected, untrusted data into the scriptedWrite div tag.
    // Do NOT use document.write() on dynamically generated data as it
    // can lead to XSS.

    document.getElementById("scriptedWrite").innerText += clientSideUntrustedInputOldStyle;

    // Or you can use createElement() to dynamically create document elements
    // This time we're using textContent to ensure the data is properly encoded.
    var x = document.createElement("div");
    x.textContent = clientSideUntrustedInputHtml5;
    document.body.appendChild(x);

    // You can also use createTextNode on an element to ensure data is properly encoded.
    var y = document.createElement("div");
    y.appendChild(document.createTextNode(clientSideUntrustedInputHtml5));
    document.body.appendChild(y);

</script>

이전 태그는 다음 HTML을 생성합니다.

<div id="injectedData"
     data-untrustedinput="&lt;script&gt;alert(1)&lt;/script&gt;" />

<div id="scriptedWrite" />
<div id="scriptedWrite-html5" />

<script>
    var injectedData = document.getElementById("injectedData");

    // All clients
    var clientSideUntrustedInputOldStyle =
        injectedData.getAttribute("data-untrustedinput");

    // HTML 5 clients only
    var clientSideUntrustedInputHtml5 =
        injectedData.dataset.untrustedinput;

    // Put the injected, untrusted data into the scriptedWrite div tag.
    // Do NOT use document.write() on dynamically generated data as it can
    // lead to XSS.

    document.getElementById("scriptedWrite").innerText += clientSideUntrustedInputOldStyle;

    // Or you can use createElement() to dynamically create document elements
    // This time we're using textContent to ensure the data is properly encoded.
    var x = document.createElement("div");
    x.textContent = clientSideUntrustedInputHtml5;
    document.body.appendChild(x);

    // You can also use createTextNode on an element to ensure data is properly encoded.
    var y = document.createElement("div");
    y.appendChild(document.createTextNode(clientSideUntrustedInputHtml5));
    document.body.appendChild(y);

</script>

이전 코드는 다음 출력을 생성합니다.

<script>alert(1)</script>
<script>alert(1)</script>
<script>alert(1)</script>

Warning

JavaScript에서 신뢰할 수 없는 입력을 연결하여 DOM 요소를 만들거나 동적으로 생성된 콘텐츠에 document.write()를 사용하지 마세요.

다음 방식 중 하나를 사용하여 코드가 DOM 기반 XSS에 노출되지 않도록 합니다.

  • createElement() 및 적절한 메서드 또는 속성(예: node.textContent= 또는 node.InnerText=)을 사용하여 속성 값을 할당합니다.
  • document.CreateTextNode() 및 적절한 DOM 위치에 추가합니다.
  • element.SetAttribute()
  • element[attribute]=

코드에서 인코더 액세스

HTML, JavaScript 및 URL 인코더는 다음 두 가지 방법으로 코드에서 사용할 수 있습니다.

  • 종속성 주입을 통해 삽입합니다.
  • 네임스페이스에 포함된 기본 인코더를 System.Text.Encodings.Web 사용합니다.

기본 인코더를 사용하는 경우 안전으로 처리할 문자 범위에 적용된 모든 사용자 지정은 적용되지 않습니다. 기본 인코더는 가능한 가장 안전한 인코딩 규칙을 사용합니다.

DI를 통해 구성 가능한 인코더를 사용하려면 생성자는 HtmlEncoder, JavaScriptEncoderUrlEncoder 매개 변수를 적절하게 사용해야 합니다. 예를 들어;

public class HomeController : Controller
{
    HtmlEncoder _htmlEncoder;
    JavaScriptEncoder _javaScriptEncoder;
    UrlEncoder _urlEncoder;

    public HomeController(HtmlEncoder htmlEncoder,
                          JavaScriptEncoder javascriptEncoder,
                          UrlEncoder urlEncoder)
    {
        _htmlEncoder = htmlEncoder;
        _javaScriptEncoder = javascriptEncoder;
        _urlEncoder = urlEncoder;
    }
}

URL 매개 변수 인코딩

신뢰할 수 없는 입력을 값으로 사용하여 URL 쿼리 문자열을 빌드하려면 UrlEncoder를 사용하여 값을 인코딩합니다. 예를 들면 다음과 같습니다.

var example = "\"Quoted Value with spaces and &\"";
var encodedValue = _urlEncoder.Encode(example);

인코딩한 후 encodedValue 변수에 .가 포함됩니다.%22Quoted%20Value%20with%20spaces%20and%20%26%22 공백, 따옴표, 문장 부호 및 기타 안전하지 않은 문자는 16진수 값으로 인코딩됩니다. 예를 들어 공백 문자는 %20이 됩니다.

Warning

신뢰할 수 없는 입력을 URL 경로의 일부로 사용하지 마세요. 항상 신뢰할 수 없는 입력을 쿼리 문자열 값으로 전달합니다.

인코더 사용자 지정

기본적으로 인코더에서는 기본 라틴어 유니코드 범위로 제한된 안전 목록을 사용하고 해당 범위 밖의 모든 문자를 해당 문자 코드와 동등한 것으로 인코딩합니다. 이 동작은 인코더를 Razor 사용하여 문자열을 출력하기 때문에 TagHelper 및 HtmlHelper 렌더링에도 영향을 줍니다.

그 이유는 알 수 없거나 향후 브라우저 버그로부터 보호하기 위한 것입니다(이전 브라우저 버그가 영어가 아닌 문자 처리에 따라 구문 분석이 진행되었습니다). 웹 사이트에서 중국어, 키릴 자모 등의 라틴 문자가 아닌 문자를 많이 사용하는 경우 이는 원하는 동작이 아닐 수 있습니다.

인코더 안전 목록은 Program.cs에서 시작하는 동안 앱에 적합한 유니코드 범위를 포함하도록 사용자 지정할 수 있습니다.

예를 들어 다음과 유사한 Razor HtmlHelper를 사용하여 기본 구성을 사용합니다.

<p>This link text is in Chinese: @Html.ActionLink("汉语/漢語", "Index")</p>

위의 태그는 중국어 텍스트로 인코딩되어 렌더링됩니다.

<p>This link text is in Chinese: <a href="/">&#x6C49;&#x8BED;/&#x6F22;&#x8A9E;</a></p>

인코더에서 안전한 것으로 처리되는 문자를 넓히려면 Program.cs에 다음 줄을 삽입합니다.

builder.Services.AddSingleton<HtmlEncoder>(
     HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,
                                               UnicodeRanges.CjkUnifiedIdeographs }));

ConfigureServices()에서 시작하는 동안 애플리케이션에 적합한 유니코드 범위를 포함하도록 인코더 안전 목록을 사용자 지정할 수 있습니다.

예를 들어 기본 구성을 사용하면 다음과 같이 Razor HtmlHelper를 사용할 수 있습니다.

<p>This link text is in Chinese: @Html.ActionLink("汉语/漢語", "Index")</p>

웹 페이지의 원본을 보면 중국어 텍스트가 인코딩된 상태에서 다음과 같이 렌더링된 것을 볼 수 있습니다.

<p>This link text is in Chinese: <a href="/">&#x6C49;&#x8BED;/&#x6F22;&#x8A9E;</a></p>

인코더에서 안전한 것으로 처리되는 문자를 넓히려면 startup.csConfigureServices() 메서드에 다음 줄을 삽입합니다.

services.AddSingleton<HtmlEncoder>(
     HtmlEncoder.Create(allowedRanges: new[] { UnicodeRanges.BasicLatin,
                                               UnicodeRanges.CjkUnifiedIdeographs }));

이 예제에서는 유니코드 범위 CjkUnifiedIdeographs를 포함하도록 안전 목록을 확장합니다. 렌더링된 출력은 이제 다음이 됩니다.

<p>This link text is in Chinese: <a href="/">汉语/漢語</a></p>

안전 목록 범위는 언어가 아닌 유니코드 코드 차트로 지정됩니다. 유니코드 표준에는 문자가 포함된 차트를 찾는 데 사용할 수 있는 코드 차트 목록이 있습니다. 각 인코더(Html, JavaScript 및 Url)는 별도로 구성해야 합니다.

참고 항목

안전 목록의 사용자 지정은 DI를 통해 제공된 인코더에만 영향을 미칩니다. System.Text.Encodings.Web.*Encoder.Default를 통해 인코더에 직접 액세스하는 경우 기본 라틴어 전용 안전 목록이 사용됩니다.

인코딩은 어디에서 수행해야 하나요?

일반적으로 허용되는 방법은 인코딩이 출력 지점에서 수행되며 인코딩된 값은 데이터베이스에 저장되지 않아야 한다는 것입니다. 출력 지점에서 인코딩하면 데이터 사용을 HTML에서 쿼리 문자열 값으로 변경할 수 있습니다. 또한 검색하기 전에 값을 인코딩하지 않고도 데이터를 쉽게 검색할 수 있으며 인코더에 대한 변경 내용 또는 버그 수정을 활용할 수 있습니다.

XSS 방지 기술로서의 유효성 검사

유효성 검사는 XSS 공격을 제한하는 데 유용한 도구일 수 있습니다. 예를 들어 0~9 문자만 포함된 숫자 문자열은 XSS 공격을 트리거하지 않습니다. 사용자 입력에서 HTML을 수락하면 유효성 검사가 더 복잡해집니다. HTML 입력을 구문 분석하는 것은 불가능하지 않더라도 어렵습니다. 포함된 HTML을 제거하는 파서와 결합된 Markdown은 풍부한 입력을 허용하는 더 안전한 옵션입니다. 유효성 검사만 사용해서는 안됩니다. 유효성 검사 또는 삭제가 수행되었는지에 관계없이 항상 신뢰할 수 없는 입력을 출력 전에 인코딩합니다.