Source code review
7 min read

Where Glance API strings acquire markup authority

Follow contextual escaping through two server-side render passes and browser HTML insertion. Trusted content helpers change specific protections, rather than sanitizing their input.

Date
30 September 2026
Outcome
Source review and local probe
Focus
Glance v0.8.6 · immutable source and standard-library template experiment

I traced the rendering path of a Glance custom API widget through two server-side template passes and the browser's HTML insertion step. The central result is precise: normal interpolation protects its HTML context, but converting a value to trusted HTML or a trusted URL changes which protections apply. A second render does not repair an unsafe assertion made during the first one.

Review and experiment: 30 September 2026. Target: Glance v0.8.6, immutable revision fbd9c1b6ae0c50ec015843f1bea48c400cddb518. This review concerns browser content construction; it does not repeat the earlier TLS or request-destination experiments.

Three different operations are often called escaping

A widget can display an API string as text, place it inside a quoted attribute, or use it as a navigation URL. These contexts require different transformations. Encoding quotation marks protects an attribute boundary. Filtering a URL scheme prevents an active scheme from becoming a link. Choosing which HTTPS origins the browser may contact is an additional policy decision.

Go's html/template selects escaping from the template's context. Its security model assumes trusted template authors and untrusted execution data. It is designed to preserve the structure the author wrote, not to remove arbitrary hostile markup that an author explicitly marks trusted. Go template security model.

That makes the trust transition more informative than a search for one dangerous word:

API-controlled string
  → expression in a trusted template
  → ordinary string OR an asserted trusted content type
  → escaped or verbatim fragment
  → page-content HTML
  → browser DOM

The review question is: where did the value acquire the authority to become markup or an active URL?

Trace both server-side render passes

Glance parses a custom API template with html/template, executes it against API response data, then converts the rendered buffer to template.HTML. This fragment is stored as CompiledHTML. The outer widget template includes that already rendered fragment, and the page-content template includes each widget's Render result. Custom template compilation, execution and typed result, outer widget inclusion, page composition.

The typed result is reasonable when the first pass has already produced safe HTML: otherwise a widget's intended tags would become visible text. It is not an independent sanitization pass. If the first template emitted an event-handler attribute or an active URL through a trusted-content helper, the later typed inclusion carries it forward.

Conversely, conversion of a correctly escaped fragment to template.HTML does not decode an API string into an element. A string rendered as <b>demo</b> remains text when included inside an outer HTML fragment. This distinction matters when evaluating reports that treat every template.HTML conversion as equivalent.

The helpers change specific protections

Glance's global function map supplies safeHTML, safeURL and safeCSS. Each converts a string to the corresponding Go trusted-content type. The custom API function map imports these helpers. The names express an assertion by the template author; the implementation does not sanitize the input. Helper definitions, custom API function merge.

safeHTML permits the value to supply element and attribute syntax. safeURL bypasses URL scheme filtering, but does not disable every attribute-encoding step. A typed HTTPS URL containing & is still encoded appropriately in a quoted HTML attribute. These are separate transformations. Go documents the risks of both trusted types. Go trusted HTML, Go trusted URL.

The same design assumption appears outside custom API widgets: the HTML widget stores its configured source as template.HTML and returns it directly. The widget header renders its configured title URL through safeURL. These are configuration-author facilities; reviewing only API values would miss the template author's own active-content authority. HTML widget source, widget title URL.

A reproducible contextual-escaping matrix

I wrote an original template-boundary probe using only the standard library. It renders synthetic strings through ordinary and typed contexts, checks the exact output, and repeats a safe fragment through an outer render. It does not run Glance or start a browser. No URL in the output is requested.

Run from the repository directory:

run-bounded 300 go run examples/glance-template-boundary-probe.go

Recorded output on 30 September 2026, exit status 0:

