---
title: "Escaping and Security · RUAL Documentation"
description: "Where a value lands decides how it is escaped, which templates refuse to compile and why, the Content Security Policy, and who may run which action."
canonical: https://docs.rual.nl/web/security
language: en
---

# Escaping and Security

Where a value lands decides how it is escaped, and that is read from your template's own markup, so data can never escape its place. A template that cannot be made safe does not compile. On top of that, every page has a Content Security Policy with a fresh nonce, and every action passes its gates.

In short

- `{{value}}` is safe wherever you write it. `{{{value}}}` is your own responsibility.

- Three shapes do not compile: a partial anywhere but in text, branches that end in different places, and a link whose scheme comes from data.

- Inline scripts need `nonce="{{nonce}}"`. `onclick` and friends are blocked, and `data-action` replaces them.

- Actions are refused from other websites, and pass the page's scopes and their own.

## Where a value lands decides how it is written

The context of each value is worked out once, when the template compiles, from the HTML you wrote around it. Data cannot choose its own context, because the context is your markup.

| The value stands in | It is written as |
| --- | --- |
| Text, ` `, ` `, or an attribute | HTML-escaped. An unquoted attribute value also gets its spaces and `=` escaped. |
| `href`, `src`, `action`, `formaction`, `srcset`, `poster` and other URL attributes | Only `http`, `https`, `mailto`, `tel`, `sms`, relative URLs and `data:` images (png, jpeg, gif, webp, avif) pass. A link that would run script, such as `javascript:`, is replaced by a harmless one. After a `?` the value is URL-encoded. |
| Code in a ` `, including JSON-LD | JSON, safe inside the script. Write `var cart = {{cart}};` without quotes. |
| A string, template text, comment or regex inside a ` ` | Every character but letters, digits and `_` as an escape, which reads back as the value there. |
| A `style` attribute or ` ` | Only characters a CSS value needs. Anything else becomes `unsafe`. Inside `url(...)` it is checked as a URL. |
| Between attributes, ` ` | Attribute names, or an object of attributes, each escaped for its kind. Never an event handler, and anything else is dropped. |
| An HTML comment | HTML-escaped, and `--` is broken up so it cannot end the comment. |

The same data in each place, with a value that tries to break out:

```
<p>{{evil}}</p>
<div title="{{evil}}"></div>
<a href="{{link}}">A link</a>
<a href="/search?q={{evil}}">Search</a>
<div style="color: {{color}}"></div>
<input {{attrs}}>
<script nonce="{{nonce}}">
  var value = {{evil}};
  var text = "{{evil}}";
</script>
```

With `evil` as `"> alert(1) `, `link` as `javascript:alert(1)` and `color` as `red;background:url(javascript:alert(1))`, the script never runs: the link becomes `about:invalid#unsafe-url`, the color becomes `unsafe`, and the rest is inert text.

