Security
Escaping by context, and templates as code
Escaping by context
{{ $x }} escapes the value with htmlspecialchars (ENT_QUOTES). That is right for HTML text and for quoted attribute values, but not enough in URLs and JavaScript. Use the modifier that matches the place:
| Where the value goes | Write |
|---|---|
| HTML text, quoted attribute values | {{ $x }} |
<textarea> | {{ $x }} (the browser shows the original text) |
href / src / action / formaction taking a URL from data | href="{{ $x|feggUrl }}" |
Inside <script> | var x = {{ $x|feggJs|noescape }}; (no quotes around it) |
Event attributes (onclick …) | onclick="go({{ $x|feggJs }})" (no quotes around it, keep the escaping) |
| Trusted HTML (CMS rich text, messages from your config) | {{ $x|noescape }} |
Always quote attribute values (class="{{ $x }}", not class={{ $x }}).
URLs: feggUrl
HTML escaping does not stop a javascript: URL. feggUrl returns # for any URL whose scheme is not http, https, mailto or tel (also when the scheme is disguised with spaces or control characters). Relative URLs are kept.
<a href="{{ $page.data.link|feggUrl }}">{{ $page.data.link_label }}</a>
URLs built by the CMS modifiers (getImageUrl, getImageUrlFromDB, getFileUrl, getFileRawUrl) and values from $site (your configuration) do not need it.
JavaScript: feggJs
feggJs turns any value (string, number, array, object, null) into a JavaScript literal and encodes <, >, &, ' and ", so the value can neither end the string nor close the <script> element. It writes the quotes itself: do not add your own.
<script>
var title = {{ $page.title|feggJs|noescape }};
</script>
<button type="button" onclick="showItem({{ $item.id|feggJs }})">Show</button>
In an attribute, the browser decodes the HTML escaping before it runs the JavaScript, so the normal {{ }} escaping must stay: never add noescape there.
Arrays and objects work too, so a whole data set can go to a script in one line:
<script>
var items = {{ $page.items|feggJs|noescape }}; // [{"id":1,"title":"..."}, ...]
</script>
noescape
noescape outputs the value as it is. Use it only for HTML you trust: CMS rich text written by administrators, validation messages from your config, or the result of feggJs inside <script>. Never use it on $in (request parameters) or anything a visitor can type.
Templates are code
{{ code }}, conditions, {{ assign }} and modifiers run arbitrary PHP. Treat templates like PHP files:
- Only developers may edit templates. Never let site users upload or edit
.tplfiles. - Never build a template name, an
includepath or a modifier name from user input. - A controller that picks the template from its URL arguments must only be reachable through
site/config/route.php. ImplementFegg\Http\RouteOnly, as the skeleton'sSite\Controller\Pagedoes:
$route['about/'] = 'page/render/about'; // site/template/about.tpl
/page/render/... typed in the browser is then a 404.
What lint checks
./fegg-cli template:lint reports outputs that lack the right modifier:
- an output in
<script>withoutfeggJsandnoescape(scripts with a non-JavaScripttype, such astext/template, are skipped); - an output in an
on...attribute withoutfeggJs, or withnoescape; - an output at the start of an
href,src,actionorformactionvalue withoutfeggUrl(or a CMS URL modifier).
Outputs of $site values are exempt from these checks. See Lint and errors.