Skip to content

Commit

Permalink
feat: add support for plain HTML inside elements, not just Go express…
Browse files Browse the repository at this point in the history
…ions - fixes #22
  • Loading branch information
a-h committed Oct 10, 2021
1 parent 1229489 commit 4eb4878
Show file tree
Hide file tree
Showing 18 changed files with 359 additions and 72 deletions.
107 changes: 57 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,46 +24,6 @@ The language generates Go code, some sections of the template (e.g. `package`, `
* `templ fmt` formats template files in the current directory tree.
* `templ lsp` provides a Language Server to support IDE integrations. The compile command generates a sourcemap which maps from the `*.templ` files to the compiled Go file. This enables the `templ` LSP to use the Go language `gopls` language server as is, providing a thin shim to do the source remapping. This is used to provide autocomplete for template variables and functions.

## Security

templ will automatically escape content according to the following rules.

```
{% templ Example() %}
<script type="text/javascript">
{%= "will be HTML encoded using templ.Escape, which isn't JavaScript-aware, don't use templ to build scripts" %}
</script>
<div onClick={%= "will be HTML encoded using templ.Escape, but this isn't JavaScript aware, don't use user-controlled data here" %}>
{%= "will be HTML encoded using templ.Escape" %}</div>
</div>
<style type="text/css">
{%= "will be escaped using templ.Escape, which isn't CSS-aware, don't use user-controlled data here" %}
</style>
<div style={%= "will be HTML encoded using templ.Escape, which isn't CSS-aware, don't use user controlled data here" %}</div>
<div class={%= templ.CSSClasses(templ.Class("will not be escaped, because it's expected to be a constant value")) %}</div>
<div>{%= "will be escaped using templ.Escape" %}</div>
<a href="http://constants.example.com/are/not/sanitized">Text</a>
<a href={%= templ.URL("will be sanitized by templ.URL to remove potential attacks") %}</div>
<a href={%= templ.SafeURL("will not be sanitized by templ.URL") %}</div>
{% endtempl %}
```

CSS property names, and constant CSS property values are not sanitized or escaped.

```
{% css className() %}
background-color: #ffffff;
{% endcss %}
```

CSS property values based on expressions are passed through `templ.SanitizeCSS` to replace potentially unsafe values with placeholders.

```
{% css className() %}
color: {%= red %};
{% endcss %}
```

## Design

### Overview
Expand Down Expand Up @@ -192,31 +152,37 @@ Templ provides a `templ.URL` function that sanitizes input URLs and checks that

### Text

Text is rendered from Go expressions, which includes constant values:
Text is rendered from HTML included in the template itself, or by using Go expressions. No processing or conversion is applied to HTML included within the template, whereas Go string expressions are HTML encoded on output.

```
{%= "this is a string" %}
Plain HTML:

```html
<div>Plain HTML is allowed.</div>
```

Using the backtick format (single-line only):
Constant Go expressions:

```
{%= `this is also a string` %}
<div>{%= "this is a string" %}</div>
```

Calling a function that returns a string:
The backtick constant expression (single-line only):

```
{%= time.Now().String() %}
<div>{%= `this is also a string` %}</div>
```

Or using a string parameter, or variable that's in scope.
Functions that return a string:

```
{%= v.s %}
<div>{%= time.Now().String() %}</div>
```

What you can't do, is write text directly between elements (e.g. `<div>Some text</div>`, because the parser would have to become more complex to support HTML entities and the various mistakes people make when they're doing that (bare ampersands etc.). Go strings support UTF-8 which is much easier, and the escaping rules are well known by Go programmers.
A string parameter, or variable that's in scope:

```
<div>{%= v.s %}</div>
```

### CSS

Expand Down Expand Up @@ -549,3 +515,44 @@ Please get in touch if you're interested in building a feature as I don't want p
* https://adrianhesketh.com/2021/05/18/introducing-templ/
* https://adrianhesketh.com/2021/05/28/templ-hot-reload-with-air/
* https://adrianhesketh.com/2021/06/04/hotwired-go-with-templ/

## Security

templ will automatically escape content according to the following rules.

```
{% templ Example() %}
<script type="text/javascript">
{%= "will be HTML encoded using templ.Escape, which isn't JavaScript-aware, don't use templ to build scripts" %}
</script>
<div onClick={%= "will be HTML encoded using templ.Escape, but this isn't JavaScript aware, don't use user-controlled data here" %}>
{%= "will be HTML encoded using templ.Escape" %}</div>
</div>
<style type="text/css">
{%= "will be escaped using templ.Escape, which isn't CSS-aware, don't use user-controlled data here" %}
</style>
<div style={%= "will be HTML encoded using templ.Escape, which isn't CSS-aware, don't use user controlled data here" %}</div>
<div class={%= templ.CSSClasses(templ.Class("will not be escaped, because it's expected to be a constant value")) %}</div>
<div>{%= "will be escaped using templ.Escape" %}</div>
<a href="http://constants.example.com/are/not/sanitized">Text</a>
<a href={%= templ.URL("will be sanitized by templ.URL to remove potential attacks") %}</div>
<a href={%= templ.SafeURL("will not be sanitized by templ.URL") %}</div>
{% endtempl %}
```

CSS property names, and constant CSS property values are not sanitized or escaped.

```
{% css className() %}
background-color: #ffffff;
{% endcss %}
```

CSS property values based on expressions are passed through `templ.SanitizeCSS` to replace potentially unsafe values with placeholders.

```
{% css className() %}
color: {%= red %};
{% endcss %}
```

32 changes: 32 additions & 0 deletions generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,8 @@ func (g *generator) writeNode(indentLevel int, node parser.Node) error {
g.writeStringExpression(indentLevel, n.Expression)
case parser.Whitespace:
// Whitespace is not included in template output to minify HTML.
case parser.Text:
g.writeText(indentLevel, n)
default:
g.w.Write(fmt.Sprintf("Unhandled type: %v\n", reflect.TypeOf(n)))
}
Expand Down Expand Up @@ -790,3 +792,33 @@ func (g *generator) writeStringExpression(indentLevel int, e parser.Expression)
}
return nil
}

