a-h / templ

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

documentation: templ.SafeURL is not working as described #908

Closed vdmitriyev closed 1 month ago

vdmitriyev commented 1 month ago

Hi all,

I would really appreciate a help/advice on an issue with sanitization. Despite a number of old issues were helpful:

Description

It looks like there is no different between outputs of templ.SafeURL and templ.URL when they are applied to a <a href="<URL>">test</a>

To Reproduce

Templ file:

templ Example() {
    <a href={ templ.URL("/upload?param1=value1&param2=value2&param3=value3") }>TestWithURL</a>
    <a href={ templ.SafeURL("/upload?param1=value1&param2=value2&param3=value3") }>TestWithSafeURL</a>
}

Screenshot:

image

Expected behavior

Expected behavior would be omit a url sanitization/escaping by templ.SafeURL, as it mentioned in the documentation here - https://templ.guide/syntax-and-usage/attributes/

However it also could be that I am simply confused about the expected behavior of templ.SafeURL

Additional

In the documentation here https://templ.guide/security/injection-attacks/, the examples should look as follows (closing tag is missing by tag, so templ cannot be compiler properly):

templ Example() {
  <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") }>Test2</a>
  <a href={ templ.SafeURL("will not be sanitized by templ.URL") }>Test3</a>
}

Environment (version)

  • os: Windows
  • templ: v0.2.771
    • go: go1.23.0
    • gopls: v0.16.2

P.S.:

Thank you for an amazing library and your time, it took you all to develop it! Besides the great library I really appreciate the documentation. It is very helpful how developer experience with VS Code extension. further details on deployment with docker, etc. 🚀 💪

kalafut commented 1 month ago

Neither one will ever change the provided URL. URL() will check the URL and error if the check fails. SafeURL() isn't a function at all, but rather a type, and converting your string to the SafeURL type allows you to declare the URL safe and bypass the checks.

See: https://github.com/a-h/templ/blob/main/url.go

a-h commented 1 month ago

Hi @vdmitriyev, I think that if I clarify the difference between escaping and sanitization and why templ does this stuff, it might become clear...

Escaping

Escaping is the process of turning a URL string into something that is allowed within a HTML attribute - i.e. HTML encoding.

So that means http://google.com/?q=test&language=en-US (correctly) becomes http://google.com/?q=test&amp;language=en-US.

