a-h / templ

A language for writing HTML user interfaces in Go.
https://templ.guide/
MIT License
7.92k stars 260 forks source link

Improve support for template/html.HTML type #175

Closed aranw closed 9 months ago

aranw commented 11 months ago

I'm using the https://github.com/gorilla/csrf library to get a csrf token for forms and it provides you with a https://pkg.go.dev/html/template#HTML when you use the https://pkg.go.dev/github.com/gorilla/csrf#TemplateField func

csrf.TemplateField(r)

This all works fine as HTML is just a string but right now the compiler does give a warning or something?

Screenshot 2023-09-29 at 20 58 33

A really simple reproducible example is

package template

templ SomePage(h template.HTML) {
    <div>
        { h }
    </div>
}

Which generates the following

package template

//lint:file-ignore SA4006 This context is only used if a nested component is present.

import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"

func SomePage(h template.HTML) templ.Component {
    return templ.ComponentFunc(func(ctx context.Context, w io.Writer) (err error) {
        templBuffer, templIsBuffer := w.(*bytes.Buffer)
        if !templIsBuffer {
            templBuffer = templ.GetBuffer()
            defer templ.ReleaseBuffer(templBuffer)
        }
        ctx = templ.InitializeContext(ctx)
        var_1 := templ.GetChildren(ctx)
        if var_1 == nil {
            var_1 = templ.NopComponent
        }
        ctx = templ.ClearChildren(ctx)
        _, err = templBuffer.WriteString("<div>")
        if err != nil {
            return err
        }
        var var_2 string = h
        _, err = templBuffer.WriteString(templ.EscapeString(var_2))
        if err != nil {
            return err
        }
        _, err = templBuffer.WriteString("</div>")
        if err != nil {
            return err
        }
        if !templIsBuffer {
            _, err = templBuffer.WriteTo(w)
        }
        return err
    })
}

Templ Version

❯ templ --version
v0.2.335-0.20230929162951-efd33120d9e8

I would try do this but I'm still getting to know the library and how it all works so I thought I'd just log it here in case anyone else with more experience can quickly solve this

KasonBraley commented 11 months ago

Might not be the proper solution, but you can explicitly cast it to a string to resolve the error:

templ SomePage(h template.HTML) {
    <div>
        { string(h) }
    </div>
}

This doesn't appear to be an issue with templ, but just how Go's type system works.

You get the same error with just Go:

func foo(h template.HTML) {
    var s string = h
}
aranw commented 11 months ago

Might not be the proper solution, but you can explicitly cast it to a string to resolve the error:

templ SomePage(h template.HTML) {
    <div>
        { string(h) }
    </div>
}

This doesn't appear to be an issue with templ, but just how Go's type system works.

You get the same error with just Go:

func foo(h template.HTML) {
  var s string = h
}

The problem with this is it escapes the html and puts it into the HTML as a string

The gorilla/csrf package is giving me a HTML input element that I want to embed into my template

Would be handy to have something similar to templ.SafeURL or templ.SafeClass e.g. templ.SafeHTML where it does not escape the given string or template.HTML in this case

a-h commented 11 months ago

This might be a good feature to add.

In terms of how this could work with templ, then I think it's just a templ component that renders the template.HTML value. Here's how it would outside of the templ module (in your own code).

func GoTemplate(h template.HTML) templ.Component {
  return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
      _, err := io.WriteString(w, string(h))
     return err
  })
}

You could then use this in your templ code as:

<form>
  @GoTemplate(csrf)
</form>

The only difference if it was built into templ would be that it would be a call to:

<form>
  @templ.GoTemplate(csrf)
</form>

So far, I've resisted adding something built-in to templ to render unsafely since I don't want people to shoot themselves in the foot, but a more general version would bypass templ's output escaping for all strings:

func DangerouslyIncludeHTML(s string) templ.Component {
  return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
      _, err := io.WriteString(w, s)
     return err
  })
}

The question is whether to provide a "Dangerous..." component, and if so, whether to bother with a specific function for Go HTML templates OR, to provide one for Go HTML template values only.

And for both, the classic problem of what they should be called. 😁

nics commented 11 months ago

I would find a SafeHTML function, similar to SafeURL, that strips script tags etc very useful to handle untrusted html. A builtin UnsafeHTML escape hatch would be similarly useful when there are other trusted generators of html (could be in an unsafe sub package to keep people on their guard).

aranw commented 11 months ago

I would find a SafeHTML function, similar to SafeURL, that strips script tags etc very useful to handle untrusted html. A builtin UnsafeHTML escape hatch would be similarly useful when there are other trusted generators of html (could be in an unsafe sub package to keep people on their guard).

