Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74ec320135 | ||
|
|
85ae2be773 | ||
|
|
b7daf5198b | ||
|
|
e36bce3526 | ||
|
|
17a48a7782 | ||
|
|
e012bab0be |
No files matched your search
@@ -5344,3 +5344,21 @@ Gander: after Duckworth-0100 design approval with C1-C5, prepared the two host-s
|
||||
2026-10-22T18:11:57Z | Sage | Row 275 (#1780, Morgana, Duckworth) LANDED 3410df7c3. Before: refactor at 48e450ce8; viewers had no verb list of their own and board reads were not refused to users. After: a fast-forward of 3 commits; manifest 39847b15 (4 paths) blobs match; gate 17:52:12Z-18:05:00Z green (whole files 7/22/6, bus 834, cli 861, harness 151, business 231, seat 167, 10 scripts with 3 named skips), M6 and M10 killed; Duckworth APPROVE rev 3260 (comment 30717); note and done by sage-2897; #1780 comment 30728 and closed. N1 (board refusal test as a loop over BOARD_VERBS) is non-blocking.
|
||||
|
||||
2026-10-22 18:11:58 UTC | Gander row276 #1781 prepared, gate pending | Before: row253 left5legacy Case1 AS_USER calls unsupervised across sudo pty sessions. After: candidate uses its unchanged supervisor with per-call inner/outer budgets, separate streams, strict display-only failure trailer, fake-only paired process-model cases and15unique mutants. Duckworth0182 B1 corrected the plan before product code;0183 cleared build. Parse-only Bash checks pass; no Node, behavioral test, host, unit or config action. Runtime gate and publication await Sage.
|
||||
|
||||
2026-10-22T23:03:39Z | Gyro Q195 O2 rewrite, #1752 / Vikunja #448, mos-1356 | Before: landed O3 design 95e3f7166/blob 4cb59999. After: Q231/Q233 full WordCraft O2 design on gyro/editor-o2 from refactor c4f77373f; canonical git DOCX, original Markdown/wiki preservation, human cadence default 0 with instant accepted-save commits, full rich-feature first slice. Ten briefs include independent source/dependency security clearance before any build queue entry, exact editor-only CSP approval gate, restricted source/build/bridge, separate reviewed DOCX citation gate and flags-only agent access. Existing Case 2 semantics unchanged; any integration requirement change goes through Mos to Jason. Static checks: ten briefs/seven fields, independent reviewers, unique anchors, current source/base/blob pins, docs scope and append-only logs pass. No builds, browser/worker runs, code, queue or live changes. Fenton review pending; CSP and implementation gates remain closed.
|
||||
|
||||
2026-10-22T23:03:53Z | Gyro Q195 O2 push guard | CI queue guard identity gyro / purpose push returned exit 0, documented degraded-pass: CANNOT_ASSERT credential-unresolvable. Audit: /mnt/storage/scratch/gyro-q195-ci-queue-audit.jsonl. No credential fallback or CI-green claim.
|
||||
|
||||
2026-10-22T23:09:38Z | Gyro Q195 O2 fenton-0006 fixes | Before: e012bab0b left stale-source ambiguity, incomplete migration/shipping ownership, undefined typing transactions and an unmeasured CSP gate. After: D12 explicitly routes superseded-source/C2 reply notices to Mos/Jason and refuses adoption before W4b/notice path; Q2 remains open. F2-F6 add schema-edge placeholders and restore-accounting files, shipped runtime data/writing closure with no sandbox store mount, per-chapter DOCX and 2s-pause/15s-span save groups, volume/latency/growth gates, split-run clickable wikilinks and historical Obsidian graph warning, and S0 isolated existing-artifact CSP measurement before exact-header approval. Static ten-brief/ownership/settings/checklist checks pass. No build/browser run, product/queue/live change. Independent re-check pending.
|
||||
|
||||
2026-10-22T23:09:50Z | Gyro fenton-0006 push guard | identity gyro, purpose push, exit 0 documented degraded-pass CANNOT_ASSERT credential-unresolvable; audit gyro-q195-ci-queue-audit.jsonl. No fallback credentials or CI-green claim.
|
||||
|
||||
2026-10-22T23:10:14Z | Gyro mos-1358 ruling recorded | Q36 / Vikunja 562 keeps C2 superseded notices OPEN with Jason. Markdown adoption refusal until W4b PASS is now a separate settled Mos build-order ruling, not a Jason question. D12 and Q2 distinguish them; no notice build authorized.
|
||||
|
||||
2026-10-22T23:13:27Z | Gyro Q195 O2 fenton-0007 F7/F8 | Named human-only editor.saveGroupPauseMs=2000 and editor.saveGroupMaxMs=15000 with integer/range/cross-field validation and W3a UI/CLI refusal tests. W3b owns production adoption refusal until W4b; isolated fixture conversion remains testable without request-flag bypass. W4b owns adoption module/API tests and reviewed lifting only with committed-reader/citation readiness and, if Q36 approves, the separate notice row PASS. Q36 unresolved stays closed. Static ten-brief, setting-key/bounds, refusal/lift ownership and two-question checks pass. Docs only; no build, browser/worker run, product/queue/live change. Fenton checks F7/F8 on next push.
|
||||
|
||||
2026-10-22T23:13:48Z | Gyro mos-1359 / r3 push guard | D13 records save-group timing as Mos's accepted reading of Q226 F41, not Jason's ruling; both defaults remain human-only settings. Push guard identity gyro/purpose push: exit 0 documented degraded-pass, CANNOT_ASSERT credential-unresolvable; audit gyro-q195-ci-queue-audit.jsonl, no credential fallback or CI-green claim.
|
||||
|
||||
2026-10-22T23:58:36Z | Fenton Q195 O2 S0 (#1798, sage-3058 under mos-1360) | Before: partial inventory b7daf5198, no verdict. After: CONDITIONAL PASS on WordCraft e05b7fe8. Inventory 15 local / 199 normal / 10 build-only / 25 build scripts / 10 proc-macros, 3 git deps by commit. Licenses resolved by a notice bundle (L1: epaint fonts missing from WordCraft NOTICE; self_cell and ryu elect Apache-2.0). Findings F1-F15 (F1 wuff WOFF1 orig_length allocation trap reachable from DOCX open, source only), mandatory mitigations M1-M14 with rejection tests. Browser on the copied artifact: style-src self 1 violation (rfd inline style, removed by host-owned Open), style-hash 0 violations, all functions run; Print blob iframe and cross-origin frame navigation blocked. D8 hash fallback returned to Mos, not recommended. Corrections: style-hash attempt 1 had a harness-made connect-src violation (rerun clean); Print evidence copied from the transcript. No compile, no Console/policy/live change, loopback-only fixtures. Filbert reviews.
|
||||
|
||||
2026-10-23T00:06:03Z | Fenton Q195 O2 S0 amendments (#1798, sage-3085, mos-1363) | Before: 85ae2be77 CONDITIONAL PASS; Filbert concurred with A1/A2 required (comment 30914). After: A1 M1 refuses WOFF1 per-table and summed origLength and WOFF2 totalSfntSize/brotli uncompressed_size before decode (WOFF2 a regression case, wuff bounds it at 128 MiB). A2 corrected F2: ctrl+click is already limited to http/https/mailto (canvas.rs:2490-2501, link.rs:7); exposure is DOCX save writing targets back (write/story.rs:335-340); F2 now low, M2 retargeted at host-saved DOCX output plus https under M7. N1-N3 fixed in the CSP report (resize receipt, harness/ prefix, loopback claim). D8 recorded as ruled by Mos mos-1363: style-src self, hash not approved. Docs only, no slot.
|
||||
@@ -1549,3 +1549,8 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-10-22 | Della | Row258 E01 3094e3bfa focused manifest review | APPROVED 11-path manifest/static F1/F2/N1 corrections; file and full-blob digest recipes verified. M1-M11 fixed; all execution/landing evidence still required in CLEAR. No shared-log edit, Node, tests, HTTPS Git or live action.
|
||||
2026-10-22T17:55:40Z | Sage (Opus 5.5, 1ef1e4f8) | Row 253 landing; post-253 doc sweep | 253 LANDED 48e450ce8. Sweep on sage/sweep-253: Gosalyn Case 2 design and rows, 229 design, Gyro 0acbdb0d8, Della 179391055, 268 docs, seat dirs for Della, Scrooge, Huey, Louie and Ludwig, human-users-build:70 under Q173, DEFERRED 253 N1, 198 N2, row 203 text, 224 residual and 206 sharee list, and seat log lines.
|
||||
2026-10-22 14:48 UTC | Researcher (Sonnet 5.5, 9bac0794) | Rows 220 and 216 resumed (sage-2708, sage-2716, sage-2719); local commits only | 220 e4a204836 on researcher/row220-r2 (on ea6dc9eff): C1 run tests, C2 default-reader M7 test, configDirectory fixture scope, BUILD.md round 2b. 216 4f3fbc25a on researcher/row216-r2 (on aca16404c): index cache, find/get/search/manuscript/universeInfo, PRD 1.14 levels. Nothing pushed, claimed or run; node --check not run. Waits for Sage's CLEAR and go; rebase onto 253 on a new branch before any gate.
|
||||
2026-10-22T23:03:39Z | Gyro (Codex gpt-6.1-sol, mos-1356) | Q195/Q231/Q233 O2 design rewrite, gyro/editor-o2 from c4f77373f | Canonical queue next gyro: nothing. Ten docs-only briefs; full-feature WordCraft, independent 15-crate/199-package security review, editor-only CSP candidate, instant git DOCX saves and retained Markdown/wiki sources, committed-only DOCX citation gate, no agent story prose. Static structure/source-anchor/scope checks only; no code, build/run, queue or live change. Fenton review requested after push.
|
||||
2026-10-22T23:09:38Z | Gyro (Codex gpt-6.1-sol, fenton-0006) | Q195 O2 design corrections on gyro/editor-o2 | F1 requirement boundary sent to Mos, adoption gated; F2-F6 source-checked docs fixes prepared. Ten briefs and two open owner items checked statically. No implementation, build/browser/worker run, queue or live action; Fenton re-check after push.
|
||||
2026-10-22T23:13:27Z | Gyro (Codex gpt-6.1-sol, fenton-0007) | Q195 O2 r3 F7/F8 | Save-group keys/defaults/validation and W3b refusal/W4b lift ownership explicit. Static docs checks only. Q36 remains open; no implementation or gate lift. Direct Fenton/Mos handoff after push.
|
||||
2026-10-22T23:58:36Z | Fenton (Claude Opus 5.5, sage-3058) | Q195 O2 S0 WordCraft source security and CSP review, fenton/q195-s0 | CONDITIONAL PASS, M1-M14 mandatory, D8 style-src hash held for Mos. Reviews docs only; Filbert review requested.
|
||||
2026-10-23T00:06:03Z | Fenton (Claude Opus 5.5, sage-3085) | Q195 S0 amendments A1/A2 and N1-N3 on fenton/q195-s0, D8 ruling recorded | Docs only; Filbert re-check requested.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,184 @@
|
||||
# Q195 O2 S0: editor frame CSP measurement
|
||||
|
||||
Status: COMPLETE. Feeds the S0 verdict in `2026-10-22_q195-wordcraft-security.md`.
|
||||
|
||||
- Review id `s0-20261022`, Gitea #1798. Owner: Fenton. Reviewer: Filbert.
|
||||
- Spec: `docs/plans/2026-10-21_q195-writing-editor.md` blob `322ec50f` at e36bce352, D7 and D8.
|
||||
- Scratch: `/mnt/storage/scratch/q195-csp-review/s0-20261022/` (called R below). Receipts are in
|
||||
`R/receipts/`.
|
||||
- Nothing was compiled. No Console code, policy or live stack was touched. The harness is a
|
||||
loopback-only stand-in server with fixture data, and the browser had no external network use:
|
||||
every request in every run went to 127.0.0.1.
|
||||
|
||||
## What was measured
|
||||
|
||||
The unmodified WordCraft 0.9.0 web build, framed same-origin under a stand-in shell that sends
|
||||
the Console shell CSP exactly. Three candidate frame policies differ only in `style-src`.
|
||||
|
||||
### Artifact
|
||||
|
||||
Copied from Louie's build (origin/louie/wordcraft-eval 880ef19e0); each hash matches Louie's receipt.
|
||||
|
||||
| File | SHA-256 |
|
||||
|---|---|
|
||||
| index.html (not served; reference only) | `361c9ec8eda95afa7483fef1f6e95270a199cfe59591285096bf22cb06bfef43` |
|
||||
| wordcraft-fa59e09a3f921e03.ico | `53a23dcf66260f44d87d4d143d633395f3e967f391eda47e6e9cc3a52f820225` |
|
||||
| wordcraft-web-88f7089f25a3b338.js | `866a1de6a33eff2ded8ae9588fd049aeacfc42557a3863ccd2107b3faf4d691e` |
|
||||
| wordcraft-web-9bda74d1c7bc6623_bg.wasm | `90dec375924b47b40054eb4ecafb4235b6ed0bd63880b91ec138fdbb6f939e75` |
|
||||
|
||||
### Wrapper and harness (mine, scratch only)
|
||||
|
||||
| File | SHA-256 |
|
||||
|---|---|
|
||||
| harness/www/frame.html | `6a8d141fa8b073cff73fa666fdf030a9f91030924e74a6aa0a441d76a5583f82` |
|
||||
| harness/www/frame-bootstrap.js | `32c8f71acfe14f3bdc4d31534ed3a84609fc4fb1fb8a1a4a96f56eb28f39d3d4` |
|
||||
| harness/www/frame.css | `629eb32e9cdf6b97f0ce79f841419ab4a80d42f322c10948ec5d23198799111e` |
|
||||
| harness/www/shell.html | `cf94f1ce359e8b90f86d39cdef98894d7874e97da9cf7eb0808b0f6e038b3bac` |
|
||||
| harness/www/shell.css | `372716168beb22dd9fbc7e3d45a76bc06c1b3b1d8f59807a187700689d9ff38c` |
|
||||
| harness/serve.py | `ad34aed5cdf49a77128778f5f6523698d35f5a6eca771e1dbe53941f09fabe80` |
|
||||
| harness/instrument.js | `028e914a994a518043dab0b834a9917fc67bc35730f9e3725b59ebd8fa8fb0fa` |
|
||||
| harness/fixtures/s0-full-feature.docx (9985 B) | `b40eb5875518701d10de1a5419931f02e10c378560fbd5ab6a2ed68d19be53f4` |
|
||||
|
||||
The frame wrapper loads the wasm-bindgen module from `frame-bootstrap.js`; it has no inline
|
||||
script or inline style. The fixture is generated by `harness/make_fixture.py`. It carries
|
||||
two sections (portrait, then landscape), a header and footer, headings, a table, a small PNG, an
|
||||
external hyperlink to `https://example.invalid/` and wikilinks. It has no embedded font and no
|
||||
real writing.
|
||||
|
||||
Browser: Chrome 150.0.0.0, Linux x86_64, driven through DevTools MCP isolated contexts. WebGPU
|
||||
reported "No available adapters", so wgpu ran on its GL backend.
|
||||
|
||||
## Headers
|
||||
|
||||
Shell (stand-in for `serve.mjs:605`, unchanged, no SSO), sent on `/shell.html`, `/shell.css`,
|
||||
assets, fixtures and every 404/405:
|
||||
|
||||
```
|
||||
default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; font-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'
|
||||
```
|
||||
|
||||
Frame, sent only on `/editor/wordcraft/frame`:
|
||||
|
||||
```
|
||||
default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src <V>; connect-src 'self'; font-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'; frame-src 'none'; form-action 'none'
|
||||
```
|
||||
|
||||
| Variant | `<V>` |
|
||||
|---|---|
|
||||
| style-self | `'self'` |
|
||||
| style-hash | `'self' 'sha256-9tSqG2Th9gNn6sbn1ZYMSqakJLznxnsamRDeigez2Mo='` |
|
||||
| style-inline | `'self' 'unsafe-inline'` (defined, not run: the first two answered the question) |
|
||||
|
||||
Every response also carries `x-content-type-options: nosniff`, `referrer-policy: no-referrer`
|
||||
and `cache-control: no-store`. Routes are fixed (`/shell.html`, `/shell.css`,
|
||||
`/editor/wordcraft/frame`, five allowlisted asset names, `/fixtures/<name>`). Anything else is a
|
||||
404, and every non-GET method is a 405. Each request and its exact headers are logged to
|
||||
`requests-<variant>.jsonl`.
|
||||
|
||||
Note on the shell header: the stand-in shell sends `frame-ancestors 'none'` as Console does.
|
||||
The frame is framed by the shell, not the other way round, so this does not affect the
|
||||
measurement. The real adapter's frame route sends the frame header above.
|
||||
|
||||
## Results
|
||||
|
||||
| Run | Port | CSP violations | Functions | Receipts |
|
||||
|---|---|---|---|---|
|
||||
| style-self full pass | 7391 | **1**: `style-src-elem`, blocked `inline`, js line 93 | all | `full-style-self-events.json`, `requests-style-self.jsonl` |
|
||||
| style-hash clean pass | 7392 | **0** | all | `clean-style-hash-events.json`, `requests-style-hash.jsonl` |
|
||||
| frame navigation test | 7392 | 1 in the shell, intended: `frame-src`, enforce | n/a | `browser-console-network.txt` |
|
||||
| Print probe | 7392 | 1, intended: `frame-src`, blocked `blob` | Print blocked | `print-probe.json` |
|
||||
|
||||
Each full pass made exactly 8 requests: the 8 routes, all 200, and the fixture fetch is one of
|
||||
them. That the client was 127.0.0.1 rests on the server binding 127.0.0.1 only (`serve.py`); the
|
||||
request log does not record the peer address. The browser's network list showed no request to
|
||||
any other origin.
|
||||
|
||||
The one style-self violation fired between the file-picker click and the fixture feed. Source:
|
||||
rfd 0.17.2 `src/backend/wasm.rs:127-129` appends its `wasm/style.css` as a `<style>` element. Its
|
||||
SHA-256 is the hash in the style-hash variant, and the console message names the same hash.
|
||||
Nothing else in the app adds inline style. eframe and egui write styles through CSSOM, which
|
||||
`style-src` does not govern. Blocking the `<style>` did not stop any function: the picker still
|
||||
opened and the fixture loaded.
|
||||
|
||||
### Function coverage
|
||||
|
||||
Every function in both full passes ran on the same fixture. Event logs record each one.
|
||||
|
||||
| Function | Evidence (both passes) |
|
||||
|---|---|
|
||||
| Load and start | wgpu GL backend, event handlers installed |
|
||||
| Open (Browse) | `input[type=file]` click, accept `.docx,.docm,.dotx,.dotm,.doc,.dot,.odt,.rtf,.txt,.md,.html,.htm,.tex,.json,.xml`; fixture fed, title changes to `s0-full-feature` |
|
||||
| Edit | text inserted and the title gains the dirty mark (screenshots `clean-02`, `selffull-03`) |
|
||||
| Resize | 1024x700 (style-self also 1280x800 then back). style-self has a `step` event; the style-hash resize has no event record, only screenshot `clean-03-resized.png` and the session transcript |
|
||||
| Hyperlink ctrl+click | `window.open("https://example.invalid/", "_blank")` |
|
||||
| Discord title button | `window.open("https://discord.gg/artcraft", "_blank")` |
|
||||
| Save As (.docx) | `showSaveFilePicker`, then `createObjectURL` + anchor download, 10382 / 10386 B |
|
||||
| Export PDF | same path, 29014 / 29085 B |
|
||||
| Export HTML | same path, 1678 / 1689 B |
|
||||
| Export PNG | same path, 126467 / 128310 B |
|
||||
| About links | three `window.open(..., "_self")`: getartcraft.com, discord.gg, github.com |
|
||||
| Sidebar community | `window.open("https://discord.gg/artcraft", "_blank")` |
|
||||
|
||||
Byte counts differ between runs because the typed edit text differs.
|
||||
|
||||
Print is not counted in the zero-violation result. D7 removes WordCraft's hidden blob PDF
|
||||
iframe (`web.rs:548-604`) with no blob-frame exception. The probe confirms that path creates
|
||||
`iframe#wordcraft-print-frame` with a `blob:` PDF and that `frame-src 'none'` blocks it. Print
|
||||
under the adapter is host-owned and must be measured in W2.
|
||||
|
||||
Storage after the style-hash pass: `localStorage["wordcraft.ui"]` (1958 chars, including the
|
||||
recent file list and author), sessionStorage empty, no cookies. An earlier run also showed the
|
||||
IndexedDB `wordcraft` font store. See finding F9 in the security review.
|
||||
|
||||
### Frame navigation
|
||||
|
||||
After the style-hash pass the frame ran `location.href = "http://127.0.0.1:7391/shell.html"`,
|
||||
another loopback origin. The shell CSP blocked it: `frame-src` falling back to `default-src
|
||||
'self'`, disposition enforce. No request reached port 7391. The only new requests were three
|
||||
`data:image/png` loads from Chrome's own blocked-frame error page. Same-origin navigation is not
|
||||
blocked by CSP. That case needs the sandbox and host link gate (mitigation M7).
|
||||
|
||||
## Harness changes, disclosed
|
||||
|
||||
The harness changes behaviour in these places. Each one stands in for a host or OS
|
||||
surface the scratch harness cannot provide.
|
||||
|
||||
1. `instrument.js` is injected into the frame by DevTools after load. It is never served. It
|
||||
replaces `window.open` (records, returns null), `showSaveFilePicker` (records, rejects with
|
||||
SecurityError so WordCraft's anchor fallback runs), file-input `click` (feeds the fixture from
|
||||
`/fixtures/`), anchor `click` (records, drops the download) and wraps `URL.createObjectURL`.
|
||||
A byte-identical copy is `receipts/instrument-as-injected.js`.
|
||||
2. The `showSaveFilePicker` stub is an async function from the shell realm. WordCraft's
|
||||
same-realm Promise check rejects it, which logs `web.rs:405: showSaveFilePicker failed: not a
|
||||
promise` and an unhandled `SecurityError: S0 harness stub`. Both are harness artefacts. The
|
||||
fallback path that ran is the one a picker failure or refusal takes.
|
||||
3. DevTools keyboard typing does not reach egui. The edit was a synthetic
|
||||
`InputEvent('input', {inputType: 'insertText'})` on eframe's hidden text-agent `<input>`. It
|
||||
exercises the same egui text path. It is not real keyboard input.
|
||||
4. Clicks are synthetic pointer and mouse events on the canvas (`__tap`).
|
||||
5. style-hash attempt 1 recorded one `connect-src` violation from my own instrumentation: its
|
||||
anchor stub fetched the `blob:` URL to measure it. The violation's source was DevTools
|
||||
(`pptr`), not WordCraft. I removed the fetch and reran clean. Attempt 1's log is kept as
|
||||
`requests-style-hash-attempt1.jsonl`.
|
||||
6. style-self attempt 1 ran on an earlier serve.py that predates the current hash and covered
|
||||
load and open only. Its log is kept as `requests-style-self-attempt1.jsonl` and
|
||||
`server-style-self-attempt1.log`. The style-self result above is the full pass on
|
||||
serve.py `ad34aed5`.
|
||||
7. The Print probe's event excerpt was copied from the session transcript because it was not
|
||||
saved to a file at the time (`print-probe.json` says so).
|
||||
|
||||
## Recommendation for D8
|
||||
|
||||
1. The final frame header uses `style-src 'self'`, with no hash, nonce or `'unsafe-inline'`.
|
||||
The rest is as measured above.
|
||||
2. D7 already requires host-owned Open/import, which replaces `Services.open_async`
|
||||
(`web.rs:216-224`). That is the only web caller of rfd, so the measured violation goes away
|
||||
with it. Mitigation M5 makes this a gate.
|
||||
3. W2 re-measures the adapter build under `style-src 'self'` and must record 0 violations with
|
||||
full function coverage, including host-owned Print.
|
||||
|
||||
**Ruled by Mos (mos-1363, 2026-10-23T00:05Z).** On the unmodified artifact, `style-src 'self'`
|
||||
is not violation-free, and the measured fallback was the rfd hash above. Mos ruled that
|
||||
`style-src` stays `'self'`; the hash is not approved and not kept open. W2 must re-measure 0
|
||||
violations with full function coverage, including host-owned Print. If W2 cannot remove rfd
|
||||
from the web path, the change goes back to Mos.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,253 @@
|
||||
# Q195 O2 S0: WordCraft source security review
|
||||
|
||||
Status: COMPLETE. Verdict: **CONDITIONAL PASS**. The baseline source is acceptable for the
|
||||
O2 adapter only with mandatory mitigations M1-M14 adopted as gates. Amended after Filbert's
|
||||
review (#1798 comment 30914, A1 and A2). Mos ruled the D8 question in mos-1363 (see "Verdict").
|
||||
|
||||
- Review id: `s0-20261022`. Record: Gitea #1798. Owner: Fenton. Reviewer: Filbert.
|
||||
- Spec: `docs/plans/2026-10-21_q195-writing-editor.md` blob `322ec50f` at e36bce352: D6-D9 and
|
||||
the "S0 independent WordCraft source security review" section.
|
||||
- Source: WordCraft `e05b7fe86f3b85454000dffc321de638d7009817` (0.9.0), exported with
|
||||
`git archive` into the review dir. `Cargo.lock` SHA-256
|
||||
`06b745e43c13df3df7f048fa6a1981625ce7a5fe4cd484bc6254d0913d865b0b`, byte-identical to the pin.
|
||||
- Artifact: Louie's web build from origin/louie/wordcraft-eval 880ef19e0, copied into the review
|
||||
dir, with hashes checked against Louie's receipt. The hashes are listed in the CSP report.
|
||||
- Companion files: `2026-10-22_q195-wordcraft-security.json` (inventory, findings,
|
||||
mitigations, per-package notes) and `2026-10-22_q195-editor-csp.md` (browser measurement).
|
||||
- Independence: I did not author any WordCraft import, patch or browser adapter. I reviewed the
|
||||
O2 design (fenton-0004 to fenton-0008). That review shaped the wording of D6-D9, not the code.
|
||||
|
||||
## Method
|
||||
|
||||
Evidence labels:
|
||||
|
||||
- **[source]**: I read it in the pinned source.
|
||||
- **[browser]**: measured in the scratch harness (Chrome 150, Linux).
|
||||
- **[agent-reported]**: found by a read-only subagent; I did not re-read the cited lines unless
|
||||
the text says so.
|
||||
|
||||
Nothing was compiled, and no code from the tree was run. Browser runs used the copied
|
||||
prebuilt artifact.
|
||||
|
||||
1. Ran `cargo fetch --locked --target wasm32-unknown-unknown` into an isolated `CARGO_HOME`.
|
||||
Cargo checks every crates.io archive against its `Cargo.lock` checksum.
|
||||
2. Ran `cargo tree -p wordcraft-web --target wasm32-unknown-unknown --locked` with
|
||||
`--edges normal` and with `--edges normal,build`, then `cargo metadata --filter-platform`.
|
||||
3. `harness/inventory.py` joined these into one record per package: lock checksum, declared
|
||||
license, license and notice files, build script, proc-macro flag, and counts of capability
|
||||
markers. A marker count is a lead for reading, not a finding.
|
||||
4. Read the D7 surfaces in all 15 local crates myself, plus the parts of `wuff`, `rfd`,
|
||||
`eframe` and `epaint_default_fonts` named below.
|
||||
5. Four read-only subagents traced the same ground in parallel, split as ui-egui and web,
|
||||
document parsers, fonts/zotero/rest, and third-party hotspots. I re-read every finding that
|
||||
sets a mitigation's scope (F1, F2, F3, the spin cap in F4, F5's package parts, F6-F14). The
|
||||
rest stay labelled [agent-reported].
|
||||
6. Did the license pass from crate files, the craft-libs git checkouts, and a string scan of the
|
||||
built wasm for embedded fonts and data.
|
||||
7. Browser measurement: see the CSP report.
|
||||
|
||||
## Inventory (complete)
|
||||
|
||||
| Set | Count |
|
||||
|---|---|
|
||||
| Local WordCraft crates in the web closure | 15 |
|
||||
| Third-party packages, normal edges (D6's 199) | 199 |
|
||||
| Third-party build-only packages (build-dependencies) | 10 |
|
||||
| Third-party packages with a build script | 25 |
|
||||
| Third-party proc-macro crates | 10 |
|
||||
|
||||
The local crates are wordcraft-doc, -docbin, -docx, -engine, -fonts, -formats, -geom, -layout,
|
||||
-metafile, -pdf, -proof, -render, -ui-egui, -web and -zotero. This matches D6. `control-key` and
|
||||
`mcp` exist in the source tree but are not in the web closure.
|
||||
|
||||
Three packages come from git rather than crates.io and have no `Cargo.lock` checksum:
|
||||
`craft-pdf` and `krilla` at craft-libs `c3487320` (krilla lives at `vendor/krilla`), and
|
||||
`craft-ui` at craft-libs `d27646a5`. Their pin is the git commit id.
|
||||
|
||||
## Licenses and notices
|
||||
|
||||
Declared licenses for the 209 third-party packages (199 normal plus 10 build-only):
|
||||
|
||||
- 116 `MIT OR Apache-2.0` and 25 `Apache-2.0 OR MIT`.
|
||||
- 23 `MIT`.
|
||||
- The remainder are other permissive forms: Zlib, BSD-2/3, 0BSD and Unlicense options.
|
||||
- Unicode-3.0, alone or combined: unicode-ident, yoke, yoke-derive, zerofrom, zerofrom-derive.
|
||||
- Apache-2.0 only: codespan-reporting, unicode-general-category, unicode-linebreak, zopfli.
|
||||
- Two dual licenses need an election (L4):
|
||||
- `ryu`: Apache-2.0 OR BSL-1.0.
|
||||
- `self_cell`: Apache-2.0 OR GPL-2.0-only.
|
||||
- `encoding_rs`: (Apache-2.0 OR MIT) AND BSD-3-Clause, with a WHATWG license file.
|
||||
- `epaint_default_fonts`: (MIT OR Apache-2.0) AND OFL-1.1 AND Ubuntu-font-1.0.
|
||||
|
||||
No copyleft license is forced. The only GPL option is avoided by electing Apache-2.0.
|
||||
|
||||
Notice obligations:
|
||||
|
||||
- **L1** [source + artifact]: `epaint_default_fonts` is enabled, and the wasm contains its
|
||||
emoji-icon-font.ttf (MIT), Hack-Regular.ttf (MIT plus the Bitstream Vera license),
|
||||
NotoEmoji-Regular.ttf (OFL-1.1) and Ubuntu-Light.ttf (UFL-1.0). WordCraft's NOTICE and
|
||||
ATTRIBUTION do not mention these fonts. The Mosaic notice bundle must ship `OFL.txt`,
|
||||
`UFL.txt`, `Hack-Regular.txt` and `emoji-icon-font-mit-license.txt` from the crate's `fonts/`.
|
||||
- **L2** [source + artifact]: WordCraft's own bundled fonts are Inter, JetBrains Mono, Source
|
||||
Sans 3 and Source Serif 4 (all OFL). It also bundles en-us, it, pl and pt hyphenation data and
|
||||
the en-extra and ru-extra spelling lists. WordCraft's NOTICE covers all of these, so it must
|
||||
ship. Its line "Rust dependencies… (MIT/Apache-2.0/BSD/ISC/Zlib)" leaves out Unicode-3.0, BSL
|
||||
and the font licenses, so it is not a complete notice for the build on its own.
|
||||
- **L3** [source]: also ship:
|
||||
- subsetter's NOTICE (ttf-parser, MIT).
|
||||
- The craft-libs `LICENSE-APACHE`, `LICENSE-MIT` and `NOTICE` at both revs, plus
|
||||
`licenses/pdf-writer` at `c3487320`.
|
||||
- encoding_rs's WHATWG text.
|
||||
- The Unicode-3.0 text.
|
||||
|
||||
Thirteen packages carry no license file in the crate. The bundle supplies the standard text
|
||||
of the declared license for each: accesskit, craft-pdf, craft-ui, ecolor, eframe, egui,
|
||||
egui-wgpu, emath, epaint, epaint_default_fonts (font licenses are in `fonts/`), krilla,
|
||||
profiling and wuff. `cfg_aliases` has a NOTICES.md but is build-only, so nothing ships from it.
|
||||
- **L4**: Record the elections in the bundle: `self_cell` takes Apache-2.0, and `ryu` takes
|
||||
Apache-2.0.
|
||||
- **L5** [artifact]: The craft-fonts Japanese fonts are not in this artifact. A scan for "BIZ
|
||||
UDPGothic" and "Shippori" found 0 matches. If a later build enables them, their licenses join
|
||||
the bundle.
|
||||
|
||||
The license findings are resolved by M13, the notice bundle. None blocks.
|
||||
|
||||
## Build scripts and proc-macros
|
||||
|
||||
[agent-reported, file list source-verified from metadata]
|
||||
|
||||
- Build scripts (25): ahash, crc32fast, encoding_rs, getrandom, libm, naga, num-traits,
|
||||
parking_lot_core, proc-macro2, quote, rfd, rustversion, serde, serde_core, serde_json, slotmap,
|
||||
thiserror, unicode-general-category, wasm-bindgen, wasm-bindgen-shared, wgpu, wgpu-core,
|
||||
wgpu-hal, zerocopy, zmij.
|
||||
- Agent report: they probe the compiler or target and emit cfgs or generated tables. None
|
||||
uses the network.
|
||||
- `wasm-bindgen-shared` runs `git rev-parse HEAD`, which is non-hermetic (F15).
|
||||
- Proc-macros (10): bytemuck_derive, document-features, multiversion_no_op, rustversion,
|
||||
serde_derive, thiserror-impl, wasm-bindgen-macro, yoke-derive, zerocopy-derive,
|
||||
zerofrom-derive.
|
||||
- `document-features` reads its crate's `Cargo.toml`.
|
||||
- `rustversion` reads the compiler version and date.
|
||||
- None writes files or uses the network.
|
||||
- Build-only packages (10): autocfg, cfg_aliases, rustversion, serde_spanned, toml,
|
||||
toml_datetime, toml_parser, toml_writer, version_check, winnow.
|
||||
|
||||
## D7 trace
|
||||
|
||||
| D7 surface | What WordCraft does on web | Evidence | Disposition |
|
||||
|---|---|---|---|
|
||||
| Open/import | `Services.open_async` (web.rs:216-224) opens rfd's `<input type=file>` for documents, pictures, fonts and recipients. Accept list includes `.json`, which loads a native `Document` through serde (io.rs:202). | [source][browser] | F3, F8; M3, M5 |
|
||||
| Save/export | `showSaveFilePicker` (web.rs:398-451), then a `createObjectURL` plus `a.download` fallback. Every save and export took this path. | [source][browser] | F8; M5 |
|
||||
| Fonts | Embedded DOCX fonts and Add Font decode through `wuff`. User fonts persist in IndexedDB (font_store.rs). `queryLocalFonts` is called (web.rs:274-301). | [source][browser] | F1, F9, F10; M1, M9, M10 |
|
||||
| Storage | localStorage `wordcraft.ui` (prefs.rs) holds the recent list, author, dictionary, autocorrect and UI keys. No cookies, and sessionStorage is empty. | [source][browser] | F9; M9 |
|
||||
| Clipboard | Document-level paste listener, added once and never removed (web.rs ~203). | [source] | F11; M11 |
|
||||
| Objects | vbaProject, attachedTemplate and OLE external relationships survive a round trip. Nothing executes them. | [source][agent-reported] | F5; M6 |
|
||||
| Hyperlinks | Targets are imported unfiltered (docx read/story.rs:485-496) and DOCX save writes them back unfiltered (docx write/story.rs:335-340). Ctrl+click is already gated: canvas.rs:2490-2501 follows a link only if `is_web_or_mail_link` (doc link.rs:7) allows it (http, https, mailto). The About links and Discord buttons call `window.open` with hard-coded targets. `show_discord` defaults to true (ui-egui lib.rs:376). | [source][browser] | F2, F6; M2, M7 |
|
||||
| Print/PDF | Hidden `iframe#wordcraft-print-frame` with a `blob:` PDF (web.rs:541-604). `frame-src 'none'` blocks it. | [source][browser] | F7; M8 |
|
||||
| MCP | The `mcp` crate is not in the web closure. | [source] | none |
|
||||
| Zotero | A protocol driver that takes a caller-supplied answer callback and has no transport of its own. Its UI actions are disabled on wasm32 (widgets.rs:183). | [source] | none |
|
||||
| Script and eval | No `eval`, `Function`, `Worker` or dynamic script anywhere in the closure. eframe and egui set styles through CSSOM. The only inline `<style>` comes from rfd. | [agent-reported][browser] | F13 |
|
||||
| Start error | `set_inner_html` with a formatted eframe error (web.rs:89). It is not derived from the document. | [source] | F12 |
|
||||
| URL flags | `?webgl` and `?sample` (apps/wordcraft-web/src/main.rs:10). | [source] | F14; M12 |
|
||||
|
||||
## Findings
|
||||
|
||||
Severity is for the O2 deployment: same-origin frame, single trusted user, documents they
|
||||
choose. The full text of each finding is in the companion `.json`.
|
||||
|
||||
| Id | Severity | Evidence | Finding |
|
||||
|---|---|---|---|
|
||||
| F1 | medium | source | `wuff` 0.2.9 trusts the WOFF1 `orig_length`: `decompress_woff1.rs:96` passes it to `decompress_z`, which runs `Vec::with_capacity` at `:15`. The only check is `woff_length < orig_length`. A DOCX with an embedded WOFF reaches this on open: io.rs:164-182 → docx fonts.rs:30 → fonts user.rs:143-163 → wuff. The input cap is 50 MiB; the 128 MiB decoded cap is checked only after decoding. `catch_unwind` (user.rs:166) does nothing on wasm32-unknown-unknown (panic=abort), and allocation failure aborts anyway. Result: the wasm instance traps and unsaved edits are lost. Not reproduced in the browser. |
|
||||
| F2 | low | source | DOCX hyperlink targets (`javascript:`, `data:`, `file:`, custom schemes) are kept on import and written back unfiltered on DOCX save (write/story.rs:335-340), so a saved file carries them to a desktop Office user. Activation inside WordCraft is already limited to http, https and mailto (canvas.rs:2490-2501, link.rs:7). `is_safe_href` filters the markdown, latex, html, ODT (odt.rs:399) and RTF (rtf.rs:261) exporters, not DOCX. Corrected after Filbert's A2: this row first said activation was ungated and rated it medium. |
|
||||
| F3 | medium | source + browser | `.json` opens as a native serde `Document` and skips the package caps. The web picker accepts it. |
|
||||
| F4 | medium | agent-reported, spin cap source-verified | Import exhausts resources on the main thread: encrypted DOCX spin count up to 10,000,000 (crypt.rs:31); no ODT package total; CFB up to 1 GiB; image decode limits 512 MiB; zip has no ratio limit of its own. The frame shares the shell's renderer process. |
|
||||
| F5 | low | source + agent-reported | vbaProject (package.rs:40/83/87), attachedTemplate (:27) and OLE external relationships are passed through on save. |
|
||||
| F6 | medium | browser + source | Outbound `window.open` from the hyperlink, Discord, the three About links and the sidebar community link. A cross-origin self-navigation of the frame was blocked by the shell CSP. |
|
||||
| F7 | low | source + browser | The Print blob iframe is blocked by `frame-src 'none'` and must be removed. |
|
||||
| F8 | medium | source + browser | Open and Save are browser-owned: rfd's input and the FS Access picker with an anchor-download fallback. |
|
||||
| F9 | low | source + browser | localStorage holds document-derived data (the recent list, the author); fonts persist in IndexedDB. |
|
||||
| F10 | low | source | `queryLocalFonts` is called. |
|
||||
| F11 | low | source | An unconditional paste listener feeds clipboard content to the importer. |
|
||||
| F12 | low | source | `innerHTML` is used for the start error; not derived from the document. |
|
||||
| F13 | info | source + browser | rfd injects one inline `<style>`, the only `style-src 'self'` violation. |
|
||||
| F14 | low | source | The `?sample` and `?webgl` URL flags. |
|
||||
| F15 | low | agent-reported | Non-hermetic build inputs; no build script or proc-macro uses the network. |
|
||||
| L1-L5 | license | source + artifact | See "Licenses and notices". |
|
||||
|
||||
Filbert's A1 and A2 (#1798 comment 30914) amended F2, M1 and M2; the rows above are the amended text.
|
||||
|
||||
No finding blocks on its own. Each medium is closed by a mandatory mitigation with a rejection
|
||||
test. Two things would turn F1 into a blocker: the adapter failing to gate font decode size, or
|
||||
the corpus in M14 finding a trap that M1 does not cover.
|
||||
|
||||
## Mandatory mitigations
|
||||
|
||||
Each mitigation is a gate on the named W brief. Its rejection test must exist and fail on the
|
||||
unmodified baseline before the mitigation lands.
|
||||
|
||||
| Id | Closes | Requirement | Rejection test |
|
||||
|---|---|---|---|
|
||||
| M1 | F1 | Before any decode, refuse a font whose declared sizes exceed `MAX_DECODED_BYTES`: each WOFF1 table `origLength`, the WOFF1 sum of `origLength`, and the WOFF2 `totalSfntSize` and brotli `uncompressed_size`. Applies to embedded fonts and Add Font alike. | Each case, embedded in a DOCX and through Add Font, is refused before decode with the editor responsive: (a) WOFF1 table `origLength` 0xFFFFFFFF; (b) WOFF1 tables each under the cap whose `origLength` sum exceeds it; (c) WOFF2 with a huge `totalSfntSize` and brotli `uncompressed_size`. (a) and (b) fail on the baseline. (c) is a regression case: wuff already bails above 128 MiB and a ratio of 100 (decompress_woff2.rs:23,27,85-92), though brotli.rs:60 still allocates up to that bound. |
|
||||
| M2 | F2 | Host-saved DOCX output carries only http, https and mailto external hyperlink targets; any other target is dropped or kept as inert text. Activation keeps WordCraft's existing `is_web_or_mail_link` gate, and an https activation stays inside the M7 sandbox. | A DOCX with `javascript:`, `data:`, `file:` and `vbscript:` hyperlinks, saved through host Save, has none of those targets in its relationships; this fails on the baseline. Regression: ctrl+click on each makes no `window.open` call, and ctrl+click on an https link opens no browsing context under the M7 sandbox. |
|
||||
| M3 | F3 | The host import allowlist excludes `.json` and any other native format. | A `.json` file sent through host Open is refused before WordCraft sees it. |
|
||||
| M4 | F4 | Host caps before handoff: file size, zip entry count and uncompressed total. Encrypted documents are refused, or their spin count is capped low. An import watchdog tears down the frame on a stall. | A zip bomb, an oversize ODT, an encrypted DOCX with spinCount 10,000,000 and an oversize PNG are each refused or aborted, and the shell stays responsive. |
|
||||
| M5 | F8, F13 | Host-owned Open, Save and Export replace `Services.open_async`, `showSaveFilePicker` and the anchor download. rfd is never called on web. | A full pass under `style-src 'self'` records 0 violations, no file-input click, no `showSaveFilePicker` call and no `a.download`. |
|
||||
| M6 | F5 | Strip vbaProject, attachedTemplate and OLE external relationships on save, or refuse `.docm` and `.dotm`. | A `.docm` round-trips with neither part present. |
|
||||
| M7 | F6, F2 | Frame sandbox without `allow-popups` or `allow-top-navigation`. `show_discord` forced false. The About and community links are removed or go through the host link gate. | None of the six measured `window.open` triggers creates a browsing context or sends an off-origin request. |
|
||||
| M8 | F7 | Remove the blob print iframe. Print goes through the host, and `frame-src 'none'` stays. | Print creates no iframe and records no `frame-src` violation. |
|
||||
| M9 | F9 | No document-derived data goes into origin storage. The dictionary, autocorrect list and fonts are host-owned, or kept in a partition the host clears. | After open, edit and close, no fixture file name, author or user word remains in localStorage or IndexedDB. |
|
||||
| M10 | F10 | Disable Local Font Access: `Permissions-Policy: local-fonts=()` on the frame response, or remove the call. | `queryLocalFonts` is missing or rejects, and no prompt appears. |
|
||||
| M11 | F11 | Pasted content goes through M4's caps. | Pasting an oversize image or deeply nested HTML is refused, and the editor stays responsive. |
|
||||
| M12 | F14 | The host serves a fixed frame URL and ignores or refuses query strings. | `?sample` and `?webgl` change nothing. |
|
||||
| M13 | L1-L3 | Mosaic notice bundle as listed under "Licenses and notices", with the L4 elections. | A bundle check maps every inventory package and every font embedded in the wasm to a shipped license text. |
|
||||
| M14 | residuals | The W6 malformed-input corpus covers deep nesting, panic sites and the PDF writer. | Every corpus input opens or is refused, with no trap and no hang past the watchdog. |
|
||||
|
||||
## Residual risks (not cleared by S0)
|
||||
|
||||
1. **Recursion depth against the wasm stack.** The DOCX element-depth cap is 256
|
||||
[agent-reported]. I did not show that every recursive walk (layout, render, the serde
|
||||
`Document` path) fits in the default wasm stack at that depth. M14 covers this.
|
||||
2. **Panics.** On wasm32-unknown-unknown every panic traps the instance. S0 did not audit
|
||||
every `unwrap`, index and arithmetic overflow in the importers. M14 is the empirical
|
||||
backstop, and M4's watchdog limits the damage to the frame.
|
||||
3. **krilla and craft-pdf** were reviewed by marker scan only: their filesystem markers are in
|
||||
docs and tests (krilla lib.rs:42 and :135, util.rs:278). The PDF writer works on WordCraft's
|
||||
own model, not on raw input, which lowers the exposure.
|
||||
4. **Decoders** (`image`, `zune-jpeg`, `gif`, `png`) were trusted to their upstream limits
|
||||
beyond the 512 MiB cap noted in F4.
|
||||
5. **[agent-reported] items** that were not re-read: the DOCX cap values, ODT and CFB totals,
|
||||
the OLE paths, the image limits and the build-script behaviour. Filbert may spot-check any
|
||||
of them. Each cites file and line in the `.json`.
|
||||
|
||||
## Verdict
|
||||
|
||||
**CONDITIONAL PASS**, against the S0 conditions:
|
||||
|
||||
- Complete inventory: yes. 15 local crates, 199 normal, 10 build-only, 25 build scripts,
|
||||
10 proc-macros, and 3 git dependencies pinned by commit.
|
||||
- License findings resolved: yes, by M13 and the L4 elections. Nothing copyleft is forced.
|
||||
- No unresolved blocking security finding: yes, provided M1-M12 are accepted. With any of M1,
|
||||
M2, M3, M5 or M7 refused, S0 fails.
|
||||
- An accepted list of mandatory adapter mitigations and rejection tests: M1-M14 are proposed
|
||||
here. They need acceptance by Filbert's review and Mos.
|
||||
- Baseline meets the restrictions: yes. The measured frame header holds with every function
|
||||
working, and the only `style-src 'self'` violation sits on a path that D7 already removes.
|
||||
|
||||
**D8, ruled by Mos (mos-1363, 2026-10-23T00:05Z).** On the unmodified artifact, `style-src
|
||||
'self'` records one violation: rfd's inline `<style>`. I recommended the final header with
|
||||
`style-src 'self'` and no extra source, and returned rfd's hash
|
||||
(`'sha256-9tSqG2Th9gNn6sbn1ZYMSqakJLznxnsamRDeigez2Mo='`) as the D8 delta. Mos ruled:
|
||||
`style-src` stays `'self'`; the hash is not approved and not kept open. M5 gates removing rfd
|
||||
from the web path. W2 must re-measure 0 violations with full function coverage, including
|
||||
host-owned Print. If W2 cannot remove rfd, the change goes back to Mos.
|
||||
|
||||
## Process disclosures
|
||||
|
||||
- Four general-purpose subagents ran read-only traces of the scratch source copy. They were
|
||||
told not to compile, run code from the tree, use the network or write files. The
|
||||
document-parser agent reported one slip: it sent a loop's output to `/tmp/none`. The file was
|
||||
empty and the agent deleted it at once. Nothing else was written.
|
||||
- The harness changes made for the browser runs are disclosed in the CSP report, "Harness
|
||||
changes, disclosed".
|
||||
- I did not read any other seat's scratch apart from copying Louie's artifact, which Sage
|
||||
authorized (sage-3058). No credential was used or read.
|
||||
Reference in new issue
Block a user