Add WithUnsafe option

This commit is contained in:
yuin 2019-04-27 15:30:16 +09:00
parent 0cca3c1ea1
commit 77aabc8f8e
3 changed files with 110 additions and 32 deletions

View file

@ -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 `<br>` if true, otherwise `\n` .|
| `html.WithHardWraps` | `-` | Render new lines as `<br>`.|
| `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.

View file

@ -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 {

View file

@ -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 '<br>'.
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("<!-- raw HTML omitted -->\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("<!-- raw HTML omitted -->\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("<a href=\"")
w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
if r.Unsafe || !IsDangerousURL(n.Destination) {
w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
}
w.WriteByte('"')
if n.Title != nil {
w.WriteString(` title="`)
@ -412,7 +449,9 @@ func (r *Renderer) renderImage(w util.BufWriter, source []byte, n *ast.Image, en
return ast.WalkContinue
}
w.WriteString("<img src=\"")
w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
if r.Unsafe || !IsDangerousURL(n.Destination) {
w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
}
w.WriteString(`" alt="`)
w.Write(n.Text(source))
w.WriteByte('"')
@ -430,7 +469,11 @@ func (r *Renderer) renderImage(w util.BufWriter, source []byte, n *ast.Image, en
}
func (r *Renderer) renderRawHTML(w util.BufWriter, source []byte, n *ast.RawHTML, entering bool) ast.WalkStatus {
return ast.WalkContinue
if r.Unsafe {
return ast.WalkContinue
}
w.WriteString("<!-- raw HTML omitted -->")
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("<br />\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)
}