크로스 사이트 스크립팅 (XSS)

Cross-Site Scripting (XSS)

설명

크로스 사이트 스크립팅(XSS)은 웹 페이지를 만들 때 사용자 입력을 제대로 HTML 이스케이프하지 않고 그대로 출력할 때 발생하는 취약점입니다. 공격자는 <script> 태그나 이벤트 핸들러(onclick 등) 같은 스크립트 코드를 입력값에 섞어 넣고, 이 값이 브라우저에 그대로 출력되면 피해자의 브라우저에서 악성 JavaScript가 실행됩니다. 이 취약점은 쿠키 탈취, 세션 하이재킹, 피싱 페이지 삽입, 키로깅 등으로 악용될 수 있습니다.

잠재적 영향

  • 세션 탈취 및 계정 도용: 스크립트가 읽을 수 있는 쿠키나 토큰을 탈취하거나 사용자의 권한으로 요청을 보낼 수 있습니다.
  • 악성 스크립트 실행: 피해자 브라우저에서 임의의 JavaScript를 실행해, DOM 조작, 악성 사이트 리다이렉트, 키 입력 기록 등이 가능합니다.
  • 피싱 및 UI 변조: 화면 내용을 조작해 가짜 로그인 폼이나 결제 화면을 띄워 민감 정보를 탈취할 수 있습니다.
  • 신뢰도 하락: 사용자가 악성 팝업, 리다이렉트 등을 겪으면서 서비스에 대한 신뢰가 떨어지고, 법적/금전적 손실로 이어질 수 있습니다.

해결 방법

  • HTML 응답에는 신뢰한 템플릿과 html/template를 사용하고, 사용자 입력은 일반 데이터로 전달하세요.
  • http.ResponseWriter.Write() 자체가 위험한 것은 아닙니다. 사용자 입력을 직접 조립한 HTML로 제공하지 마세요.
  • 값이 들어가는 HTML, URL, JavaScript 문맥에 맞게 처리하고, 사용자 입력으로 템플릿 자체를 만들지 마세요.
  • template.HTML, template.URL, template.JS처럼 신뢰를 표시하는 타입으로 외부 입력을 강제 변환하지 마세요.
  • URL이나 이름 등의 형식을 검증하되, 입력 검증이 출력 문맥 처리를 대신한다고 가정하지 마세요.

예시

HTML로 제공되는 응답을 비교합니다. html/template의 URL 문맥 처리는 위험한 스킴과 속성 삽입을 제한하지만, 허용할 외부 링크의 도메인 정책까지 정하지는 않습니다.

변경 전

go
package main

import (
    "fmt"
    "net/http"
)

// 취약한 예: 사용자 입력을 그대로 ResponseWriter에 씀
func errorPage(w http.ResponseWriter, r *http.Request) {
    // 사용자가 직접 조작 가능한 쿼리 파라미터
    url := r.URL.Query().Get("url")

    // 사용자 입력이 들어가는 HTML 템플릿
    const template = `
    <html>
    <body>
      <h1>error; page not found. <a href="%s">go back</a></h1>
    </body>
    </html>`

    // 문자열 포매팅 후 직접 Write
    // 공격자가 url에 "javascript:alert(1)" 또는 "\" onclick=\"alert(1)" 같은 값을 넣을 수 있음
    w.WriteHeader(http.StatusBadRequest)
    w.Write([]byte(fmt.Sprintf(template, url)))
}

func main() {
    http.HandleFunc("/error", errorPage)
    http.ListenAndServe(":8080", nil)
}

변경 후

go
package main

import (
    "html/template"
    "log"
    "net/http"
)

// 안전한 예: html/template 사용으로 자동 이스케이프

// 템플릿에서는 {{.URL}} 위치에 데이터가 들어가며,
// html/template 가 HTML 이스케이프를 자동으로 수행
var errorTmpl = template.Must(template.New("error").Parse(`
<html>
<body>
  <h1>error; page not found. <a href="{{.URL}}">go back</a></h1>
</body>
</html>`))

func errorPageSafe(w http.ResponseWriter, r *http.Request) {
    url := r.URL.Query().Get("url")

    // URL이 비어 있으면 기본값 설정
    if url == "" {
        url = "/"
    }

    w.WriteHeader(http.StatusBadRequest)

    // 템플릿에 데이터 구조 전달
    data := struct {
        URL string
    }{URL: url}

    if err := errorTmpl.Execute(w, data); err != nil {
        log.Println("template execute error:", err)
    }
}

func main() {
    http.HandleFunc("/error", errorPageSafe)
    http.ListenAndServe(":8080", nil)
}

설명:

  • 변경 전: 취약한 코드에서는 fmt.Sprintf()로 HTML 문자열을 직접 만들고, 그 안에 사용자 입력(url 파라미터)을 그대로 삽입한 뒤 w.Write()로 브라우저에 전송합니다. 이 과정에서 HTML 이스케이프가 전혀 적용되지 않기 때문에, 공격자가 url 값에 " onclick="alert(1) 같은 코드를 넣으면 <a> 태그 속성이 깨지면서 임의의 JavaScript를 실행할 수 있습니다. 즉, http.ResponseWriter.Write()를 이용해 사용자 입력이 포함된 HTML을 직접 쓰면 XSS에 매우 취약해집니다.
  • 변경 후: 신뢰한 템플릿의 href 위치에 일반 문자열을 전달합니다. html/template가 URL 문맥을 처리해 위험한 스킴과 속성 경계 탈출을 제한합니다. 사용자 입력을 template.URL이나 template.HTML로 강제 변환해 이 처리를 우회하지 마세요.

참조