**Triple braces opt out.** `{{{value}}}` and `{{& value}}` write the value as it is, wherever they stand. Use them only for HTML you built yourself, such as the output of [`web_foreach`](https://docs.rual.nl/block-types/web/web_foreach) or [`web_template`](https://docs.rual.nl/block-types/web/web_template), whose values are escaped already. Never for something a visitor typed.

The same goes for the `html` pin of [`web_update`](https://docs.rual.nl/block-types/web/web_update), [`web_replace`](https://docs.rual.nl/block-types/web/web_replace) and [`web_insert`](https://docs.rual.nl/block-types/web/web_insert): it is HTML. Run a visitor's text through a [`web_template`](https://docs.rual.nl/block-types/web/web_template) first.

## Some templates refuse to compile, with a message that says why

No single escaping is right for these shapes, so they are errors and not risks.

### A partial anywhere but where text goes

A partial compiles on its own, starting in text, and must end in text too.

#### Does not compile

` part}}">` ` const x = {{> part}}; ` `{{define "p"}} {{/define}}`

#### Compiles

` {{> part}} ` ` const x = {{value}}; `

### A branch that ends somewhere else

The next value would otherwise be escaped for a place it may not be in. With an `{{else}}`, the branches must agree with each other. Without one, or in a section or a loop that may run zero or many times, the block must end where it began.

#### Does not compile

`{{if a}} {{/if}}{{v}}` `{{if a}} {{/if}}` `{{#items}} `

#### Compiles

`{{if a}} {{else}} {{/if}}` ` ` ` ` ` `

The fourth one catches most people. A bare `checked` leaves the block in the middle of an attribute name, and `checked="checked"` ends where it began. ` ` with an object of attributes is the other way to write it.

### A link whose scheme would come from data

A value followed by a `:` in your text could build `javascript:` out of the two.

#### Does not compile

` ` ` `

#### Compiles

` ` ` ` ` `, checked as a whole when it renders

A value that may be the scheme is checked together with the text before it, so `{{a}}{{b}}` cannot build `javascript:` either.

### And the plain mistakes

A filter that does not exist, a block that is never closed, a closing tag for the wrong block, a second `{{else}}`, changed delimiters and a `{{define}}` used twice for one name are all errors with their line and column, too. So a typo shows up the first time the page renders, for you, and is never a blank spot for a visitor to find.

## Every page has a Content Security Policy with a fresh nonce

Every page response carries this policy, with a new random nonce each time:

```
default-src 'self';
script-src 'self' 'nonce-RANDOM' https:;
style-src 'self' 'unsafe-inline' https:;
img-src 'self' data: blob: https:;
font-src 'self' data: https:;
media-src 'self' blob: https:;
connect-src 'self' https: wss:;
frame-src 'self' https:;
form-action 'self' https:;
frame-ancestors 'self';
base-uri 'self';
object-src 'none'
```

| You want | What the policy says |
| --- | --- |
| An inline script | It needs `nonce="{{nonce}}"`. Without it, the browser refuses it. |
| `onclick` and other handler attributes | Blocked, as they cannot carry a nonce. `data-action` and an event listener in your own script replace them. |
| A script from a CDN or analytics | Allowed: your own files and anything over `https:`. |
| Inline styles and ` ` | Allowed, also over `https:`. |
| Images, fonts and media | Allowed from your own site and `https:`. Images and fonts also from `data:`, images and media from `blob:`. |
| Fetching from a script | Your own site, `https:` and `wss:`. |
| Showing a frame, or posting a form elsewhere | Your own site and `https:`. |
| Being framed by another site | Refused. Only your own site may frame the page. |
| A ` ` pointing elsewhere, or a plugin | Refused. |

In short, a page may load what a website loads, and is refused what an injected string would need. To set your own policy, send `Content-Security-Policy` through the `headers` pin of [`web_render`](https://docs.rual.nl/block-types/web/web_render): yours then replaces this one, nonce included, so you take over keeping the runtime script allowed.

Next to the policy, a page gets `X-Content-Type-Options: nosniff` and `Referrer-Policy: strict-origin-when-cross-origin` unless you set them yourself.

## Scopes decide who may open a page

A [`web_page`](https://docs.rual.nl/block-types/web/web_page) is gated like every other page: `*public` for anyone, `*loggedin` for any signed-in user, a scope of your own, or `*ip:` for addresses. With no scopes at all it opens for developers only. See [Remote access control](https://docs.rual.nl/blueprints/remote-access-control).

| The visitor | Gets |
| --- | --- |
| Not signed in, opening a page | A redirect to `/login?redirect=...`, back to the page afterwards. |
| Not signed in, running an action | `401`. The page reloads, which leads to the login. |
| Signed in, without the scope | `403`, with a short message. |

## An action passes more gates than its page

- **Another website is refused.** A `POST` the browser marks as coming from another site, or whose `Origin` is another host, gets a `403`. That includes other subdomains of your domain. The session cookie on such a request is the visitor's, but the intent is not.

- **The page's scopes** must admit the visitor, as for opening it.

- **The action's own scopes** must admit the visitor, if it has any.

- **Without scopes of its own,** the scopes of every [`web_page`](https://docs.rual.nl/block-types/web/web_page) in the blueprint must admit the visitor.

A blueprint with a public page and an admin page therefore cannot leak the admin actions through the public page. The reverse is the catch: an action the public page uses needs `scopes: ["*public"]` of its own in that blueprint, and a page without scopes closes every action without scopes to anyone but developers.

An action name means nothing outside its blueprint, and a name the blueprint does not have is a `404`. A script outside a browser sends none of the headers above and passes the first gate, so never treat it as authentication: the scopes do that job.

## A failure tells the visitor little

A visitor who reaches a broken page reads that it failed. Why it failed goes to the log, and on the page or in the action's answer only to the entity's blueprint developers: sessions with the `setting_blueprint_view_all` permission. So a template error never shows a stranger how the page is built, and you can debug a live page by opening it signed in.

On the block, a template error sets `success` to false and puts the message on `error`, and the page answers with status 500.

## Next steps

- [Remote access control](https://docs.rual.nl/blueprints/remote-access-control): All the scopes a page or action can carry.

- [Actions](https://docs.rual.nl/web/actions): The gates at work, and what an action can answer.

- [Template Syntax](https://docs.rual.nl/web/templates): The rest of what a template can say.

## Frequently asked

**How does a RUAL web template stop script injection?**

Where a value lands decides how it is escaped, and that is read from the template's own markup when it compiles: text and attributes are HTML-escaped, links refuse schemes that run script, script contents get JSON. Data cannot choose its place. Triple braces write a value as it is, so use those only for HTML you built yourself.

**Why does my RUAL web template refuse to compile?**

It holds something no single escaping is right for: a partial inside a tag, attribute or script, an if whose branches end in different places, or a link whose scheme would come from data. The error says which. Open and close tags inside each branch, and write https://{{host}} rather than {{scheme}}://{{host}}.