Without escaping, any string that contained a double quote could start to modify the underlying HTML document structure. We don't want that, because that's a vector for a content injection attack (https://owasp.org/www-community/attacks/Content_Spoofing) or XSS attack.

For example, if I used a URL string containing: http://google.com" onclick="alert('hello') and templ didn't escape the content, instead of the expected HTML output of <a href="google.com"> I'd get <a href="http://google.com" onclick="alert('hello')"> which injects script into my page.

templ always escapes output content, unless you explicitly use the unsafe templ.Raw component to write out arbitrary HTML, because failure to do so would allow injection attacks.

Sanitization

templ allows developers to include string content that is loaded from external resources, such as databases, content management systems and files. That's the whole point, really, or we'd just use plain HTML.

Those external resources might contain content that is not under the developer's control. For example, in Github, you can provide a URL to your personal website in your profile.

However, it turns out that you can abuse URIs and include malicious content in them. See https://positive.security/blog/ms-officecmd-rce for an example - as you can see from the article, a URL that starts with ms-officecmd: instead of http: can be used to damage the recipient's computer.

So, if you're building a system, one of your end users might manage to get a malicious URL to display on your system. Not good!

To avoid this security threat, templ forces you to sanitize URLs by using the templ.URL function. The function strips out untrusted / unusual schemes from URIs. In most cases, this is what you want.

BUT... if you know what you're doing, and you know that the URL is definitely safe, you can mark the string as safe by converting the string into a templ.SafeURL type.

vdmitriyev commented 1 month ago

@kalafut thank your for providing further insides on what SafeURL() is, it really helps to understand how it works

vdmitriyev commented 1 month ago

replying to: https://github.com/a-h/templ/issues/908#issuecomment-2336633242

@a-h thanks for your very detailed explanations and materials, I appreciate that very much. Now it got much clearer why URL() is enforced by templ and why it works like that by-design. Because it would be easier to assume, that all values that are passed to a templ-template are non trusted by default and handle it.

However, I am still confused, why templ.SafeURL() is still keep changing supplied string. Here is an example using <a> tag with workaround how to overcome sanitization using <form>.

Instead of using <a> tag to generate a clickable URL with parameters (on a server side without using any input user), a HTML <form> must be used, to overcome sanitization through templ.SafeURL().

Workaround using <form>

templ UploadViewUploadedAsForm(fileHash string, exerciseID string, uploadType string) {
    <form
        id={ fmt.Sprintf("upload-download-form-%s", exerciseID) }
        method="GET"
        action="/upload/download"
    >
        <input type="hidden" name="id" value={ exerciseID }/>
        <input type="hidden" name="group" value="exercises"/>
        <input type="hidden" name="upload-type" value={ uploadType }/>
        <input type="hidden" name="hash" value={ fileHash }/>
        <button
            type="submit"
            class="btn btn-success"
            id={ fmt.Sprintf("upload-download-form-button-%s", exerciseID) }
            hx-swap="true"
        >Download</button>
    </form>
}

<form> rendered

<form id="upload-download-form-E01" method="GET" action="/upload/download"><input type="hidden" name="id" value="E01"> <input type="hidden" name="group" value="exercises"> <input type="hidden" name="upload-type" value="test-type"> <input type="hidden" name="hash" value="test"> <button type="submit" class="btn btn-success" id="upload-download-form-button-E01" hx-swap="true">Download</button></form>

templ.SafeURL with unexpected output (changes & to &amp;)

templ UploadViewUploadedAsLink(fileHash string, exerciseID string, uploadType string) {
    <a
        href={ templ.SafeURL(fmt.Sprintf("/upload/download?id=%s&group=exercises&upload-type=%s&hash=%s", exerciseID, uploadType, fileHash)) }
        id={ fmt.Sprintf("upload-download-form-button-%s", exerciseID) }
    >
        Download
    </a>
}

This is how it is getting rendered by templ.SafeURL

<a href="/upload/download?id=E01&amp;group=exercises&amp;upload-type=test-type&amp;hash=test" id="upload-download-form-button-E01">Download</a>

Calling both tests at once

templ UploadViews() {
    @UploadViewUploadedAsForm("test", "E01", "test-type")
    @UploadViewUploadedAsLink("test", "E01", "test-type")
}

Hope, that this example make it clearer what I would like to achieve with templ.SaveURL().

a-h commented 1 month ago

It's doing the correct thing here. It's escaping, not sanitizing, the content. Your expectation is incorrect.

See: https://jsfiddle.net/n6tvwj94/1/

vdmitriyev commented 1 month ago

@a-h thank you for your prompt response and clarification.

So, there is no way available to "turn off" escaping done by templ, even for some special cases (see the example from the previous comment -> https://github.com/a-h/templ/issues/908#issuecomment-2351658168)? Because templ.SaveURL() is not a correct approach to be used to achieve such behavior, due to the reason that escaping and sanitization got different goals.

The only way to overcome this, is to user <forms> (which will not work for HTMX's hx-vals).

joerdav commented 1 month ago

@vdmitriyev I think what @a-h is getting at here is that for your use case the escaping is correct.

This anchor:

<a href="/upload/download?id=E01&amp;group=exercises&amp;upload-type=test-type&amp;hash=test" id="upload-download-form-button-E01">Download</a>

Will take you to:

/upload/download?id=E01&group=exercises&upload-type=test-type&hash=test

Which I believe is your expectation.

vdmitriyev commented 1 month ago

@joerdav thank you for the clarification, unfortunately it didn't work in my case.

In my understanding, the GET request using <form> or <a> should be equal and initiate an equal GET request to a backend. But I am going to investigate the backend side in details and more carefully, in order to see, what exactly backend receives in both cases.

a-h commented 1 month ago

I'll close this, since no further discussion has happened. Feel free to re-open if needed, but I'm pretty sure templ is not doing anything wrong here.

vdmitriyev commented 1 month ago

I'll close this, since no further discussion has happened. Feel free to re-open if needed, but I'm pretty sure templ is not doing anything wrong here.

@a-h tanks for the note closing note. templ is definitely not doing anything wrong. The only suggestion from my side for now would be to reflect the escaping behavior of templ in documentation besides sanitization (e.g., for example this page - https://templ.guide/syntax-and-usage/attributes/#url-attributes). This could potentially reduce the confusion.

a-h commented 1 month ago

Agreed, I just made a commit to add some extra information to the docs.