LearnNewsExamplesServices
Frontmatter
id17484
titleTemplate escaping is a no-op, so an apostrophe emits unparseable code
stateClosed
labels
bugaibuildsecurity
assigneesneo-opus-grace
createdAtAug 21, 2026, 5:58 PM
updatedAtAug 21, 2026, 7:52 PM
githubUrlhttps://github.com/neomjs/neo/issues/17484
authorneo-opus-grace
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 21, 2026, 7:52 PM

Template escaping is a no-op, so an apostrophe emits unparseable code

Closed Backlog/active-chunk-18 bugaibuildsecurity
neo-opus-grace
neo-opus-grace commented on Aug 21, 2026, 5:58 PM

Context

Operator surfaced the code-scanning queue for triage. Two of the open alerts — 41 and 42, rule js/identity-replacement — are a genuine defect rather than a false positive, and this ticket carries only those two. The src/Neo.mjs prototype alerts (62/63) are deliberate prototype enrichment and want a documented dismissal, not a fix; alert 64 is a separate rule in a separate file. Neither is in scope here.

This was verified before filing, not inferred from the alert. CodeQL named the line; the reproduction below is mine.

Live latest-open sweep: checked latest 20 open issues at 2026-08-21T15:5x UTC; gh search issues for identity-replacement OR templateBuildProcessor returned zero; corpus grep over resources/content/issues/** for templateBuildProcessor returned zero; A2A claim sweep over the last 30 messages found no overlapping lane. No equivalent exists.

The Problem

Both sites intend to escape single quotes before wrapping a static chunk in a single-quoted JS string literal. The comment at :119 says so outright:

// Escape single quotes for the string literal part of the chain.
return `'${part.replace(/'/g, "\'" )}'`;

In a double-quoted JS string, "\'" is not an escape sequence — it evaluates to a bare '. The call therefore replaces every apostrophe with itself. The intent and the behaviour have diverged silently since the line was written, and nothing downstream notices because the failure needs an apostrophe in static template text adjacent to a dynamic part.

Proven, not asserted:

input : "it's here"
output: "it's here"
IDENTITY: true

Reproduced through the real exported function, not a reconstruction of it — processHtmlTemplateLiteral(['<div>it\'s ', ' here</div>'], ['count']), i.e. the build-time equivalent of html`<div>it's ${count} here</div>` :

NEO_CODE_BLOCK_2

The emitted expression is 'it's ' + (count) + ' here'. With a positive control:

NEO_CODE_BLOCK_3

The control matters — it establishes that the correctly-escaped form parses, so the failure is attributable to the escaping and not to the surrounding expression shape.

Realistic impact is build-time breakage, not a runtime vulnerability. Template text is author-controlled source, so this is a correctness defect that happens to wear an injection-shaped rule name: any component author who writes a contraction in static markup next to an interpolation gets generated code that will not parse. The security label is here so the alert-triage path finds the ticket, not as a severity claim.

The Architectural Reality

buildScripts/util/templateBuildProcessor.mjs converts tagged html template literals into Neo VDOM at build time. Static chunks and ##__NEO_EXPR__ placeholders are joined into a single + addition chain, which is later emitted as source. The two flawed sites are the only places that construct a quoted literal from a raw static chunk:

  • buildScripts/util/templateBuildProcessor.mjs:120 — the text-node chain
  • buildScripts/util/templateBuildProcessor.mjs:182 — the attribute-value chain

Both are reached from the sole export, processHtmlTemplateLiteral at :312. The two paths are independent: :120 handles children, :182 handles attributes, so a fix to one does not cover the other and each needs its own coverage.

The Fix

Replace the identity replacement with a real escape at both sites, so the emitted literal carries a backslash:

NEO_CODE_BLOCK_4

A backslash in the static chunk must also survive, which the current code does not handle either — escape backslashes before quotes, or the emitted literal mis-nests on input containing \. The fix should handle both characters in the correct order rather than only the character CodeQL named.

Prefer routing both sites through one shared helper rather than repeating the expression twice: the defect exists in duplicate precisely because the logic was inlined at two call sites, and a third chain would inherit it again.

Acceptance Criteria

  • :120 and :182 emit a correctly-escaped single-quoted literal; both go through one shared escaping helper rather than duplicated inline expressions
  • Backslash in static text is escaped before the quote, so \ and ' in the same chunk both survive
  • RED-first: a test asserts the emitted expression parses (not merely that it string-matches) and fails on current dev for the text-node path
  • A second RED arm covers the attribute path at :182 independently — a fix to one site must not green the other's arm
  • Mutation control: reverting either site individually reddens its own arm and only its own arm
  • CodeQL alerts 41 and 42 close on the merge commit

Out of Scope

  • Alerts 62/63 (js/prototype-pollution-utility, src/Neo.mjs:563,565) — deliberate prototype enrichment; wants a documented dismissal decision from @tobiu, tracked separately
  • Alert 64 (js/shell-command-injection-from-environment, buildScripts/build/highlightJs.mjs:53) — different rule, different file, separate analysis
  • Any broader rework of the expression-chain emitter

Avoided Traps

Fixing only the character CodeQL named. The alert points at the quote; the backslash case is the same defect one character over and would remain after a literal-minded fix, with the alert closed and the bug alive.

Asserting the emitted string instead of parsing it. A test that compares the output to an expected string passes for any consistent-but-wrong escaping. Parsing is the property that actually matters, which is why the AC names it.

Related

Surfaced from the code-scanning queue during triage alongside 62/63/64.

Retrieval Hint: templateBuildProcessor identity replacement single quote escaping build

Origin Session ID: 752da6ac-a6c3-447f-8847-1da4ce49deb8

Decision Record impact: none — build-script correctness, no ADR authority touched. Structure-map gate: N/A, no ai/ surface and no file placement.

tobiu referenced in commit da19ff6 - "fix(build): escape backslash and quote in template string chunks (#17484) (#17485) on Aug 21, 2026, 7:52 PM
tobiu closed this issue on Aug 21, 2026, 7:52 PM