
bluemonday: OWASP Java HTML Sanitizer에서 영감을 받아 사용자 생성 콘텐츠에서 XSS를 제거하는 빠른 golang HTML 새니타이저
bluemonday는 Go로 구현된 HTML 살균기(sanitizer)입니다. 빠르고 설정이 매우 자유롭습니다.
bluemonday는 신뢰할 수 없는 사용자 생성 콘텐츠를 입력으로 받아, 승인된 HTML 요소 및 속성의 허용 목록(allowlist)을 기준으로 살균된 HTML을 반환합니다. 따라서 해당 콘텐츠를 웹 페이지에 안전하게 포함할 수 있습니다.
사용자 생성 콘텐츠를 받아들이고 서버가 Go를 사용한다면, 반드시 bluemonday가 필요합니다.
사용자 생성 콘텐츠를 위한 기본 정책(bluemonday.UGCPolicy().Sanitize())은 다음을:
Hello <STYLE>.XSS{background-image:url("javascript:alert('XSS')");}</STYLE><A CLASS=XSS></A>World
무해한 결과로 바꿉니다:
Hello World
그리고 다음을:
<a href="javascript:alert('XSS1')" onmouseover="alert('XSS2')">XSS<a>
다음과 같이 바꿉니다:
XSS
반면에 다음은:
<a href="http://www.google.com/">
<img src="https://ssl.gstatic.com/accounts/ui/logo_2x.png"/>
</a>
거의 변경 없이 통과시킵니다(rel="nofollow"가 추가되는데, 사용자 생성 콘텐츠에는 좋은 것입니다):
<a href="http://www.google.com/" rel="nofollow">
<img src="https://ssl.gstatic.com/accounts/ui/logo_2x.png"/>
</a>
이 라이브러리는 사이트를 XSS 공격으로부터 보호합니다. XSS 공격에는 다양한 경로가 있으며, 위험을 완화하는 가장 좋은 방법은 알려진 안전한 HTML 요소 및 속성 목록을 기준으로 사용자 입력을 살균하는 것입니다.
항상 다른 모든 처리를 마친 후에 bluemonday를 실행해야 합니다.
blackfriday나 Pandoc을 사용한다면 bluemonday는 이 단계들 다음에 실행되어야 합니다. 이렇게 해야 프로세스 후반부에 안전하지 않은 HTML이 유입되지 않습니다.
bluemonday는 OWASP Java HTML Sanitizer와 HTML Purifier에서 크게 영감을 받았습니다.
허용 목록 기반으로, 허용할 HTML 요소와 속성(및 속성의 regexp 패턴)을 설명하는 정책을 직접 구축하거나, 좋은 기본값을 제공하는 기본 정책 중 하나를 사용해야 합니다.
허용 목록이 포함된 정책은 Go 핵심 팀이 구현한 Go net/html 라이브러리의 빠른 비검증(forward only, token 기반) 파서를 사용하여 적용됩니다.
우리는 올바르게 형식화된 HTML(모든 열린 요소에 대해 해당 닫는 요소가 있고, 중첩이 올바른 HTML)이 입력으로 제공될 것으로 기대합니다. 따라서 잘못 중첩되었거나 불완전한 HTML을 복구하는 데는 초점을 맞추지 않습니다. 우리는 단순히 존재하는 모든 요소가 정책 허용 목록에 설명되어 있고, 속성과 링크가 웹 페이지에서 사용하기에 안전한지 확인하는 데 집중합니다. GIGO가 적용되며, 잘못된 HTML을 입력하면 bluemonday가 이를 다시 올바르게 만드는 방법을 알아내는 일은 하지 않습니다.
그렇습니다
우리는 널리 사용되고 현장에서 많이 검증된 OWASP Java HTML Sanitizer에서 전환하여 bluemonday를 프로덕션에서 사용하고 있습니다.
우리는 광범위한 테스트 스위트(AntiSamy 테스트 및 제기된 모든 이슈에 대한 테스트 포함)를 통과하고 있습니다. 미해결 이슈가 있는지 확인하여 차단 요소가 있는지 살펴보세요.
우리는 사용자 생성 콘텐츠를 통한 다양한 공격에 대해 포괄적인 보호를 제공할 수 있도록 풀 리퀘스트와 이슈를 환영합니다.
go get github.com/microcosm-cc/bluemonday로 설치합니다.
그런 다음 호출합니다:
package main
import (
"fmt"
"github.com/microcosm-cc/bluemonday"
)
func main() {
// 고유한 정책별로 한 번만 수행하고, 프로그램 수명 동안 해당 정책을 사용합니다.
// 정책 생성/수정은 여러 고루틴에서 사용하기에 안전하지 않습니다.
p := bluemonday.UGCPolicy()
// 그런 다음 이 정책을 사용하여 많은 입력을 살균할 수 있으며, 여러 고루틴에서도 안전합니다.
html := p.Sanitize(
`<a onblur="alert(secret)" href="http://www.google.com">Google</a>`,
)
// Output:
// <a href="http://www.google.com" rel="nofollow">Google</a>
fmt.Println(html)
}
Sanitize를 호출하는 세 가지 방법을 제공합니다:
p.Sanitize(string) string
p.SanitizeBytes([]byte) []byte
p.SanitizeReader(io.Reader) bytes.Buffer
성능에 집착한다면 p.SanitizeReader(r).Bytes()가 입력이나 출력에 불필요한 캐스팅 없이 []byte를 반환합니다. 다만 그 차이는 무시할 만큼 작으므로 신경 쓸 필요가 없습니다.
자체 정책을 만들 수도 있습니다:
package main
import (
"fmt"
"github.com/microcosm-cc/bluemonday"
)
func main() {
p := bluemonday.NewPolicy()
// URL이 net/url.Parse로 파싱 가능해야 하며 다음 중 하나여야 합니다:
// mailto: http:// 또는 https://
p.AllowStandardURLs()
// <p> 및 <a href="">만 허용합니다.
p.AllowAttrs("href").OnElements("a")
p.AllowElements("p")
html := p.Sanitize(
`<a onblur="alert(secret)" href="http://www.google.com">Google</a>`,
)
// Output:
// <a href="http://www.google.com">Google</a>
fmt.Println(html)
}
기본 정책 두 개를 제공합니다:
bluemonday.StrictPolicy() — 허용 목록에 아무것도 없으므로 모든 HTML 요소와 속성을 제거하는 것과 동일하다고 볼 수 있습니다. 예시 사용 시나리오는 블로그 게시물 제목으로, HTML 태그가 전혀 예상되지 않고 만약 있다면 해당 요소 및 그 내용이 제거되어야 하는 경우입니다. 매우 엄격한 정책입니다.bluemonday.UGCPolicy() — 사용자 생성 콘텐츠에 안전한 광범위한 HTML 요소와 속성 집합을 허용합니다. 이 정책은 iframe, object, embed, styles, script 등을 허용하지 않습니다. 예시 사용 시나리오는 블로그 게시물 본문으로, 다양한 서식과 함께 TABLE 및 IMG가 사용될 가능성이 있는 경우입니다.정책 구축의 핵심은 어떤 HTML 요소와 속성이 시나리오에 안전한지 결정하는 것입니다. OWASP는 위험을 설명하는 XSS 방지 치트 시트를 제공하지만, 기본적으로 다음을 따르면 됩니다:
script, style, iframe, object, embed, base 요소를 피하세요.기본적으로, 시나리오에 어떤 HTML이 괜찮은지 설명할 수 있어야 합니다. 정책을 설명할 자신이 없다면 bluemonday.UGCPolicy() 같은 기본 정책 중 하나를 사용하는 것을 고려하세요.
새 정책을 만들려면:
p := bluemonday.NewPolicy()
정책에 요소를 추가하려면 요소만 추가하거나:
p.AllowElements("b", "strong")
정규식을 사용할 수도 있습니다:
참고: 위와 같이 요소를 이름으로 추가하면 일치하는 정규식은 무시됩니다.
또한 여러 패턴이 겹치지 않도록 하는 것이 좋습니다. 실행 순서가 보장되지 않아 일부 규칙이 누락될 수 있기 때문입니다.
p.AllowElementsMatching(regex.MustCompile(`^my-element-`))
또는 속성을 추가하는 방식으로 요소를 추가할 수 있습니다:
// 아래 .Matching() 사용에 대한 권장 사항을 참고하세요.
p.AllowAttrs("nowrap").OnElements("td", "th")
이 역시 정규식 패턴 일치 대안을 지원합니다:
p.AllowAttrs("nowrap").OnElementsMatching(regex.MustCompile(`^my-element-`))
속성은 모든 요소에 추가하거나:
p.AllowAttrs("dir").Matching(regexp.MustCompile("(?i)rtl|ltr")).Globally()
특정 요소에만 추가할 수 있습니다:
// 아래 .Matching() 사용에 대한 권장 사항이 아닙니다.
p.AllowAttrs("value").OnElements("li")
속성은 항상 패턴에 일치시키는 것이 권장됩니다. 그렇지 않으면 HTML 속성에서의 XSS가 매우 쉽게 발생합니다:
// \p{L}는 유니코드 문자를, \p{N}은 유니코드 숫자를 매칭합니다.
p.AllowAttrs("title").Matching(regexp.MustCompile(`[\p{L}\p{N}\s\-_',:\[\]!\./\\\(\)&]*`)).Globally()
언제든지 멈추고 .Sanitize()를 호출할 수 있습니다:
// HTTP POST에서 전달된 string htmlIn
htmlOut := p.Sanitize(htmlIn)
기존 정책을 가져와 확장할 수도 있습니다:
p := bluemonday.UGCPolicy()
p.AllowElements("fieldset", "select", "option")
인라인 CSS를 Matching 규칙과 함께 AllowAttrs로 처리하는 것이 가능하지만, 허용하려는 모든 인라인 CSS를 안전하게 처리하는 단일 모놀리식 정규식을 작성하는 것은 간단한 작업이 아닙니다. 그렇게 시도하는 대신, 원하는 요소에 style 속성을 허용하고 스타일 정책을 사용하여 인라인 스타일을 제어하고 살균할 수 있습니다.
각 스타일이 요구 사항에 맞는지 확인하려면 Matching(적절한 정규식 사용), MatchingEnum 또는 MatchingHandler를 사용하는 것이 강력히 권장되지만, 가장 널리 사용되는 스타일에는 기본 핸들러가 제공됩니다.
속성과 유사하게, 특정 CSS 속성이 인라인으로 설정되도록 허용할 수 있습니다:
p.AllowAttrs("style").OnElements("span", "p")
// 'color' 속성을 유효한 RGB(A) 16진수 값으로만 허용합니다 ('style' 속성이 허용된 모든 요소에 적용).
p.AllowStyles("color").Matching(regexp.MustCompile("(?i)^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$")).Globally()
또한 CSS 속성이 허용된 값으로만 설정되도록 할 수 있습니다:
p.AllowAttrs("style").OnElements("span", "p")
// 'text-decoration' 속성이 'span' 요소에서만
// 'underline', 'line-through' 또는 'none'으로 설정되도록 허용합니다.
p.AllowStyles("text-decoration").MatchingEnum("underline", "line-through", "none").OnElements("span")
또는 정규식 패턴 일치를 기반으로 요소를 지정할 수 있습니다:
p.AllowAttrs("style").OnElementsMatching(regex.MustCompile(`^my-element-`))
// 'text-decoration' 속성이 'span' 요소에서만
// 'underline', 'line-through' 또는 'none'으로 설정되도록 허용합니다.
p.AllowStyles("text-decoration").MatchingEnum("underline", "line-through", "none").OnElementsMatching(regex.MustCompile(`^my-element-`))
더 구체적인 검사가 필요하다면, 문자열을 받아 특정 속성의 값을 검증하는 bool을 반환하는 핸들러를 만들 수 있습니다. 문자열 매개변수는 소문자로 변환되었고 유니코드 코드 포인트도 변환되었습니다.
myHandler := func(value string) bool{
// 여기서 입력을 검증하세요.
return true
}
p.AllowAttrs("style").OnElements("span", "p")
// 'color' 속성을 핸들러가 검증한 값으로만 허용합니다 ('style' 속성이 허용된 모든 요소에 적용).
p.AllowStyles("color").MatchingHandler(myHandler).Globally()
링크는 안전하게 살균하기 어려운 대상이면서 동시에 악성 콘텐츠의 가장 큰 공격 벡터 중 하나입니다.
다음과 같이 할 수 있습니다:
p.AllowAttrs("href").Matching(regexp.MustCompile(`(?i)mailto|https?`)).OnElements("a")
하지만 이 경우 정규식이 잘못된 값이 예상치 못한 동작을 하는 것을 막기에 불충분하므로, 이 방법은 당신을 보호하지 못합니다.
우리는 링크를 안전하게 처리하기 위한 몇 가지 추가적인 전역 옵션을 제공합니다.
RequireParseableURLs는 URL이 Go의 net/url 패키지로 파싱 가능한지 확인합니다:
p.RequireParseableURLs(true)
파싱 가능한 URL을 활성화했다면 다음 옵션은 AllowRelativeURLs를 허용합니다. 기본적으로 이 옵션은 비활성화되어 있으며(bluemonday는 허용 목록 도구이므로... 무언가를 허용하려면 명시적으로 알려줘야 합니다) 비활성화되면 모든 로컬 및 스킴 상대 URL(즉, href="localpage.html", href="../home.html", 심지어 href="//www.google.com"도 상대 URL)을 차단합니다:
p.AllowRelativeURLs(true)
파싱 가능한 URL을 활성화했다면 허용할 스킴(보통 http와 https를 생각할 때 프로토콜이라고 함)을 허용할 수 있습니다. 위 옵션에서 상대 URL을 허용하면 빈 스킴도 허용된다는 점을 유의하세요:
p.AllowURLSchemes("mailto", "http", "https")
파싱 가능한 URL을 활성화했는지 여부와 관계없이 모든 URL에 rel="nofollow" 속성을 강제할 수 있습니다. href가 유효할 때만 존재하지 않으면 추가됩니다:
// "href" 속성이 있는 "a" "area" "link" 요소에 적용됩니다.
p.RequireNoFollowOnLinks(true)
마찬가지로 모든 URL의 rel 속성에 "noreferrer"를 강제할 수 있습니다.
// "href" 속성이 있는 "a" "area" "link" 요소에 적용됩니다.
p.RequireNoReferrerOnLinks(true)
위의 모든 사항을 적용하는 편의 메서드도 제공하지만, URL 규칙이 적용되도록 링크 가능한 요소를 계속 허용해야 합니다:
p.AllowStandardURLs()
p.AllowAttrs("cite").OnElements("blockquote", "q")
p.AllowAttrs("href").OnElements("a", "area")
p.AllowAttrs("src").OnElements("img")
링크와 관련된 또 다른 복잡성은 RFC2397에 정의된 데이터 URI입니다. 데이터 URI를 사용하면 다음과 같은 형식으로 이미지를 인라인으로 제공할 수 있습니다:
<img src="data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA=">
데이터 URI 링크의 mimetype과 base64 콘텐츠를 검증하는 헬퍼를 제공합니다:
p.AllowDataURIImages()
이 헬퍼는 GIF, JPEG, PNG 및 WEBP 이미지를 활성화합니다.
데이터 URI 링크 사용에는 잠재적인 보안 위험이 있다는 점에 유의해야 합니다. 데이터 URI 링크는 이미 콘텐츠를 신뢰하는 경우에만 활성화해야 합니다.
또한 사용자 생성 콘텐츠를 처리하는 데 도움이 되는 몇 가지 기능도 있습니다:
p.AddTargetBlankToFullyQualifiedLinks(true)
이렇게 하면 완전히 한정된(full qualified, href 대상에 호스트 이름이 포함된) 앵커 <a href="" /> 링크에 target="_blank"가 추가됩니다.
또한 정책이 적용된 후 target="_blank"가 있는 모든 링크에는 rel 속성에 noopener가 추가되도록 조정됩니다. 즉, 링크가 <a href="//host/path"/>로 시작하여 <a href="//host/path" rel="noopener" target="_blank">로 끝날 수 있습니다. noopener의 추가는 보안 기능이지 문제가 아니라는 점을 명심하는 것이 중요합니다. 브라우저에는 target="_blank"로 인해 열린 브라우저 창이 여전히 오프너(웹 페이지)를 제어할 수 있는 불행한 기능이 있으며, 이는 이를 방지합니다. 배경은 여기에서 확인할 수 있습니다: https://dev.to/ben/the-targetblank-vulnerability-by-example
또한 정책 구축을 단순화하는 몇 가지 헬퍼를 번들로 제공합니다:
// "dir", "id", "lang", "title" 속성을 전역적으로 허용합니다.
p.AllowStandardAttributes()
// "img" 요소와 그 표준 속성을 허용합니다.
p.AllowImages()
// 순서 있는 목록, 순서 없는 목록 및 정의 목록을 허용합니다.
p.AllowLists()
// HTML 테이블과 해당되는 모든 요소 및 스타일링이 아닌 속성을 허용합니다.
p.AllowTables()
다음은 유효하지 않습니다:
// 속성이 어디에 허용되는지 명시하지 않았습니다. .Globally() 또는 .OnElements(...)를
// 추가해야 합니다.
// 오류 없이 무시됩니다.
p.AllowAttrs("value")
// 속성이 어디에 허용되는지 명시하지 않았습니다. .Globally() 또는 .OnElements(...)를
// 추가해야 합니다.
// 오류 없이 무시됩니다.
p.AllowAttrs(
"type",
).Matching(
regexp.MustCompile("(?i)^(circle|disc|square|a|A|i|I|1)$"),
)
두 예제 모두 동일한 문제를 보여줍니다. 속성을 선언했지만 속성이 전역적으로 허용되는지 아니면 특정 요소(및 어떤 요소)에서만 허용되는지를 지정하지 않았습니다. 속성은 하나 이상의 요소에 속하며, 정책은 이를 선언해야 합니다.
CSS를 허용하고 살균하는 데 도움이 되는 도구는 아직 포함하지 않습니다. 즉, 단일 정규식에서 그 무거운 작업을 직접 수행하려는 경우가 아니라면(권장하지 않음), 어디에서도 "style" 속성을 허용하지 말아야 합니다.
같은 맥락에서 <script>와 <style>은 모두 유해한 것으로 간주합니다. 이러한 요소(및 그 내용)는 기본적으로 렌더링되지 않으며, 명시적으로 p.AllowUnsafe(true)를 설정해야 합니다. 이러한 요소를 허용하는 것은 JavaScript(및 명백히 작성된 모든 XSS)와 CSS(DOM을 수정하여 JS를 삽입할 수 있음)를 명시적으로 허용하는 것이므로 HTML 살균기를 사용하는 목적을 무색하게 만들며, 또한 이 라이브러리의 한계로 인해 HTML이 유효하게 구조화되었는지 인식하지 못해 이러한 요소가 WhatWG HTML 파서 표준에 내장된 일부 안전 메커니즘을 우회할 수 있다는 점을 알아야 합니다.
bluemonday의 역할은 잘못된 HTML을 고치는 것이 아니라, 악성 HTML이 통과하지 못하도록 막는 것입니다. 짝이 맞지 않는 HTML 요소나 비표준 중첩이 있다면 그대로 남아 있습니다. 그러나 구조가 잘 잡힌 HTML이라면 bluemonday는 그것을 깨뜨리지 않습니다.
bluemonday.UGCPolicy())을 가져와서 자신이 찾는 것의 90%를 포함하지만 요구보다 더 많은 작업을 수행하는 정책에서 원하지 않는 추가 항목을 제거하여 100% 원하는 정책으로 만들 수 있습니다.table 요소가 caption의 하위 요소가 아닌지, colgroup, thead, tbody, tfoot 및 tr이 허용되는지, 문자 데이터가 허용되지 않는지) 검증하는 HTML 모드를 원하는지 조사합니다.