diff --git a/README.md b/README.md index 9af7ee2..600b0f7 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,8 @@ md := goldmark.NewMarkdown( parser.WithHeadingID(), ), goldmark.WithRendererOptions( - html.WithSoftLineBreak(true), - html.WithXHTML(true), + html.WithHardWraps(), + html.WithXHTML(), ), ) var buf bytes.Buffer @@ -78,8 +78,9 @@ Parser and Renderer options | Functional option | Type | Description | | ----------------- | ---- | ----------- | | `html.WithWriter` | `html.Writer` | `html.Writer` for writing contents to an `io.Writer`. | -| `html.WithSoftLineBreak` | `-` | Render new lines as `
` if true, otherwise `\n` .| +| `html.WithHardWraps` | `-` | Render new lines as `
`.| | `html.WithXHTML` | `-` | Render as XHTML. | +| `html.WithUnsafe` | `-` | By default, goldmark does not render raw HTMLs and potentially dangerous links. With this option, goldmark renders these contents as it is. | ### Built-in extensions @@ -104,6 +105,12 @@ Summary: 3. Write a renderer that implements `renderer.NodeRenderer`. 4. Define your goldmark extension that implements `goldmark.Extender`. +Security +-------------------- +By default, goldmark does not render raw HTMLs and potentially dangerous urls. +If you need to gain more control about untrusted contents, it is recommended to +use HTML sanitizer such as [bluemonday](https://github.com/microcosm-cc/bluemonday). + Benchmark -------------------- You can run this benchmark in the `_benchmark` directory. diff --git a/commonmark_test.go b/commonmark_test.go index 0318cc1..d975900 100644 --- a/commonmark_test.go +++ b/commonmark_test.go @@ -27,7 +27,10 @@ func TestSpec(t *testing.T) { if err := json.Unmarshal(bs, &testCases); err != nil { panic(err) } - markdown := New(WithRendererOptions(html.WithXHTML())) + markdown := New(WithRendererOptions( + html.WithXHTML(), + html.WithUnsafe(), + )) for _, testCase := range testCases { var out bytes.Buffer if err := markdown.Convert([]byte(testCase.Markdown), &out); err != nil { diff --git a/renderer/html/html.go b/renderer/html/html.go index 3fa5470..903f591 100644 --- a/renderer/html/html.go +++ b/renderer/html/html.go @@ -12,27 +12,31 @@ import ( // A Config struct has configurations for the HTML based renderers. type Config struct { - Writer Writer - SoftLineBreak bool - XHTML bool + Writer Writer + HardWraps bool + XHTML bool + Unsafe bool } // NewConfig returns a new Config with defaults. func NewConfig() Config { return Config{ - Writer: DefaultWriter, - SoftLineBreak: false, - XHTML: false, + Writer: DefaultWriter, + HardWraps: false, + XHTML: false, + Unsafe: false, } } // SetOption implements renderer.NodeRenderer.SetOption. func (c *Config) SetOption(name renderer.OptionName, value interface{}) { switch name { - case SoftLineBreak: - c.SoftLineBreak = value.(bool) + case HardWraps: + c.HardWraps = value.(bool) case XHTML: c.XHTML = value.(bool) + case Unsafe: + c.Unsafe = value.(bool) case TextWriter: c.Writer = value.(Writer) } @@ -67,27 +71,27 @@ func WithWriter(writer Writer) interface { return &withWriter{writer} } -// SoftLineBreak is an option name used in WithSoftLineBreak. -const SoftLineBreak renderer.OptionName = "SoftLineBreak" +// HardWraps is an option name used in WithHardWraps. +const HardWraps renderer.OptionName = "HardWraps" -type withSoftLineBreak struct { +type withHardWraps struct { } -func (o *withSoftLineBreak) SetConfig(c *renderer.Config) { - c.Options[SoftLineBreak] = true +func (o *withHardWraps) SetConfig(c *renderer.Config) { + c.Options[HardWraps] = true } -func (o *withSoftLineBreak) SetHTMLOption(c *Config) { - c.SoftLineBreak = true +func (o *withHardWraps) SetHTMLOption(c *Config) { + c.HardWraps = true } -// WithSoftLineBreak is a functional option that indicates whether softline breaks +// WithHardWraps is a functional option that indicates whether softline breaks // should be rendered as '
'. -func WithSoftLineBreak() interface { +func WithHardWraps() interface { renderer.Option Option } { - return &withSoftLineBreak{} + return &withHardWraps{} } // XHTML is an option name used in WithXHTML. @@ -113,6 +117,29 @@ func WithXHTML() interface { return &withXHTML{} } +// Unsafe is an option name used in WithUnsafe. +const Unsafe renderer.OptionName = "Unsafe" + +type withUnsafe struct { +} + +func (o *withUnsafe) SetConfig(c *renderer.Config) { + c.Options[Unsafe] = true +} + +func (o *withUnsafe) SetHTMLOption(c *Config) { + c.Unsafe = true +} + +// WithUnsafe is a functional option that renders dangerous contents +// (raw htmls and potentially dangerous links) as it is. +func WithUnsafe() interface { + renderer.Option + Option +} { + return &withUnsafe{} +} + // A Renderer struct is an implementation of renderer.NodeRenderer that renders // nodes as (X)HTML. type Renderer struct { @@ -255,15 +282,23 @@ func (r *Renderer) renderFencedCodeBlock(w util.BufWriter, source []byte, n *ast func (r *Renderer) renderHTMLBlock(w util.BufWriter, source []byte, n *ast.HTMLBlock, entering bool) ast.WalkStatus { if entering { - l := n.Lines().Len() - for i := 0; i < l; i++ { - line := n.Lines().At(i) - w.Write(line.Value(source)) + if r.Unsafe { + l := n.Lines().Len() + for i := 0; i < l; i++ { + line := n.Lines().At(i) + w.Write(line.Value(source)) + } + } else { + w.WriteString("\n") } } else { if n.HasClosure() { - closure := n.ClosureLine - w.Write(closure.Value(source)) + if r.Unsafe { + closure := n.ClosureLine + w.Write(closure.Value(source)) + } else { + w.WriteString("\n") + } } } return ast.WalkContinue @@ -394,7 +429,9 @@ func (r *Renderer) renderEmphasis(w util.BufWriter, source []byte, n *ast.Emphas func (r *Renderer) renderLink(w util.BufWriter, source []byte, n *ast.Link, entering bool) ast.WalkStatus { if entering { w.WriteString("") + return ast.WalkSkipChildren } func (r *Renderer) renderText(w util.BufWriter, source []byte, n *ast.Text, entering bool) ast.WalkStatus { @@ -442,7 +485,7 @@ func (r *Renderer) renderText(w util.BufWriter, source []byte, n *ast.Text, ente w.Write(segment.Value(source)) } else { r.Writer.Write(w, segment.Value(source)) - if n.HardLineBreak() || (n.SoftLineBreak() && r.SoftLineBreak) { + if n.HardLineBreak() || (n.SoftLineBreak() && r.HardWraps) { if r.XHTML { w.WriteString("
\n") } else { @@ -586,3 +629,28 @@ func (d *defaultWriter) Write(writer util.BufWriter, source []byte) { // DefaultWriter is a default implementation of the Writer. var DefaultWriter = &defaultWriter{} + +var bDataImage = []byte("data:image/") +var bPng = []byte("png;") +var bGif = []byte("gif;") +var bJpeg = []byte("jpeg;") +var bWebp = []byte("webp;") +var bJs = []byte("javascript:") +var bVb = []byte("vbscript:") +var bFile = []byte("file:") +var bData = []byte("data:") + +// IsDangerousURL returns true if given url seems a potentially dangerous url, +// otherwise false. +func IsDangerousURL(url []byte) bool { + if bytes.HasPrefix(url, bDataImage) && len(url) >= 11 { + v := url[11:] + if bytes.HasPrefix(v, bPng) || bytes.HasPrefix(v, bGif) || + bytes.HasPrefix(v, bJpeg) || bytes.HasPrefix(v, bWebp) { + return false + } + return true + } + return bytes.HasPrefix(url, bJs) || bytes.HasPrefix(url, bVb) || + bytes.HasPrefix(url, bFile) || bytes.HasPrefix(url, bData) +}