# mmo.client — Security Review
**Scope:** `libs/client` (the `mmo.client` package) — the JS runtime (`src/js/**`), the `.jhs` compiler (`src/JHSCompiler.php`, `src/jhs/**`), the Sass/build pipeline (`src/DartSass.php`, `src/GoogleClosure.php`, `src/SassParser.php`, `src/Theme.php`, `src/DocGenerator.php`), and the repo-root files that sit next to the web-servable `demo/` folder (`.htaccess`, `test.php`).
**Method:** direct source reading plus a targeted multi-agent audit (Sept 2026) tracing real data flow into every HTML-insertion/code-execution sink, plus a review of the compile-time (PHP) pipeline. Findings are anchored to `file:line`; severity reflects **exploitability in this framework as shipped**, not worst-case theoretical risk.
**Framing:** this is a defensive review of Marc's own framework, intended to harden it before more consumers build on `bind-html`, `mmo-markdown`, etc. Several findings are "not exploited today because nothing uses this path yet" — that is deliberately treated as urgent, not low-priority, because the path is fully wired and one `.jhs` author away from being used.
## Summary
| # | Finding | Area | Severity | Status |
|---|---|---|---|---|
| 1 | `Printer.render()` has no escaping fallback for plain strings rendered as HTML | XSS | **High** | Real, root cause of #2/#3 |
| 2 | `.jhs` compiler defaults `{=...}` text interpolation to the HTML sink, not the text sink | XSS | **High** | Real — live in `mmo-chart2.jhs:86` |
| 3 | `bind-html` / `$html` / `linkHtml` is a fully-wired, zero-sanitization two-way raw-HTML binding | XSS | **High** | Real, unused by shipped elements so far |
| 4 | `MMO.core.markdown` — unescaped plain text, attribute-breakout in image syntax, unvalidated link/image URL scheme | XSS | **High** | Real, reachable via shipped `` |
| 5 | `Map.toObject()`/`MMO.core.merge()` — no `__proto__`/`constructor` guard when converting reactive data to plain objects | Prototype pollution | **Medium** | Real, conditional on attacker-influenced JSON reaching these paths |
| 6 | `test.php` at repo root + `.htaccess` `.php` allowlist | Info disclosure | **Medium–High** | Real *if* this repo root is ever the deployed docroot |
| 7 | `DocGenerator::sanitizeInlineHtml()` — `javascript:` scheme bypass on `` | XSS | Medium | Dead code today (class has no caller) |
| 8 | `MMO.core.strings.generatePassword()` uses `Math.random()` | Weak randomness | Medium | Real if used for actual credentials |
| 9 | Shell command builder (`shell\serialize()`) doesn't escape positional args; used by `GoogleClosure.php`/`DartSass.php` | Command injection (latent) | Medium | Not currently reachable with attacker-controlled filenames |
| 10 | `MMO.core.svg.insertText()` uses `innerHTML` instead of `textContent` for SVG labels | XSS | Medium | Half fixed by #16: the first `` is text, the `` split of a long label is not |
| 11 | No built-in CSRF token/header handling in `APIRequester`/`server.js` | CSRF | Low–Medium | Framework-level gap, left to the consuming app/server |
| 12 | `MMO.core.Storage` persists arbitrary entity JSON to `localStorage`/`sessionStorage` unencrypted, no field redaction | Sensitive data at rest | Low | Depends entirely on what a consumer stores through it |
| 13 | JSON-Schema `pattern` compiled directly into `RegExp` | ReDoS | Low | Depends on schema provenance (assumed trusted) |
| 14 | Vendored dart-sass (1.57.1) / Closure Library (20230228) are old, unpinned in `mmo.json` | Dependency hygiene | Info | — |
| 15 | `server.js:extractLink()` assigns `data`/`m` without `let/const/var` (implicit globals) | Code fragility | Low | Not a vulnerability by itself; fix while nearby |
| 16 | `MMO.core.dom.createElement()`/`appendChildren()` parse a string child as HTML | XSS | **High** | **Fixed** in 3.1.0 (Sept 2026): a string child is a text node, `MMO.core.dom.html()` marks markup |
| 17 | ``'s messages (`notify`, `alert`, `confirm`, `ask`, `error`, `showModal`, `showNotification`, `showPanel`) take HTML strings | XSS | **High** | **Fixed** in 3.1.0 (Sept 2026): text unless it is a node |
No findings for: `eval`/string-arg `setTimeout` (none exist outside the one intentional `new Function` in `Template.js:274`, which only ever compiles static `.jhs`-authored `on*` strings); prototype pollution via `Map`'s own `set`/`merge` (native `Map` storage isn't subject to `__proto__` key tricks); open redirect via `MMO.url()` (base is always validated against `MMO.config.paths`); `SassParser.php`/`Theme.php`/`functions.php` (no shell calls, no request-driven file paths).
---
## 1–4. XSS: the templating language defaults to "unescaped HTML" in several places
These four findings share one root cause and should be fixed together: **this framework's HTML-insertion sinks are currently opt-out, not opt-in.**
### 1. `Printer.render()` has no default escaping for strings
`src/js/core/Printer.js` registers renderers for `date/boolean/number/phone/euro*/km/percent/bytesize/array/object/url/file-url/scopes/roles/tags/tag/status` (lines ~94–254) but never for the plain `'string'` type. The fallback (line ~80, `return (value+'')`) returns the raw string untouched. `MMO.core.Map.getHTML(key)` (`src/js/core/Map.js:145-149`) and `Schema.renderHTML()` (`src/js/schemas/Schema.js:465-468`) both fall through to this for ordinary string fields — so any code path that ends up calling `getHTML()`/`renderHTML()` on a string returns it completely unescaped and callers reasonably assume it's safe to insert as HTML because it came from "the renderer."
**Fix:** register a default `'string'` renderer that HTML-escapes (reuse `MMO.core.htmlentitiesEncode`, already used in `markdown.js`) whenever `opts.html` is true. Require an explicit marker (e.g. a wrapper type, or `opts.trustedHtml`) for the rare case where a string is genuinely pre-sanitized HTML.
### 2. The `.jhs` compiler defaults `{=js}` text interpolation to the HTML sink
`src/jhs/Printer.php:121`: `$fn = ($opts['html']??true)? 'html' : 'text';` — and no caller anywhere in `JHSCompiler.php`/`jhs/*.php` ever passes `opts['html'] = false`. Confirmed against the compiled output (`demo/www/bin/mmo.client.jhs.js:969, 3484`): ordinary `{=...}` expressions compile to `.$htmlFn()`, not `.$textFn()`. A live instance: [`src/customElements/elements/mmo-chart2.jhs:86`](../src/customElements/elements/mmo-chart2.jhs) — `| ...{= $this.view.get('config/legend/' + $i)} | ` renders a chart's legend label (documented as "list of title for each set in data", i.e. caller-supplied) through the raw-HTML sink.
**Fix:** flip the default to `$opts['html']??false` — text-mode by default — and require something explicit (a `jhs-html` marker, or route through `bind-html`) to opt into HTML-mode interpolation. This is a breaking change to compiler output but is the correct default for a template language that mixes static markup with bound data; every existing `.jhs` file should be recompiled and spot-checked after the change (`compileText(..., $debug=true)` — see the `mmo-client` skill's "Compiling / testing a change" section — makes the diff easy to inspect).
### 3. `bind-html` is a fully-wired, unsanitized two-way raw-HTML binding
`src/jhs/Printer.php:63-69` compiles `bind-html="path"` to `.$html('path', true)`. `View.js:787-841` (`htmlUpdater`/`linkHtml`/`$html`) then does two dangerous things: it inserts the bound value via `MMO.core.dom.appendChildren` → `addHTML` → `p.innerHTML = html` (`src/js/core/functions/dom.js:848-857`, no sanitization), **and**, because `bind=true`, it listens for `blur` and writes `node.innerHTML` straight back into the data model (`View.js:834`). No shipped `.jhs` file currently uses `bind-html` (confirmed via grep), but it's a documented, first-class part of the template language — exactly the kind of thing a future contact-editable-bio or ticket-description field would reach for.
**Fix:** sanitize on both the read path (before `.innerHTML =`) and the write-back path (before `this.set()`), using an allowlist-based sanitizer. At minimum, document prominently (done in the new `mmo-client` skill, see `SKILL.md` point 4 and `references/jhs-syntax.md`) that `bind-html` must never be bound to a field whose value is later shown to a different user without server-side sanitization too.
### 4. `MMO.core.markdown` — the most likely real-world XSS path
`src/js/core/functions/markdown.js`, reachable today via the shipped `` element (`src/customElements/modules/mmo-markdown.jhs`, whose entire purpose is `MMO.core.markdown.insert(this, value, opts)` on an arbitrary `value`):
- `addHtml()` (lines 9-17) does `root.innerHTML += html`.
- Plain (non-formatted) text runs are concatenated into that HTML string **unescaped** (lines 155, ~200) — only text matched by the inline-formatting regex (code/em/strong/sub/sup/mark) is escaped (lines 161-184). Literal `