Are you thinking something along the lines of:

safe.HTML/safe.URL/safe.CSS/etc and unsafe.HTML/unsafe.URL/unsafe.CSS/etc

aranw commented 11 months ago

In terms of how this could work with templ, then I think it's just a templ component that renders the template.HTML value. Here's how it would outside of the templ module (in your own code).

func GoTemplate(h template.HTML) templ.Component {
  return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
      _, err := io.WriteString(w, string(h))
     return err
  })
}

You could then use this in your templ code as:

<form>
  @GoTemplate(csrf)
</form>

This suggested solution worked for me and I switched to it for now until a feautre in templ itself lands

nics commented 11 months ago

Are you thinking something along the lines of:

safe.HTML/safe.URL/safe.CSS/etc and unsafe.HTML/unsafe.URL/unsafe.CSS/etc

The safe variants already live in the templ package. The choice between templ.UnsafeHTML and putting it in another package depends mainly on how explicit the authors want to make unsafe behaviour? One of the main attractions for me is the default safety of templ.

a-h commented 11 months ago

The concept of safety that we're talking about is whether the content is fully under control of the developer. Dynamic content from external sources (user input, 3rd party libraries etc.) isn't under our control, so we shouldn't trust it.

It's fairly well explained at https://gohugo.io/functions/safe/url/ and https://gohugo.io/functions/safe/html/ docs in short form, and in the linked OWASP video at https://github.com/google/safehtml

The SafeHTML function from Hugo and similar libraries isn't making the HTML safe. It is a declaration by the developer. By using the function, the developer "Declares a provided string as a “safe” HTML document, and Hugo then bypasses content sanitization and escaping.

The idea is that all content goes through the sanitization and escaping process, unless it is a Safe* type.

In the case of templ, adding a SafeHTML type and a component to render it would allow bypassing of sanitization. It's basically the same code as what I called DangerouslyIncludeHTML:

// SafeHTML is HTML that has come from a trusted source, or has been sanitized.
type SafeHTML string

// Include HTML from another trusted source.
func Include(s SafeHTML) templ.Component {
  return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
      _, err := io.WriteString(w, string(s))
     return err
  })
}

Then, it would be a case of:

<form>
  @templ.Include(templ.SafeHTML(csrf))
</form>

Could also echo Google's SafeHTML approach which prevents type casting.

// SafeHTML is HTML that has come from a trusted source, or has been sanitized.
type SafeHTML struct {
  content string
}

func DeclareHTMLSafe(s string) SafeHTML {
  return SafeHTML{ content: s }
}

// Include HTML from another trusted source.
func Include(s SafeHTML) templ.Component {
  return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
      _, err := io.WriteString(w, s.content)
     return err
  })
}
<form>
  @templ.Include(templ.DeclareHTMLSafe(csrf))
</form>
nics commented 11 months ago

Hi @a-h you're right, I got confused, ignore my remarks above.

a-h commented 11 months ago

If we focus back on CSRF token handling, then we don't need a Safe HTML thing at all.

Note the csrfToken argument, and that it's applied to the hidden gorilla.csrf.Token input element.

templ form(csrfToken string) {
    <form action="/" method="POST">
        <input type="hidden" name="gorilla.csrf.Token" value={ csrfToken }/>
        <div><button type="submit" name="global" value="global">Global</button></div>
        <div><button type="submit" name="user" value="user">User</button></div>
    </form>
}

Then, in the HTTP handler, it's a case of passing the csrf.Token(r) to the template instead of csrf.TemplateField.

func getHandler(w http.ResponseWriter, r *http.Request) {
    userCount := sessionManager.GetInt(r.Context(), "count")
    component := page(csrf.Token(r), global.Count, userCount)
    component.Render(r.Context(), w)
}

So. I think we have two things in this issue.

  1. Do we want a raw "Include" component built into templ?
  2. Do we need to document how to do CSRF with templ?

I think the answer to #1 is no. Not having it makes accidental XSS much less likely, and I think that anyone who knows they need it won't mind writing the few lines of code. It could be documented though.

On 2, I think a docs update would be a good idea. I think a new section of "Using with other tools", and having "Gorilla", "HTMX" etc. under that would make sense.

So... actions are:

  1. Document the Include and SafeHTML concept above in "Syntax and usage" > "Including raw HTML".
  2. Add "Using with other tools" as a section, add Gorilla, and include a section on how to use Gorilla CSRF with templ.
stephenafamo commented 10 months ago

Right now, I have created a helper function I use for this

func Raw(s string, errs ...error) templ.Component {
    return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
        _, err := fmt.Fprint(w, s)
        return errors.Join(append(errs, err)...)
    })
}

