Go의 html/template에서 XSS 보호 이해하기
Grace Collins
Solutions Engineer · Leapcell

소개
웹 개발의 복잡한 세계에서 보안은 단순한 기능이 아니라 근본적인 요구 사항입니다. 웹 애플리케이션이 직면하는 가장 널리 퍼지고 위험한 보안 취약점 중 하나는 사이트 간 스크립팅(XSS)입니다. XSS 공격은 공격자가 다른 사용자가 보는 웹 페이지에 악성 클라이언트 측 스크립트를 삽입할 때 발생합니다. 이러한 스크립트는 사용자 세션을 납치하거나, 웹사이트를 변조하거나, 사용자를 악성 사이트로 리디렉션할 수 있습니다. 많은 프레임워크가 XSS를 완화하는 메커니즘을 제공하지만, 특정 프레임워크가 이를 어떻게 달성하는지 이해하는 것은 강력하고 안전한 애플리케이션을 구축하는 데 중요합니다. 이 글은 Go의 html/template 패키지가 XSS를 뒷전으로 미룬 것이 아니라 설계의 필수적인 부분으로 삼아 어떻게 정면으로 대처하는지 살펴보고, 신뢰할 수 없는 입력을 자동으로 정리하고 이스케이프하는 독자적인 접근 방식에 대해 자세히 알아봅니다.
핵심 개념 및 XSS 공격 벡터 탐색
html/template의 구체적인 내용으로 들어가기 전에, 핵심 개념과 XSS 공격이 나타나는 주요 방식에 대한 공통된 이해를 확립해 봅시다.
사이트 간 스크립팅(XSS): 언급했듯이 XSS는 공격자가 클라이언트 측 스크립트(일반적으로 JavaScript)를 웹 페이지에 삽입할 수 있도록 합니다. 다른 사용자가 손상된 페이지를 방문하면 브라우저가 악성 스크립트를 실행합니다.
공격 벡터:
- 저장된 XSS(영구 XSS): 악성 스크립트는 대상 서버(예: 데이터베이스, 댓글 필드, 포럼 게시물)에 영구적으로 저장됩니다. 다른 사용자가 이 저장된 콘텐츠를 검색하면 스크립트가 제공되어 실행됩니다.
 - 반사된 XSS(비영구 XSS): 악성 스크립트는 웹 서버에서 사용자 브라우저로 반사됩니다. 일반적으로 URL 매개변수에 스크립트를 삽입하는 것이 포함됩니다. 사용자가 특별히 제작된 링크를 클릭하면 서버가 응답에 스크립트를 포함하고, 브라우저가 이를 실행합니다.
 - DOM 기반 XSS: 취약점은 클라이언트 측 JavaScript 코드 자체에 있으며, 이 코드는 사용자 입력을 안전하지 않게 처리하고 페이지의 문서 객체 모델(DOM)을 수정하여 스크립트 실행으로 이어집니다.
 
이스케이핑: 특정 컨텍스트 내에서 해석하기에 안전한 형식으로 데이터의 특수 문자를 변환하는 프로세스입니다. 예를 들어, HTML에서 <를 <로 변환하면 브라우저가 이를 HTML 태그의 시작으로 해석하는 것을 방지합니다.
Go의 html/template: 설계 기반 보안 접근 방식
Go의 html/template 패키지는 단순한 템플릿 엔진이 아니라 보안을 중요하게 생각하는 엔진입니다. 기본 설계 원칙은 XSS와 관련하여 XSS를 방지하는 안전한 웹 애플리케이션을 기본적으로 쉽게 작성할 수 있도록 하는 것입니다. 많은 템플릿 엔진이 명시적으로 이스케이프 함수를 호출해야 하는 것과 달리, html/template는 자동 컨텍스트별 이스케이핑 접근 방식을 취합니다.
컨텍스트별 이스케이핑의 마법
html/template의 XSS 방지 핵심은 데이터가 HTML 문서에 삽입되는 컨텍스트를 이해하는 능력에 있습니다. html/template에 렌더링할 데이터를 전달할 때, 이는 단순히 맹목적으로 삽입하는 것이 아닙니다. 대신 주변 HTML 구조를 분석하여 데이터가 다음 위치에 배치되는지 확인합니다:
- HTML 요소의 내용 내부(예: 
<p>...</p>) - HTML 속성 값 내부(예: 
<a href="...">) - JavaScript 블록의 일부(예: 
<script>...</script>) - CSS 스타일 블록 내부(예: 
<style>...</style>) - URL 경로 또는 쿼리 매개변수로
 
