Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
278 lines
23 KiB
Markdown
278 lines
23 KiB
Markdown
# Row 148 / #1642: library choice (step 1)
|
|
|
|
Researcher, 2026-10-16T17:48Z. Nothing under `public/` changed; no claim made; nothing in the repository was
|
|
installed or run. Downloads live in `/mnt/storage/scratch/researcher/q148-lib/` (each in its own new directory,
|
|
extracted with `tar`, no scripts run).
|
|
|
|
## Answer
|
|
|
|
**Toast UI Editor 3.2.2 is the only candidate that comes close, and it does not pass your grep rule as written.**
|
|
Your decision, Sage:
|
|
|
|
1. The built file contains two `Function("return this")` call sites, one written `new Function(…)` (details below). Both sit behind
|
|
`globalThis` / `window` / `self` checks and did not run in my CSP probe. A literal grep still finds them. If you rule that a
|
|
dead fallback is acceptable, I vendor it. If you rule the literal rule is absolute, **no candidate fits** and the fallback
|
|
(textarea plus a preview built from repository code) applies.
|
|
2. Its stylesheet embeds 8 `data:` images. The Console CSP has no `img-src`, so it falls back to `default-src 'self'` and
|
|
blocks them (toolbar rendered empty, 1 violation). A CSP change is not allowed, so I would ship a **derived stylesheet**
|
|
with the images extracted into files served from `'self'. That is a modification of the upstream CSS, so say yes or no.
|
|
|
|
If both are yes, the vendor set is the minified variant below.
|
|
|
|
## The candidate
|
|
|
|
| Field | Value |
|
|
|---|---|
|
|
| Name | Toast UI Editor (`@toast-ui/editor`) |
|
|
| Version | 3.2.2 (built Fri Feb 17 2023; last release) |
|
|
| License | MIT, Copyright (c) 2020 NHN Cloud Corp. Header in the bundle says `@license MIT`. |
|
|
| Bundled third-party | DOMPurify 2.3.3 (Apache-2.0 / MPL-2.0 dual), ProseMirror (MIT), tui-code-snippet (MIT) |
|
|
| Mode | Real WYSIWYG, with a Markdown tab toggle. Output is Markdown (`getMarkdown()`). |
|
|
| Upstream LICENSE | `https://raw.githubusercontent.com/nhn/tui.editor/master/LICENSE`, 1,072 B, sha256 `1c7c070da956f0c3122883c1b4eb706b1b819971082ccde001fe2d241d90e3c0` |
|
|
|
|
### Provenance gap
|
|
|
|
- The npm tarball (`@toast-ui/editor@3.2.2`, sha512 `dist.integrity` verified) has **no LICENSE file** and **no standalone browser
|
|
bundle**. Its `dist/toastui-editor.js` expects ProseMirror externals and is not usable in a plain `<script>`.
|
|
- The standalone bundle `toastui-editor-all(.min).js` comes from `uicdn.toast.com/editor/3.2.2/`, downloaded once. It is not an
|
|
npm-verified artifact. I compared it to the npm dist: 98.62% of the npm `dist` lines appear verbatim in the all.js
|
|
(the remainder is the UMD wrapper). The CSS from the CDN is byte-identical to the npm CSS (sha256 `c0ceb967…`).
|
|
- The LICENSE file comes from GitHub `master`, not from the release tag.
|
|
|
|
### Files I would vendor (recommended: minified variant, derived CSS)
|
|
|
|
Directory `public/vendor/toastui-editor-3.2.2/` (shown relative to it):
|
|
|
|
| sha256 | bytes | file |
|
|
|---|---:|---|
|
|
| f50e1b7c0fc4e5d9a1ccd0d8be78cb3a950ccb3bf676fbf1627810c76aeaedd8 | 534289 | toastui-editor-all.min.js (unmodified upstream) |
|
|
| 61aabe63e8ddb86e18d7560bd883c3fd60f8adc14851fcad3eecd472c1854dd2 | 27790 | toastui-editor.min.css (**derived**) |
|
|
| 1a5dd9ba2158c9f579545ece60c293b496249231a1ed3aea6143c6d646217275 | 23915 | img/1a5dd9ba.png (extracted) |
|
|
| e15d7afe083df2f3c835a681a905cce197f388dd5bda89788caef03d42ec0217 | 53818 | img/e15d7afe.png (extracted) |
|
|
| 9fc65db774689965f7dfc9b83390d748ed8640414078ebc39ba534c2433ac569 | 506 | img/9fc65db7.svg (extracted) |
|
|
| 0b41c43d3595f11277fd39f5e31723095ec70aefa28561145723eae234f75dda | 166 | img/0b41c43d.svg (extracted) |
|
|
| ccdcf3ad97fec299e13a5b57abc474c6c64b1228d3335c9963356d3b435c9a59 | 342 | img/ccdcf3ad.svg (extracted) |
|
|
| b2eb3ec2ef047acf17091ba8eab9c8cf34f9752c370c349c518c92a9ac725cd6 | 257 | img/b2eb3ec2.svg (extracted) |
|
|
| 1c7c070da956f0c3122883c1b4eb706b1b819971082ccde001fe2d241d90e3c0 | 1072 | LICENSE |
|
|
|
|
Total: **642,155 bytes** (641,083 of code and images plus the 1,072 B LICENSE). `vendor.sha256` would list these paths.
|
|
That is 9 files plus `vendor.sha256` itself. serve.mjs serves from an explicit route table, so it needs a route for each file the
|
|
page loads (the js, the css, the 6 images: 8 routes; LICENSE and `vendor.sha256` only if you want them served). Row 148 owns serve.mjs.
|
|
|
|
The derived CSS is `node harness/split-css.mjs` over the upstream `toastui-editor.min.css` (upstream sha256
|
|
`c70e24c68fefc205e8e504edc07fd6a5efd3044a623b4be7e3ac16cc8a736ed9`, 165,438 B). The script is copied next to this file as
|
|
`split-css.mjs`. It base64-decodes each `url(data:image/(png|svg+xml);base64,…)` into `img/<sha8>.<ext>`, rewrites the
|
|
reference to `url(img/<sha8>.<ext>)`, and throws on any other `data:`. The output has 0 `data:`, 0 `@import`, 0 `@font-face`;
|
|
its only `url()` references are the 6 image files. The SVG files contain no `<script>` and no `href`.
|
|
|
|
Readable alternative (unminified, derived), if you want a diffable file: `toastui-editor-all.js` 1,461,692 B
|
|
(sha256 `0d8c202706e3ea8d91c28307c445b55b746cf985d0994d70951b257ba207c8d9`), `toastui-editor.css` 32,958 B
|
|
(sha256 `5e0a615d740f7f0ddb44f8dfab6f37d5fc32d34b6beb924f3876429b87fb570f`), 6 images (4 svg with different bytes), total 1,574,902 B
|
|
before the LICENSE (its 6 images differ from the min set only in the 4 svg files). I recommend the minified one: 2.4x smaller, same code, same Function sites.
|
|
|
|
## Grep results (on the files I would vendor; JS is byte-identical in both variants of each pair)
|
|
|
|
Method: node regex over the file read as latin1 (`ctx.mjs` / one-line node), because this shell's grep is ugrep and fails on
|
|
long context patterns. Counts on `toastui-editor-all.min.js` (the unminified file gives the same except where noted):
|
|
|
|
| Pattern | Hits |
|
|
|---|---:|
|
|
| `eval(` | 0 |
|
|
| `new Function` | **1** |
|
|
| `Function(` (not preceded by identifier or `.`; includes the `new Function(` site) | **2** |
|
|
| `.constructor("…")` | 0 |
|
|
| `importScripts`, `new Worker` | 0 |
|
|
| `XMLHttpRequest`, `fetch(`, `sendBeacon`, `WebSocket` | 0 |
|
|
| `document.write` | 0 |
|
|
| `setTimeout("…")` / `setInterval("…")` string form | 0 |
|
|
| `insertAdjacentHTML` | 3 (5 in the unminified file; I did not trace what feeds them) |
|
|
|
|
The two `Function` sites:
|
|
|
|
1. `@25145`: `n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}()`
|
|
webpack global shim. `globalThis` exists in every supported browser, so the first branch returns.
|
|
2. `@488455`: `…typeof window&&window.Math===Math?window:Function("return this")()` lodash-style root detection, last in a chain after
|
|
`globalThis`/`self`/`window`.
|
|
|
|
Under the Console CSP these would throw an `EvalError` and fall into a `catch` if ever reached; the editor never reaches them in a browser.
|
|
|
|
External URLs in the bundle (distinct): three XML namespace strings (`w3.org`), a `prosemirror.net` docs link in a message, and
|
|
`https://www.google-analytics.com/collect`. The last is the **analytics ping**, sent through an `<img>` by default.
|
|
`usageStatistics` defaults to `true`; row 148 must pass `usageStatistics: false`. With the CSP it would be blocked anyway, but the
|
|
CSP violation would be noise and the code path writes `localStorage`.
|
|
|
|
## Runtime probe (the Console CSP, verbatim, on a scratch server at 127.0.0.1)
|
|
|
|
Page loads the vendored files over `script-src 'self'`, `style-src 'self' 'unsafe-inline'`, no `img-src`, `connect-src 'self'`.
|
|
A `securitypolicyviolation` listener plus `error` and `unhandledrejection` listeners recorded everything.
|
|
|
|
| Variant | Editor constructs | CSP violations | JS errors | Toolbar |
|
|
|---|---|---:|---:|---|
|
|
| As shipped (upstream CSS with `data:` images) | yes | **1** (`img-src`, `data:`) | 0 | icons blank |
|
|
| Derived CSS, unminified | yes | 0 | 0 | 17 of 17 icons render |
|
|
| Derived CSS, **minified** | yes | 0 | 0 | 17 of 17 icons have an image |
|
|
|
|
Network for the derived pages: `html`, `css`, `probe.js`, the JS bundle, `run.js`, one PNG. Nothing left the origin.
|
|
|
|
Behaviour check on the minified derived page, `setMarkdown` then Markdown tab then WYSIWYG tab then `getMarkdown()`:
|
|
|
|
- Headings, task list, fenced code, quote, strike, inline code and link survived.
|
|
- **Rewrites untouched content**: the bullet markers `-` came back as `*`. Row 148's save will turn a hand-written `-` list into a `*`
|
|
list on any edit. That is Git diff noise on the north-star spec, not data loss. I did not test tables, nested lists, or long files in round trip.
|
|
- Sanitizing: a pasted `<script>` block was dropped; `<img onerror=…>` came back as `<img src="x">`; `window.__pwn` stayed unset. Raw HTML in a spec is therefore not preserved.
|
|
- The task-list checkbox picture (the SVGs) loads from `'self'`.
|
|
|
|
Not tested: the image dialog, link popup, table context menu, code-block language picker, paste from the clipboard, IME input,
|
|
and any file over a few kB. The probe ran in a Chromium instance only.
|
|
|
|
## Rejected
|
|
|
|
| Candidate | Version | Reason |
|
|
|---|---|---|
|
|
| Wysimark standalone | 3.0.20 | Fetches emoji data from `cdn.jsdelivr.net` and calls `api.portive.com`; Prism worker; `Function("return this")`; 2.9 MB; last release 2023-09. Needs CSP changes. |
|
|
| Vditor | 4.0.0 | `Function('"use strict";return (…)')()` helper evaluates strings; `importScripts` Blob worker; injects inline `<script>` text (blocked by `script-src 'self'`); loads from unpkg by default; 3.7 MB Lute core. |
|
|
| EasyMDE | 2.21.0 | No eval, but not WYSIWYG (CodeMirror source plus preview, no better than the fallback). Fetches spell-checker dictionaries from `cdn.jsdelivr.net` and Font Awesome from maxcdn by default. |
|
|
| Milkdown / Crepe | 7.22.3 | **Not downloaded.** Registry metadata only: no prebuilt browser bundle, needs a bundler and dozens of transitive packages; the vendored file would be our own build output, not a published artifact. |
|
|
| MDXEditor | 4.3.3 | **Not downloaded.** Same reason, and it is React-only. |
|
|
| bytemd | 1.22.0 | **Not downloaded.** Same reason; it is also a split-pane source editor. |
|
|
| TinyMCE | 8.10.0 | **Not downloaded.** Not a Markdown editor, and the license field reads "SEE LICENSE IN license.md", not plainly permissive. |
|
|
|
|
## Risks to weigh
|
|
|
|
- Toast UI's last release is 2023-02-17 and it bundles DOMPurify 2.3.3, which is old; the sanitizer is the thing that stops pasted
|
|
HTML, so a known DOMPurify bypass would matter. I did not check DOMPurify 2.3.3 advisories.
|
|
- The standalone bundle is not npm-verifiable (see provenance gap). The sha256 values above pin what I downloaded, and no more.
|
|
- The stylesheet is modified from upstream, and `vendor.sha256` will pin the derived files, not upstream's.
|
|
|
|
## Fallback
|
|
|
|
If you rule against Toast UI: a `<textarea>` plus a rendered preview from a small Markdown renderer written in the repository
|
|
(headings, lists, emphasis, code, links, tables), no vendored code, no CSP change, no license file. It loses WYSIWYG editing.
|
|
That is the brief's own fallback.
|
|
|
|
## Cleanup
|
|
|
|
Scratch server on 127.0.0.1:18148 and the isolated browser page are stopped after this write-up. All downloads stay in scratch.
|
|
|
|
# Step 1b: DOMPurify 2.3.3 advisories and Toast UI provenance
|
|
|
|
Researcher, 2026-10-16T17:58Z. Same scratch rules as step 1: nothing under `public/` changed, no claim, nothing pushed, no credential
|
|
used. Only unauthenticated reads (GitHub advisory API, OSV API, npm registry, raw GitHub files). Evidence lives in
|
|
`/mnt/storage/scratch/researcher/q148-1b/` (`adv/`, `tag/`, `dompurify-meta/`) and the harness pages in
|
|
`/mnt/storage/scratch/researcher/q148-lib/harness/site/`. No downloaded script was run; the PoC test suite from the DOMPurify fix
|
|
commit was read, not executed.
|
|
|
|
## 1. DOMPurify 2.3.3: advisories
|
|
|
|
Method: GitHub Advisory API for `cure53/DOMPurify` (all 30 advisories saved in `adv/dompurify-advisories.json`) and the OSV query
|
|
for `npm/dompurify@2.3.3` (`adv/osv-dompurify-2.3.3.json`, 20 hits). I then read each advisory text and checked each against what
|
|
Toast actually calls. The two sources disagree on ranges: OSV marks 20 as `introduced=0`, GitHub gives lower bounds only for some.
|
|
Many of the 20 describe options or code that I did not find in 2.3.3 (see the "not reachable" rows). I list the 20 OSV hits; the
|
|
other 10 GitHub advisories have a lower bound above 2.3.3 (3.0.x or later) and are left out.
|
|
|
|
### What Toast passes to DOMPurify (call-site inspection of `toastui-editor-all.js`)
|
|
|
|
- One wrapper: `sanitizeHTML(html, options)` -> `purify_default().sanitize(html, {ADD_TAGS: <array>, ADD_ATTR: ['rel','target','hreflang','type'], FORBID_TAGS: [...], ...options})`.
|
|
- Four module-level callers: `@1183440` (`img[src]` parseDOM, `RETURN_DOM_FRAGMENT: true`), `@1193888` (`a[href]` parseDOM, same),
|
|
`@1224346` (`getMatchedAttributeValue`, string result into `innerHTML`), `@1240027` and `@1241321` (markdown-to-WYSIWYG inline/block
|
|
HTML convertor, string result).
|
|
- Never set: `IN_PLACE`, `USE_PROFILES`, `SAFE_FOR_TEMPLATES`, `setConfig()`, `CUSTOM_ELEMENT_HANDLING`, Trusted Types options, function-form
|
|
`ADD_TAGS`/`ADD_ATTR`. `CUSTOM_ELEMENT_HANDLING` does not exist in the 2.3.3 source at all (0 hits). I found no DOMPurify hook
|
|
registration (the single `addHook(` hit is the editor's own events hook, `@1318242`).
|
|
- Output goes back in through `innerHTML` (into a `div`) or through ProseMirror `parseDOM`.
|
|
|
|
### Advisories that matter for 2.3.3
|
|
|
|
| GHSA | CVE | Sev (CVSS) | Affects 2.3.3 | Fixed in | Source | Reachable via Toast's config | Stopped by `script-src 'self'`, no unsafe-inline |
|
|
|---|---|---|---|---|---|---|---|
|
|
| GHSA-gx9m-whjm-85jf | CVE-2024-47875 | critical (10.0) | yes, `<2.5.0` | 2.5.0, 3.1.3 | https://github.com/advisories/GHSA-gx9m-whjm-85jf | **Yes.** Nesting-based mXSS, default config. I verified on 2.3.3 in Chromium that there is no nesting-depth limit (2.5.0 added it); `purify-depth.html`. | **Script effects: yes.** Markup it yields is not filtered by `FORBID_TAGS`/`ADD_TAGS` because the mutation happens after the filter. Inline handlers, `javascript:` URLs and inline `<script>` do not run under the CSP (tested, section 2). HTML injection (links, inline-style overlays) is not stopped. |
|
|
| GHSA-mmhx-hmjr-r674 | CVE-2024-45801 | high (7.0) | yes, `<2.5.4` | 2.5.4, 3.1.3 | https://github.com/advisories/GHSA-mmhx-hmjr-r674 | **Partly.** It bypasses the depth check added in 2.5.0, and 2.3.3 has no depth check, so it is covered by the row above. The prototype-pollution half needs a pollution primitive in the page; I found none in Toast and did not look elsewhere. | Same as above for the nesting half; pollution half is independent of the CSP. |
|
|
| GHSA-p3vf-v8qc-cwcr | CVE-2024-48910 | critical (9.1) | yes, `<2.4.2` | 2.4.2 | https://github.com/advisories/GHSA-p3vf-v8qc-cwcr | **Only with a separate prototype-pollution bug** in the page. No such primitive found in Toast; I did not audit the Console. | No (it tampers with DOMPurify's own config; the follow-on effect is the same HTML injection). |
|
|
|
|
### Advisories whose OSV range includes 2.3.3 but whose code path Toast does not use
|
|
|
|
All "Reachable: no" claims below come from the Toast call-site list above, not from running a PoC. For each, "affects 2.3.3" is
|
|
the OSV range claim (`introduced=0`); I did not test whether 2.3.3 actually has the flaw, because the option is never set.
|
|
|
|
| GHSA | CVE | Sev | Fixed in | Source | Why not reachable via Toast |
|
|
|---|---|---|---|---|---|
|
|
| GHSA-39q2-94rc-95cp | CVE-2026-65903 | low | 3.4.0 | https://github.com/advisories/GHSA-39q2-94rc-95cp | needs function-form `ADD_TAGS`; Toast passes an array |
|
|
| GHSA-h7mw-gpvr-xq4m | CVE-2026-41240 | medium | 3.4.0 | https://github.com/advisories/GHSA-h7mw-gpvr-xq4m | same |
|
|
| GHSA-cjmm-f4jc-qw8r | CVE-2026-65912 | medium | 3.3.2 | https://github.com/advisories/GHSA-cjmm-f4jc-qw8r | needs function-form `ADD_ATTR`; Toast passes an array |
|
|
| GHSA-cj63-jhhr-wcxv | CVE-2026-65913 | medium | 3.3.2 | https://github.com/advisories/GHSA-cj63-jhhr-wcxv | needs `USE_PROFILES` plus Array.prototype pollution |
|
|
| GHSA-crv5-9vww-q3g8 | CVE-2026-41239 | medium | 3.4.0 | https://github.com/advisories/GHSA-crv5-9vww-q3g8 | needs `SAFE_FOR_TEMPLATES` with `RETURN_DOM`; Toast does not set it |
|
|
| GHSA-vhxf-7vqr-mrjg | CVE-2025-26791 | medium | 3.2.4 | https://github.com/advisories/GHSA-vhxf-7vqr-mrjg | needs `SAFE_FOR_TEMPLATES` |
|
|
| GHSA-h8r8-wccr-v5f2 | CVE-2026-65914 | medium | 3.3.2 | https://github.com/advisories/GHSA-h8r8-wccr-v5f2 | **Unknown.** GitHub lists only 3.3.1; OSV says from 0. mXSS by re-contextualisation, no option needed. I did not test it on 2.3.3. Treat as possibly reachable; same CSP effect as the first row. |
|
|
| GHSA-55q2-fjhq-7xh7 | CVE-2026-75838 | low | 3.4.13 | https://github.com/advisories/GHSA-55q2-fjhq-7xh7 | `IN_PLACE` plus hooks; not set |
|
|
| GHSA-6688-9rhm-gjv2 | CVE-2026-109345 | low | 3.4.16 | https://github.com/advisories/GHSA-6688-9rhm-gjv2 | `IN_PLACE`; not set |
|
|
| GHSA-hpcv-96wg-7vj8 | CVE-2026-49458 | medium | 3.4.6 | https://github.com/advisories/GHSA-hpcv-96wg-7vj8 | `IN_PLACE`; not set |
|
|
| GHSA-r47g-fvhr-h676 | CVE-2026-49459 | medium | 3.4.6 | https://github.com/advisories/GHSA-r47g-fvhr-h676 | `IN_PLACE`; not set |
|
|
| GHSA-x4vx-rjvf-j5p4 | CVE-2026-65901 | low | 3.4.7 (OSV gives no fix; GitHub says `<= 3.4.6`) | https://github.com/advisories/GHSA-x4vx-rjvf-j5p4 | `IN_PLACE`; not set |
|
|
| GHSA-rp9w-3fw7-7cwq | CVE-2026-49978 | medium | 3.4.7 | https://github.com/advisories/GHSA-rp9w-3fw7-7cwq | `IN_PLACE`; not set |
|
|
| GHSA-76mc-f452-cxcm | CVE-2026-65902 | medium | 3.4.7 | https://github.com/advisories/GHSA-76mc-f452-cxcm | needs a DOMPurify hook mutating allow-lists; Toast registers none |
|
|
| GHSA-cmwh-pvxp-8882 | CVE-2026-65898 | low | 3.4.11 | https://github.com/advisories/GHSA-cmwh-pvxp-8882 | needs `setConfig()`; Toast does not call it |
|
|
| GHSA-c2j3-45gr-mqc4 | CVE-2026-66010 | low | 3.4.12 | https://github.com/advisories/GHSA-c2j3-45gr-mqc4 | `CUSTOM_ELEMENT_HANDLING`; absent from 2.3.3 |
|
|
| GHSA-vxr8-fq34-vvx9 | CVE-2026-65899 | low | 3.4.9 | https://github.com/advisories/GHSA-vxr8-fq34-vvx9 | Trusted Types policy; not set |
|
|
|
|
Not applicable (GitHub lower bound above 2.3.3): GHSA-87xg-pxx2-7hvx, GHSA-v9jr-rg53-9pgp, GHSA-4w3q-35jp-p934, GHSA-9p3w-6h5p-cv75,
|
|
GHSA-gvmj-g25r-r7wr, GHSA-c6gr-qvv4-5v3f, GHSA-mrv2-3pqr-j5jr, GHSA-cg2g-xh64-v4q7,
|
|
GHSA-p98j-92pf-mc4p. (These are all 3.x-only; I did not read their text beyond the ranges.) OSV has no entry for `@toast-ui/editor@3.2.2`
|
|
itself (`adv/osv-toastui-3.2.2.json` is `{}`). That is absence of a record, not a clean bill: Toast is unmaintained since 2023-02.
|
|
|
|
### 2. Does the Console CSP stop the effect? (empirical, Chromium, the exact CSP from `serve.mjs:209`)
|
|
|
|
Page: `csp-effects.html/js` on the scratch server. Each payload was inserted with `innerHTML` into a same-origin page under the CSP,
|
|
which is what Toast does with sanitizer output.
|
|
|
|
| Payload | Result under the CSP |
|
|
|---|---|
|
|
| `<img src=x onerror=...>` / `<svg onload=...>` inline handlers | **blocked** (script-src has no unsafe-inline), violation reported |
|
|
| `<a href="javascript:...">` click | **blocked** |
|
|
| `<iframe srcdoc="<script>...">` | **blocked** (srcdoc inherits the policy) |
|
|
| `<img src=https://other.example/x>` | **blocked** (img-src falls back to `default-src 'self'`), so no beacon via images |
|
|
| `<link rel=stylesheet href=https://...>`, CSS `url(https://...)` | **blocked** |
|
|
| `<form action=https://other.example>` | **blocked** (`form-action 'self'`) |
|
|
| Plain `<a href=https://other.example>` | **not blocked**: user can be sent off-site by a click |
|
|
| `<meta http-equiv=refresh content="0;url=https://...">` | **not blocked** by the CSP. Toast's `FORBID_TAGS` drops `meta`, so it only matters after an mXSS bypass. |
|
|
| Inline `style=` (full-page overlay, fake UI) | **not blocked**: `style-src 'unsafe-inline'` is in the Console CSP |
|
|
| `<form action=/same-origin-path method=post>` | **not blocked** (`form-action 'self'`); `form` is in Toast's `FORBID_TAGS` but not after mXSS. Whether a same-origin POST can do harm depends on Console routes I did not review. |
|
|
|
|
So the CSP turns the critical advisories from script execution into HTML injection. It does **not** make 2.3.3 safe: the Console
|
|
shows a human content that an agent can write (agents author the `docs/` files this editor opens), so link redirection, overlay
|
|
spoofing and same-origin form POST remain possible after an mXSS bypass.
|
|
|
|
## 3. Provenance
|
|
|
|
| Question | Finding |
|
|
|---|---|
|
|
| Tag | `editor@3.2.2` (GitHub tag name; `@toast-ui/editor@3.2.2` returns 404). Commit `9b94c04231d4600b42347ff1b99d9813e9becf67`. |
|
|
| GitHub release | No release object for this tag; tag only (`tag/tags-list.json`, `tag/releases.json` checked). |
|
|
| `toastui-editor-all.min.js` in tag or release assets | **No.** The repo tree at the tag (`tag/tree.json`) has no `dist/` and no bundle; no release assets. |
|
|
| Reproducible build | **Not reproduced.** The build is a webpack run via upstream scripts and `npm install` of its dev dependencies. Both are disallowed here (no install, no running downloaded scripts). I cannot say a build from the tag would produce f50e1b7c… |
|
|
| Second source | None found. The bundle exists only at `uicdn.toast.com/editor/3.2.2/` (CDN of the vendor); the npm tarball has `dist/toastui-editor.js` (not standalone). I compared line content earlier: 98.62% of the npm dist lines occur verbatim in the all.js. |
|
|
| CSS | Upstream CDN CSS equals the npm CSS byte for byte (`c0ceb967…` unminified). The tag's own style sources (`tag/src-contents.css`, `tag/src-editor.css`) were fetched only to compare the images. |
|
|
| Images | The 2 PNGs extracted from the CSS match the PNGs in the tag source byte for byte. The 4 SVGs in the **unminified** CSS match the tag source; the 4 SVGs in the **minified** CSS differ in bytes (the minifier rewrote them). The minified set is therefore not byte-identical to source for SVGs; it is what the CDN served. |
|
|
| LICENSE at tag vs master | **Identical.** `tag/LICENSE-tag-editor-3.2.2` sha256 `1c7c070da956f0c3122883c1b4eb706b1b819971082ccde001fe2d241d90e3c0`, 1072 B, same as the master copy in step 1. Row 148 can vendor it from the tag with a recorded source URL. |
|
|
|
|
Net: the vendored JS is pinned to a CDN download with no independent confirmation. A reviewer can re-fetch the same URL and compare
|
|
the sha256 and that is all; there is no upstream-signed hash and no reproducible build I could run.
|
|
|
|
## 4. If a 1b finding is bad
|
|
|
|
It is bad, in the sense that the critical nesting mXSS (GHSA-gx9m-whjm-85jf) is reachable with Toast's default config and the CSP only converts it into HTML injection.
|
|
One line: I would not rely on `customHTMLSanitizer` alone, because four internal call sites (img/a `parseDOM`, `getMatchedAttributeValue`,
|
|
and the markdown-to-WYSIWYG HTML convertor) call the bundled 2.3.3 directly and ignore it; so the choice is to splice DOMPurify 2.5.9 (latest 2.x, same major as the
|
|
bundled 2.3.3, so the `sanitize()` options Toast uses stay the same; it fixes both 2024 advisories, and no 2.x-range advisory above lists a fix later than 2.5.4) into
|
|
the bundle's module slot (the vendored JS then becomes derived like the CSS and loses its single upstream sha256; **untested**, I
|
|
would download 2.5.9 into its own scratch dir, check npm integrity, then run the nesting test, the Toast integration page and the
|
|
0-violation CSP check before any build), or fall back to textarea plus preview if that cannot be verified or a derived JS file is refused.
|
|
DOMPurify 3.4.17 is the alternative splice, with more API drift from 2.3.3 to check.
|
|
|
|
## Not verified
|
|
|
|
- A splice of a newer DOMPurify into the bundle (not built, not run).
|
|
- Whether 2.3.3 is actually vulnerable to GHSA-h8r8-wccr-v5f2 and the other OSV-only entries; I judged them by call-site reachability.
|
|
- Whether the Console has a same-origin POST route that a forged `<form>` could hit.
|
|
- A pollution primitive anywhere in the Console page.
|
|
- Anything in Firefox or Safari; all runtime tests were in one Chromium.
|