I really think the ability to render raw HTML should be part of the package itself. It makes sense to give the user a convenient way to opt-out of HTML escaping.

boyter commented 9 months ago

The ability to render raw HTML should at least be mentioned in the documentation if its not included. I don't mind adding in DangerouslyIncludeHTML to get the functionality, but it took me a while to find this issue to get an answer about it.

It's totally understandable if you don't want to include it, but its also one of those things that is required unfortunately, hence appearing in every template language out there.

BTW thanks so much for making this. I absolutely adore it and plan on porting everything I maintain to it over time.

vietvudanh commented 9 months ago

So far, I've resisted adding something built-in to templ to render unsafely since I don't want people to shoot themselves in the foot, but a more general version would bypass templ's output escaping for all strings:


func DangerouslyIncludeHTML(s string) templ.Component {
  return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
      _, err := io.WriteString(w, s)
     return err
  })
}

Thanks for this repo and the helper func. However, I agree with @boyter: this really should be included in the docs if it's not available and point to this issue.

I had never had problem with raw HTML for frontend before until using templ. This is a valid use case.

a-h commented 9 months ago

Makes sense, I propose that we add the following...

// Raw renders the input HTML to the output without applying HTML escaping.
//
// Use of this component presents a security risk - the HTML should come from
// a trusted source, because it will be included as-is in the output.
func Raw[T ~string](html T, errs ...error) Component {
    return ComponentFunc(func(ctx context.Context, w io.Writer) (err error) {
        if err = errors.Join(errs...); err != nil {
            return err
        }
        _, err = io.WriteString(w, string(html))
        return err
    })
}

// GoHTMLTemplate renders the Go html/template to the output.
func GoHTMLTemplate(t *template.Template, data any) Component {
    return ComponentFunc(func(ctx context.Context, w io.Writer) (err error) {
        return t.Execute(w, data)
    })
}

I've got tests for these components ready, and will add some documentation alongside.

a-h commented 9 months ago

OK folks, PR is in place. Comments welcome.

I updated it to add bi-directional support - with these helper functions you can use templ in Go templates, and Go templates in templ.

xV0lk commented 8 months ago

If we focus back on CSRF token handling, then we don't need a Safe HTML thing at all.

Note the csrfToken argument, and that it's applied to the hidden gorilla.csrf.Token input element.

templ form(csrfToken string) {
  <form action="/" method="POST">
      <input type="hidden" name="gorilla.csrf.Token" value={ csrfToken }/>
      <div><button type="submit" name="global" value="global">Global</button></div>
      <div><button type="submit" name="user" value="user">User</button></div>
  </form>
}

Then, in the HTTP handler, it's a case of passing the csrf.Token(r) to the template instead of csrf.TemplateField.

func getHandler(w http.ResponseWriter, r *http.Request) {
  userCount := sessionManager.GetInt(r.Context(), "count")
  component := page(csrf.Token(r), global.Count, userCount)
  component.Render(r.Context(), w)
}

So. I think we have two things in this issue.

  1. Do we want a raw "Include" component built into templ?
  2. Do we need to document how to do CSRF with templ?

I think the answer to #1 is no. Not having it makes accidental XSS much less likely, and I think that anyone who knows they need it won't mind writing the few lines of code. It could be documented though.

On 2, I think a docs update would be a good idea. I think a new section of "Using with other tools", and having "Gorilla", "HTMX" etc. under that would make sense.

So... actions are:

  1. Document the Include and SafeHTML concept above in "Syntax and usage" > "Including raw HTML".
  2. Add "Using with other tools" as a section, add Gorilla, and include a section on how to use Gorilla CSRF with templ.

I just wanted to leave an update on using Gorilla CSRF with templ for anyone that got here for the same reason.

I believe we can achieve the same things a bit easier.

All csrf.TemplateField and csrf.Token does is look for "gorilla.csrf.Token" key on the request context.

So you can just do this instead:

templ Csrf() {
    <input type="hidden" name="gorilla.csrf.Token" value={ ctx.Value("gorilla.csrf.Token").(string) }/>
}

and use that template inside any form. You wont need to drill the token from the handler. In my case is working without an issue just make sure to have added the csrf middleware on your http Handler.

@a-h if its ok, i'm happy to make a pr adding a page to the docs about working with Gorilla CSRF including your examples

a-h commented 8 months ago

Thanks @xV0lk - if you have time for a PR, that would be great!

Nelwhix commented 6 months ago

@a-h I ran into this issue today, Thanks for your support and @xV0lk also. I have made a PR to add this to the docs here https://github.com/a-h/templ/pull/622