이 컨텍스트를 기반으로 html/template는 잠재적으로 악성인 문자를 무력화하기 위해 가장 적절한 이스케이핑 메커니즘을 적용합니다.
몇 가지 코드 예제를 통해 설명해 보겠습니다.
1. 기본 HTML 콘텐츠 이스케이핑
사용자가 HTML 태그가 포함된 댓글을 제출하는 시나리오를 생각해 봅시다.
package main import ( "html/template" "os" ) func main() { // 잠재적으로 악성 HTML이 포함된 사용자 제출 댓글 userComment := "Hello, <script>alert('XSS!');</script> This is a **bold** comment." // 템플릿 생성 tmpl, err := template.New("comment").Parse(` <!DOCTYPE html> <html> <head><title>User Comment</title></head> <body> <h1>User's Comment:</h1> <p>{{.}}</p> </body> </html> `) if err != nil { panic(err) } // 사용자 댓글로 템플릿 실행 err = tmpl.Execute(os.Stdout, userComment) if err != nil { panic(err) } }
결과:
<!DOCTYPE html> <html> <head><title>User Comment</title></head> <body> <h1>User's Comment:</h1> <p>Hello, <script>alert('XSS!');</script> This is a **bold** comment.</p> </body> </html>
html/template가 <script>를 <script>로, >를 >로, '를 '로 자동 변환했음을 알 수 있습니다. 브라우저는 이제 이 코드를 스크립트를 실행하는 대신 일반 텍스트로 렌더링합니다.
2. HTML 속성 이스케이핑
XSS는 HTML 속성 내에서도 발생할 수 있습니다.
package main import ( "html/template" "os" ) func main() { maliciousURL := `javascript:alert('XSS!');` safeURL := `/users/profile` tmpl, err := template.New("link").Parse(` <!DOCTYPE html> <html> <body> <a href="{{.MaliciousURL}}">Click Me (Malicious)</a> <a href="{{.SafeURL}}">Click Me (Safe)</a> </body> </html> `) if err != nil { panic(err) } data := struct { MaliciousURL template.URL // template.URL을 사용하면 안전하다고 확신하는 경우 자동 이스케이핑을 재정의할 수 있습니다. SafeURL string }{ // Go의 html/template는 이것이 속성으로 보이면 자동으로 이스케이프합니다. "", // 기본 이스케이핑을 보기 위해 `maliciousURL`을 직접 전달하겠습니다. // 명시적으로 문자열을 template.URL로 선언하면 html/template는 이를 신뢰합니다. // 매우 주의해서 사용하고 URL을 완전히 정리한 후에만 사용하세요. // 시연을 위해 기본 동작을 보여주기 위해 여기서는 문자열로 유지하겠습니다. safeURL, } // 악의적인 URL의 경우, 컨텍스트별 이스케이핑을 보여주기 위해 직접 실행하겠습니다. // 템플릿 컨텍스트가 URL임을 알면 "javascript:"를 정리합니다. tmpl, err = template.New("link_malicious").Parse(`<a href="{{.}}">Click Me</a>`) if err != nil { panic(err) } err = tmpl.Execute(os.Stdout, maliciousURL) // 이것은 일반적으로 'javascript:'를 제거하거나 매우 많이 인코딩합니다. if err != nil { panic(err) } // 별도로, 안전한 URL의 경우 tmpl, err = template.New("link_safe").Parse(`<a href="{{.}}">Click Me</a>`) if err != nil { panic(err) } err = tmpl.Execute(os.Stdout, data.SafeURL) if err != nil { panic(err) } }
maliciousURL의 결과는 href="#ZgotmplZ"와 같이 보일 수 있습니다. 이것은 html/template가 잠재적으로 안전하지 않은 URL 체계(javascript:)를 감지하고 이를 안전한 자리 표시자로 대체했음을 나타내는 방식입니다. 단순히 문자를 이스케이프하는 것이 아니라 잠재적으로 위험한 프로토콜을 적극적으로 정리합니다. safeURL의 경우 예상대로 렌더링됩니다.
3. JavaScript 컨텍스트
데이터가 JavaScript 블록에 삽입될 때 html/template는 JavaScript별 이스케이핑을 적용합니다.
package main import ( "html/template" "os" ) func main() { userName := `"; alert('XSS!'); var x = "` // 악의적인 입력 tmpl, err := template.New("script_var").Parse(` <!DOCTYPE html> <html> <body> <script> var user = "{{.}}"; console.log("Welcome, " + user); </script> </body> </html> `) if err != nil { panic(err) } err = tmpl.Execute(os.Stdout, userName) if err != nil { panic(err) } }
결과:
<!DOCTYPE html> <html> <body> <script> var user = "\"; alert('XSS!'); var x = \"; console.log("Welcome, " + user); </script> </body> </html>
여기서 "는 "로, '는 '(작은따옴표의 유니코드 이스케이프)로 이스케이프되어 삽입된 alert('XSS!');가 문자열 컨텍스트에서 벗어나 실행되는 것을 방지합니다.
template.HTML 및 template.URL 타입
html/template는 이스케이핑에 매우 적극적이지만, 합법적으로 이스케이프되지 않은 원시 HTML(예: 다른 라이브러리에서 이미 정리된 리치 텍스트 편집기의 출력)을 출력해야 하는 경우도 있습니다. 이러한 경우 html/template는 template.HTML, template.CSS, template.JS, template.URL 및 template.Srcset와 같은 특수 유형으로 감싸 특정 값에 대한 이스케이핑을 선택 해제할 수 있는 메커니즘을 제공합니다.
package main import ( "html/template" "os" ) func main() { // 신뢰할 수 있는 소스(예: 마크다운 렌더러)에서 사전 정리된 HTML trustedHTML := template.HTML("This is <b>bold</b> text from a trusted source.") // 우리가 신뢰할 수 있는 것으로 처리해서는 안 되는 악의적인 HTML maliciousHTML := "<script>alert('XSS!');</script>" tmpl, err := template.New("common").Parse(` <!DOCTYPE html> <html> <body> <h1>Trusted Content:</h1> <div>{{.Trusted}}</div> <h1>Untrusted Content:</h1> <div>{{.Untrusted}}</div> </body> </html> `) if err != nil { panic(err) } data := struct { Trusted template.HTML Untrusted string }{ Trusted: trustedHTML, Untrusted: maliciousHTML, } err = tmpl.Execute(os.Stdout, data) if err != nil { panic(err) } }
결과:
<!DOCTYPE html> <html> <body> <h1>Trusted Content:</h1> <div>This is <b>bold</b> text from a trusted source.</div> <h1>Untrusted Content:</h1> <div><script>alert('XSS!');</script></div> </body> </html>
보시다시피 template.HTML는 원시 HTML로 렌더링되는 반면, 표준 string 유형은 여전히 자동으로 이스케이프됩니다. 이 메커니즘은 개발자가 특정 데이터 조각에 대한 보안 책임을 명시적으로 오버라이드하는 경우에만 개발자에게 보안 부담을 지웁니다. 이로 인해 부주의로 인한 XSS 취약점의 발생 가능성이 크게 줄어듭니다.
text/template와 html/template의 구분
text/template와 html/template의 차이점을 이해하는 것이 중요합니다. text/template는 자동 이스케이핑을 수행하지 않는 일반 템플릿 엔진입니다. 구성 파일, 이메일과 같은 일반 텍스트 출력을 생성하는 데 적합합니다. HTML 생성을 위해 text/template를 사용하면 모든 이스케이핑에 대한 책임이 전적으로 귀하에게 있으며, 이는 오류가 발생하기 쉽습니다.
반대로 html/template는 HTML 출력 생성을 위해 특별히 설계되었으며, 보여준 것처럼 기본적으로 강력한 XSS 보호 기능을 통합하고 있습니다. HTML 콘텐츠를 렌더링할 때는 항상 html/template를 사용하십시오.
결론
Go의 html/template 패키지는 광범위한 XSS 취약점을 방지하기 위한 강력하고 기본적으로 안전한 접근 방식을 제공합니다. 삽입 컨텍스트에 따라 데이터를 자동으로 이스케이프함으로써, 보안 부담을 개발자에서 템플릿 엔진 자체로 근본적으로 전환합니다. template.HTML와 유사한 유형을 사용하여 원시 콘텐츠를 표시할 수는 있지만, 명시적인 사용은 개발자가 해당 특정 데이터 조각에 대한 책임을 지는 명확한 지표 역할을 합니다. 이 설계 패러다임은 Go 웹 애플리케이션의 보안 상태를 크게 향상시켜 XSS 결함을 실수로 도입하기가 극도로 어렵게 만듭니다. 안전한 웹 경험을 위해 html/template는 Go의 강력하고 안전한 소프트웨어 개발에 대한 약속의 초석으로 자리 잡고 있습니다.