func (g *generator) writeText(indentLevel int, e parser.Text) (err error) {
vn := g.createVariableName()
// vn := sExpr
if _, err = g.w.WriteIndent(indentLevel, vn+" := "+createGoString(e.Value)+"\n"); err != nil {
return err
}
// _, err = io.WriteString(w, vn)
if _, err = g.w.WriteIndent(indentLevel, "_, err = io.WriteString(w, "+vn+")\n"); err != nil {
return err
}
if err = g.writeErrorHandler(indentLevel); err != nil {
return err
}
return nil
}

func createGoString(s string) string {
var sb strings.Builder
sb.WriteRune('`')
sects := strings.Split(s, "`")
for i := 0; i < len(sects); i++ {
sb.WriteString(sects[i])
if len(sects) > i+1 {
sb.WriteString("` + \"`\" + `")
}
}
sb.WriteRune('`')
return sb.String()
}
6 changes: 3 additions & 3 deletions generator/test-a-href/template.templ
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{% package testahref %}

{% templ render() %}
<a href="javascript:alert(&#39;unaffected&#39;);">{%= "Ignored" %}</a>
<a href={%= templ.URL("javascript:alert('should be sanitized')") %}>{%= "Sanitized" %}</a>
<a href={%= templ.SafeURL("javascript:alert('should not be sanitized')") %}>{%= "Unsanitized" %}</a>
<a href="javascript:alert(&#39;unaffected&#39;);">Ignored</a>
<a href={%= templ.URL("javascript:alert('should be sanitized')") %}>Sanitized</a>
<a href={%= templ.SafeURL("javascript:alert('should not be sanitized')") %}>Unsanitized</a>
{% endtempl %}

17 changes: 10 additions & 7 deletions generator/test-a-href/template_templ.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion generator/test-attribute-escaping/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

const expected = `<div>` +
`<a href="about:invalid#TemplFailedSanitizationURL"` +
`<a href="about:invalid#TemplFailedSanitizationURL">text</a>` +
`</div>`

func TestHTML(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion generator/test-attribute-escaping/template.templ
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

{% templ BasicTemplate(url string) %}
<div>
<a href={%= templ.URL(url) %}>{%= "text" %}</a>
<a href={%= templ.URL(url) %}>text</a>
</div>
{% endtempl %}

3 changes: 2 additions & 1 deletion generator/test-attribute-escaping/template_templ.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion generator/test-call/template.templ
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
{% endtempl %}

{% templ email(s string) %}
<div>{%= "email:" %}<a href={%= templ.URL("mailto: " + s) %}>{%= s %}</a></div>
<div>email:<a href={%= templ.URL("mailto: " + s) %}>{%= s %}</a></div>
{% endtempl %}

7 changes: 4 additions & 3 deletions generator/test-call/template_templ.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion generator/test-html/template.templ
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<div>
<h1>{%= p.name %}</h1>
<div style="font-family: &#39;sans-serif&#39;" id="test" data-contents={%= `something with "quotes" and a <tag>` %}>
<div>{%= "email:" %}<a href={%= templ.URL("mailto: " + p.email) %}>{%= p.email %}</a></div>
<div>email:<a href={%= templ.URL("mailto: " + p.email) %}>{%= p.email %}</a></div>
</div>
</div>
<hr noshade?={%= true %} />
Expand Down
7 changes: 4 additions & 3 deletions generator/test-html/template_templ.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions generator/test-text/render_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package testtext

import (
"context"
"strings"
"testing"

"github.com/google/go-cmp/cmp"
)

const expected = `<div>Name: Luiz Bonfa</div>` +
`<div>Text ` + "`" + `with backticks` + "`" + `</div>` +
`<div>Text ` + "`" + `with backtick` + `</div>` +
`<div>Text ` + "`" + `with backtick alongside variable: ` + `Luiz Bonfa</div>`

func TestHTML(t *testing.T) {
w := new(strings.Builder)
err := BasicTemplate("Luiz Bonfa").Render(context.Background(), w)
if err != nil {
t.Errorf("failed to render: %v", err)
}
if diff := cmp.Diff(expected, w.String()); diff != "" {
t.Error(diff)
}
}
9 changes: 9 additions & 0 deletions generator/test-text/template.templ
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% package testtext %}

{% templ BasicTemplate(name string) %}
<div>Name: {%= name %}</div>
<div>Text `with backticks`</div>
<div>Text `with backtick</div>
<div>Text `with backtick alongside variable: {%= name %}</div>
{% endtempl %}

Loading

0 comments on commit 4eb4878

Please sign in to comment.