설명
ASP.NET Core의 ControllerBase.Content, Results.Content, TypedResults.Content는 전달된 문자열을 HTML 인코딩하지 않습니다. 요청에서 가져온 문자열을 text/html 응답으로 직접 반환하면 브라우저가 그 값을 데이터가 아닌 마크업으로 파싱합니다. 공격자는 태그, 이벤트 처리기 또는 스크립트 실행이 가능한 다른 구문을 주입해 반사형 크로스 사이트 스크립팅(XSS)을 일으킬 수 있습니다.
입력 검증과 출력 인코딩은 서로 다른 통제입니다. 입력 형식 검증은 공격 표면을 줄일 수 있지만 HTML, 속성, URL, JavaScript, CSS의 각 파서 컨텍스트에 필요한 출력 처리를 대신하지 못합니다. 이름이 Sanitize, Encode, Clean인 사용자 정의 함수도 구현과 적용 컨텍스트를 확인하기 전에는 안전한 변환으로 간주할 수 없습니다.
잠재적 영향
- 사용자의 세션 또는 민감 정보 탈취
- 피싱 UI 삽입과 페이지 내용 변조
- 사용자의 권한으로 동작하는 요청 또는 클라이언트 측 작업 실행
- 신뢰된 사이트 출처를 이용한 악성 콘텐츠 배포
해결 방법
가능하면 원시 HTML 문자열을 조립해 반환하지 말고, Razor가 일반 문자열을 출력 시점에 인코딩하도록 하세요. Razor의 일반 @value 출력은 HTML 컨텍스트에서 문자열을 인코딩하지만, 신뢰할 수 없는 값에 Html.Raw, HtmlString 또는 다른 IHtmlContent 우회 경로를 사용하면 안 됩니다.
마크업이 필요하지 않으면 응답을 text/plain 또는 구조화된 데이터 형식으로 반환하세요. HTML 텍스트 노드에 값을 넣어야 한다면 최종 출력 경계에서 신뢰할 수 없는 값 전체를 WebUtility.HtmlEncode 또는 HtmlEncoder.Default.Encode로 인코딩합니다. URL, JavaScript, CSS에는 HTML 인코더를 재사용하지 말고 해당 컨텍스트용 인코더나 안전한 데이터 전달 방식을 사용하세요. 특히 JavaScript에 값을 직접 이어 붙이기보다 인코딩된 HTML 데이터 속성이나 JSON으로 전달한 뒤 textContent 같은 안전한 싱크로 읽는 방식을 우선합니다.
사용자가 리치 HTML을 작성해야 한다면 일반 출력 인코딩만으로는 서식을 유지할 수 없습니다. 이 경우 렌더링 환경에 맞는 유지보수 중인 허용 목록 기반 HTML 새니타이저를 렌더링 경계에서 사용하세요. 브라우저에서 렌더링한다면 최신 DOMPurify가 대표적인 선택입니다. 정확한 삽입 컨텍스트에 맞게 새니타이즈하고, 새니타이즈된 결과를 삽입 전에 다시 조합하거나 수정하지 마세요. Element.setHTML()은 아직 모든 지원 환경에서 사용할 수 있는 Baseline API가 아니므로 일반적인 호환성 해결책으로 가정하지 않습니다.
예시
변경 전
컨트롤러 응답
using Microsoft.AspNetCore.Mvc;
public sealed class PreviewController : ControllerBase
{
[HttpGet("/preview")]
public IActionResult Preview([FromQuery] string html)
{
return Content(html, "text/html");
}
}
html은 요청에서 직접 바인딩되며 Content는 값을 인코딩하지 않습니다.
Minimal API 응답
using Microsoft.AspNetCore.Mvc;
app.MapGet("/preview", ([FromQuery] string html) =>
Results.Content(html, "text/html"));
변경 후
권장: Razor의 일반 인코딩 출력
using Microsoft.AspNetCore.Mvc;
public sealed class PreviewController : Controller
{
[HttpGet("/preview-name")]
public IActionResult Preview([FromQuery] string name)
{
return View("Preview", model: name);
}
}
<h1>@Model</h1>
Razor의 일반 문자열 출력은 HTML로 인코딩됩니다. 같은 값에 Html.Raw(Model)을 사용하면 이 보호가 사라집니다.
권장: 마크업이 필요하지 않은 응답
public IActionResult Preview([FromQuery] string text)
{
return Content(text, "text/plain");
}
제한된 경우: HTML 텍스트 노드 인코딩
using System.Text.Encodings.Web;
public IActionResult Preview([FromQuery] string name)
{
var encodedName = HtmlEncoder.Default.Encode(name);
return Content($"<h1>{encodedName}</h1>", "text/html");
}
이 예시는 값이 HTML 텍스트 노드에만 들어가는 경우를 대상으로 합니다. 같은 인코딩 결과를 <script>, 스타일, URL 또는 이벤트 처리기 속성에 넣는 것은 안전성을 보장하지 않습니다.
리치 HTML을 허용해야 하는 경우
서버는 콘텐츠를 데이터로 전달하고, 브라우저에서 실제 삽입 직전에 최신 DOMPurify로 새니타이즈할 수 있습니다.
const cleanHtml = DOMPurify.sanitize(untrustedHtml);
preview.innerHTML = cleanHtml;
DOMPurify 버전을 최신 상태로 유지하고, 새니타이즈 후 다른 템플릿이나 문자열 처리기가 결과를 변경하지 않도록 하세요. 서버 측 새니타이저를 선택한다면 활성 유지보수 여부, 허용 요소와 속성 정책, URL 프로토콜 처리, 대상 HTML 컨텍스트를 별도로 검증해야 합니다.
참조
- Microsoft: Prevent Cross-Site Scripting (XSS) in ASP.NET Core
- Microsoft: Razor syntax reference — Expression encoding
- Microsoft: ControllerBase.Content
- Microsoft: Results.Content
- Microsoft: TypedResults.Content
- OWASP Cross Site Scripting Prevention Cheat Sheet
- OWASP ASVS 5.0.0
- WHATWG HTML Standard: Dynamic markup insertion
- MDN: Element.setHTML()
- DOMPurify Security Goals & Threat Model
- CWE-79: Improper Neutralization of Input During Web Page Generation