go=go1.27.1-X:nodwarf5
text: <p>&lt;b&gt;demo&lt;/b&gt;</p>
attribute: <p title="demo&#34; onmouseover=&#34;void 0">label</p>
url-string: <a href="#ZgotmplZ">open</a>
url-typed: <a href="javascript:demo">open</a>
https-other-origin: <a href="https://outside.invalid/read">open</a>
html-string: <section>&lt;img src=&#34;/synthetic-missing&#34; onerror=&#34;void 0&#34;&gt;</section>
html-typed: <section><img src="/synthetic-missing" onerror="void 0"></section>
typed-url-attribute-escaping: <a href="https://metrics.invalid/?x=a&amp;y=b">open</a>
nested-typed-fragment: <article><p>&lt;b&gt;demo&lt;/b&gt;</p></article>

run-bounded is my process-deadline wrapper; the direct go run command works without it. The source is an output experiment, not an application vulnerability test.

ResultWhat it establishes
Text output encodes angle bracketsAPI markup is displayed as text in that context
Attribute output encodes injected quotation marksThe apparent onmouseover text remains inside the original title attribute
Ordinary javascript: URL becomes #ZgotmplZThe normal URL filter rejects that scheme
Typed URL preserves the active schemeThe trusted URL assertion changes scheme filtering
Ordinary foreign HTTPS link survivesScheme filtering is not an origin allowlist
Typed HTML preserves an event attributeThe trusted HTML assertion permits markup structure
Typed URL still encodes &Trusting a URL does not disable quoted-attribute encoding
Nested safe fragment retains its entitiesOuter inclusion does not undo correct first-pass text escaping

These observations make review recommendations testable. Saying only “the project uses Go templates” leaves out the decision that actually changes the boundary.

The browser insertion step is not a sanitizer

Glance's frontend fetches the page-content response as text, then assigns it to pageContentElement.innerHTML. This turns the server's fragment into DOM structure. The HTML standard defines that operation through fragment parsing and replacement; it is not an application-specific content approval step. Frontend content fetch, DOM insertion, HTML standard: innerHTML.

The probe establishes emitted bytes, not browser execution. In particular, do not infer that a <script> tag delivered through every HTML insertion mechanism behaves like a parser-loaded script. The stronger source-review question is whether untrusted data can provide any active markup, such as event-handler attributes or navigable active URLs, and what browser policy would constrain it. An authentication gate determines who can receive the content; it does not cleanse content after that user signs in.

Patch the template where the assertion occurs

For an API field intended to be a description, removing the trusted-HTML assertion is the direct correction:

-<p>{{ .JSON.String "description" | safeHTML }}</p>
+<p>{{ .JSON.String "description" }}</p>

For an API-provided navigation URL, removing safeURL restores normal scheme filtering:

-<a href="{{ .JSON.String "url" | safeURL }}">Open metric</a>
+<a href="{{ .JSON.String "url" }}">Open metric</a>

The second change still allows ordinary HTTPS links to outside origins, as the probe demonstrates. If navigation must stay on one metrics service, construct the route from a fixed trusted base and a validated identifier, or map known identifiers to literal links. Do not describe the removal of safeURL as full destination authorization.

If rich HTML is a real requirement, place sanitization before the trusted-content conversion and define the allowed element, attribute and URL policies explicitly. Casting a string to template.HTML is the final assertion after that policy, not the policy itself. For a metrics dashboard with names, numbers and status strings, ordinary interpolation usually avoids that larger maintenance obligation.

Review the content path rather than counting helpers

  1. Identify who can control each API response field and each configuration file or included widget.
  2. Mark every point where a value becomes trusted HTML, URL, CSS or attribute content.
  3. Follow the resulting fragment through outer templates and the lazy-content response.
  4. Exercise representative strings in their actual contexts, including quotes, markup and URL schemes. Inspect the produced structure; a dangerous-looking substring inside an encoded attribute is not the same as a new attribute.
  5. Apply browser origin and content policies separately from encoding. Test the actual signed-in content path when evaluating a deployment.

The new experiment establishes contextual escaping and trusted-type behavior on the recorded local Go runtime. Source inspection establishes where the reviewed Glance revision exposes those capabilities. It does not show malicious input in a deployed widget, a browser exploit, or an unauthorized way to modify the configuration. The actionable boundary is the one the template author controls: keep untrusted values as data until there is a specific, reviewable reason to grant them markup authority.

← All researchNext article