Skip to content

Rendering Untrusted Content

This page is an honest account of kBars's behavior with two different kinds of untrusted input — untrusted data (a context or @env property whose string values come from users) and untrusted templates (template source itself written by someone other than the app developer, as in a CMS) — since the two have different risk profiles and neither is fully covered by "kBars escapes HTML." See Security and Escaping Notes for what {{ }}/{{{ }}} escaping itself does and doesn't cover.

Rendering untrusted data

Treat context data (JSON/YAML bodies, @env properties, values returned from {{> key ctx }}) the same way you'd treat any user-supplied string flowing into a Kotlin app: kBars's job ends at HTML escaping for scalar interpolation (see Security and Escaping Notes). It does not sanitize for other output contexts — a template whose output is JavaScript, a URL, a shell command, or SQL needs its own escaping for that context (kBars ships url_escape/url_param_escape string transforms for the URL case; there is no JS-string or SQL escaping transform, because kBars has no way to know which target context a given interpolation feeds).

Rendering untrusted templates

This is the CMS scenario: end users author the template source itself, not just the data. A few properties of the engine matter here, and a few don't help at all.

Partial recursion is bounded. {{> key }} nesting — including a partial that (directly or through a chain of other partials) includes itself — is capped by maxPartialDepth, defaulting to 64. Once exceeded, the over-deep partial renders as nothing and a KbSoftFailEvent.Category.RecursionLimit soft failure is reported; the render completes rather than recursing without bound. See Error Handling for observing this via a KbEventListener, and set a lower maxPartialDepth in KbEnvironmentBuilder if 64 is more headroom than your partial structure ever legitimately needs.

Parser and expression nesting are not depth-limited. A parenthesized transform-argument chain ((a | b | (c | d))) or deeply nested {{#if}}/{{#each}}/{{#with}} blocks have no engine-imposed maximum — like most recursive-descent parsers, extremely deep nesting is bounded only by available call stack, not by an explicit counter. A template source with, say, tens of thousands of nested block tags is a plausible resource-exhaustion vector if you accept template source from an untrusted party without a size or nesting-depth check of your own before calling KbTemplate.create.

{{#each}} iterates whatever the context gives it, with no size cap. A template that iterates a context list has no engine-side limit on how large that list is or how much output it produces — the usual template-engine amplification risk (a small template producing enormous output against a large context) applies here as it would with any engine. Bound context size and rendered-output size at the application layer if you accept either untrusted templates or untrusted (attacker-sized) context data.

A dynamic partial key ({{> (expr) }}) is resolved through your KbPartialLoader. kBars itself does no filesystem or network access for partials — every partial byte comes from the loader your app registers via onLoadPartial. If a template can compute an arbitrary string and your loader maps that string to a file path (or a URL, or a database lookup) without validating it, path traversal or key confusion is a property of your loader, not of kBars. Validate or allowlist keys in your KbPartialLoader implementation if the key can originate from untrusted template source or untrusted context data.

Custom transforms run arbitrary Kotlin code you registered — templates can't add their own. A transform is a KbTransform your app registers ahead of time; template source can only call a transform by name and pass it arguments, never define new native behavior. An untrusted template can choose which of your registered transforms to invoke and with what argument values, so a transform that does something sensitive (file I/O, an outbound call) based on its arguments is reachable from template source — write custom transforms with the same suspicion you'd apply to any function whose arguments might come from an untrusted caller.

Summary

Concern Bounded by kBars?
Partial recursion depth Yes — maxPartialDepth, default 64
Block/expression nesting depth No — bounded only by call stack
{{#each}} output size No — bounded only by context size
Dynamic partial key resolution No — your KbPartialLoader's responsibility
Custom transform arguments No — your transform's responsibility

Where to next

  • Security and Escaping Notes — what HTML-escaping covers, from the template author's side.
  • Error Handling — observing soft failures like RecursionLimit via a KbEventListener.
  • Custom Transforms — registering the only kind of behavior a template can invoke that your app didn't already define.
  • Custom PartialsKbPartialLoader and maxPartialDepth configuration.