Escaping
Raw HTML, backtick blocks, raw(), and the two things escaping does not solve.
Backtick blocks and raw() produce raw HTML; every other value is escaped.
$doc.body.add(`<p>hello</p>`) # comes out as a tag
$doc.body.add("<script>alert(1)</script>") # comes out as textThe raw marking survives in three places:
`<li>` + $name + `</li>` # concatenation: result raw, $name escaped
`<li>`$name`</li>` # interpolation: the samefunc f() as string # an annotation does not drop the raw marking
return `<a href="/">x</a>`
end funcData from outside#
Every value entering a template is already escaped — database rows, form fields, query strings, all of them. You do not have to write anything extra:
`<td>` + $row.id + `</td>`Escaping covers quotes too (", '), so a value landing inside an attribute cannot close the quote and add a new attribute:
`<a href="` + $url + `">` # $url cannot escape the quotesThis makes it possible to write helper functions that return HTML:
func home_link() as string
return `<a href="/" class="btn">Home</a>`
end func
$doc.body.add(`<div class="container">` + home_link() + `</div>`)join follows the same rule: when one of the parts is raw HTML the result is raw HTML, and the data interpolated into it is escaped.
raw()#
raw() does the opposite: it turns escaping off. Use it only for HTML you produced yourself.
`<td>` + raw($row.id) + `</td>` # WRONG: a <script> in the database runsIn practice raw() should appear in exactly one place in an application: where your own markup is added to the document. If every view function returns plain text and raw() is called only at the last step, the answer to "where did this value come from" lives on a single line.
Two things escaping does not solve#
These you have to watch yourself.
The URL scheme. In href=" + $url + ", if $url is javascript:..., escaping does not stop it — the value never leaves the quotes, but the browser runs the scheme. Verify that the address begins with /, #, https:// or mailto::
func safe_url($url as string) as bool
if $url.starts_with("/") then: return true: end if
if $url.starts_with("#") then: return true: end if
if $url.starts_with("https://") then: return true: end if
if $url.starts_with("http://") then: return true: end if
if $url.starts_with("mailto:") then: return true: end if
return false
end funcUnquoted attributes. If you write href= + $url, a space in the value starts a new attribute. Always put attribute values in quotes.
No sanitising on input#
This is a deliberate decision. Cleaning data as it is stored is a common but mistaken habit:
- When the same data goes somewhere else — JSON, an attribute, JavaScript — cleaning done for HTML does not apply.
- The database is left holding mangled text; the
<the user typed cannot be recovered.
Escaping happens on output, where it is known which context the data is entering.