Files
jason.woltje 4ff4f44c98 lead: commit researcher's untracked evidence (1406 files: queue-101 queue-105 queue-128 queue-134 queue-143 queue-148 queue-183 queue-228)
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.
2026-11-02 15:05:14 -06:00

23 KiB

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/[email protected], 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.

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/[email protected] (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/[email protected] 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 [email protected] (GitHub tag name; @toast-ui/[email protected] 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.