reviews: Q195 S0 amendments A1/A2 from Filbert, D8 ruling (#1798)

M1 adds WOFF1 summed-table and WOFF2 declared-size refusals. F2 is
corrected: ctrl+click is already limited to http, https and mailto, and
the exposure is DOCX save writing targets back; M2 now targets
host-saved DOCX output. CSP report nits N1-N3 fixed. D8 recorded as
ruled by Mos (mos-1363): style-src 'self', rfd hash not approved.
This commit is contained in:
jason.woltje committed 2026-10-22 19:06:03 -05:00
1 parent 85ae2be773
commit 74ec320135
5 files changed
+48 -38

No files matched your search

+2
View File
@@ -5360,3 +5360,5 @@ Gander: after Duckworth-0100 design approval with C1-C5, prepared the two host-s
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: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-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.
+1
View File
@@ -1553,3 +1553,4 @@ are never rewritten or removed; corrections are new entries.
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: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: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-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.
+16 -13
View File
@@ -30,14 +30,14 @@ Copied from Louie's build (origin/louie/wordcraft-eval 880ef19e0); each hash mat
| File | SHA-256 | | File | SHA-256 |
|---|---| |---|---|
| www/frame.html | `6a8d141fa8b073cff73fa666fdf030a9f91030924e74a6aa0a441d76a5583f82` | | harness/www/frame.html | `6a8d141fa8b073cff73fa666fdf030a9f91030924e74a6aa0a441d76a5583f82` |
| www/frame-bootstrap.js | `32c8f71acfe14f3bdc4d31534ed3a84609fc4fb1fb8a1a4a96f56eb28f39d3d4` | | harness/www/frame-bootstrap.js | `32c8f71acfe14f3bdc4d31534ed3a84609fc4fb1fb8a1a4a96f56eb28f39d3d4` |
| www/frame.css | `629eb32e9cdf6b97f0ce79f841419ab4a80d42f322c10948ec5d23198799111e` | | harness/www/frame.css | `629eb32e9cdf6b97f0ce79f841419ab4a80d42f322c10948ec5d23198799111e` |
| www/shell.html | `cf94f1ce359e8b90f86d39cdef98894d7874e97da9cf7eb0808b0f6e038b3bac` | | harness/www/shell.html | `cf94f1ce359e8b90f86d39cdef98894d7874e97da9cf7eb0808b0f6e038b3bac` |
| www/shell.css | `372716168beb22dd9fbc7e3d45a76bc06c1b3b1d8f59807a187700689d9ff38c` | | harness/www/shell.css | `372716168beb22dd9fbc7e3d45a76bc06c1b3b1d8f59807a187700689d9ff38c` |
| harness/serve.py | `ad34aed5cdf49a77128778f5f6523698d35f5a6eca771e1dbe53941f09fabe80` | | harness/serve.py | `ad34aed5cdf49a77128778f5f6523698d35f5a6eca771e1dbe53941f09fabe80` |
| harness/instrument.js | `028e914a994a518043dab0b834a9917fc67bc35730f9e3725b59ebd8fa8fb0fa` | | harness/instrument.js | `028e914a994a518043dab0b834a9917fc67bc35730f9e3725b59ebd8fa8fb0fa` |
| fixtures/s0-full-feature.docx (9985 B) | `b40eb5875518701d10de1a5419931f02e10c378560fbd5ab6a2ed68d19be53f4` | | 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 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 script or inline style. The fixture is generated by `harness/make_fixture.py`. It carries
@@ -88,8 +88,10 @@ measurement. The real adapter's frame route sends the frame header above.
| frame navigation test | 7392 | 1 in the shell, intended: `frame-src`, enforce | n/a | `browser-console-network.txt` | | 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` | | 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 from 127.0.0.1, all 200. The fixture Each full pass made exactly 8 requests: the 8 routes, all 200, and the fixture fetch is one of
fetch is one of them. No other origin was contacted. 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: 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 rfd 0.17.2 `src/backend/wasm.rs:127-129` appends its `wasm/style.css` as a `<style>` element. Its
@@ -107,7 +109,7 @@ Every function in both full passes ran on the same fixture. Event logs record ea
| Load and start | wgpu GL backend, event handlers installed | | 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` | | 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`) | | Edit | text inserted and the title gains the dirty mark (screenshots `clean-02`, `selffull-03`) |
| Resize | 1024x700 (style-self also 1280x800 then back) | | 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")` | | Hyperlink ctrl+click | `window.open("https://example.invalid/", "_blank")` |
| Discord title button | `window.open("https://discord.gg/artcraft", "_blank")` | | Discord title button | `window.open("https://discord.gg/artcraft", "_blank")` |
| Save As (.docx) | `showSaveFilePicker`, then `createObjectURL` + anchor download, 10382 / 10386 B | | Save As (.docx) | `showSaveFilePicker`, then `createObjectURL` + anchor download, 10382 / 10386 B |
@@ -175,7 +177,8 @@ surface the scratch harness cannot provide.
3. W2 re-measures the adapter build under `style-src 'self'` and must record 0 violations with 3. W2 re-measures the adapter build under `style-src 'self'` and must record 0 violations with
full function coverage, including host-owned Print. full function coverage, including host-owned Print.
**Held for Mos.** On the unmodified artifact, `style-src 'self'` is not violation-free. The **Ruled by Mos (mos-1363, 2026-10-23T00:05Z).** On the unmodified artifact, `style-src 'self'`
measured fallback is the rfd hash above. D8 says any extra source is a new allowance, so I am is not violation-free, and the measured fallback was the rfd hash above. Mos ruled that
not recommending it. It is returned to Mos as the delta. If the adapter cannot remove rfd from `style-src` stays `'self'`; the hash is not approved and not kept open. W2 must re-measure 0
the web path, the choice between the hash and stopping the change is Mos's to make. violations with full function coverage, including host-owned Print. If W2 cannot remove rfd
from the web path, the change goes back to Mos.
@@ -23,7 +23,7 @@
"buildScripts": 25, "buildScripts": 25,
"procMacros": 10 "procMacros": 10
}, },
"verdict": "conditional-pass: baseline source acceptable only with mandatory mitigations M1-M14 adopted as S1/W gates; D8 style-src question returned to Mos (style-src 'self' measured 1 violation on the unmodified artifact, attributed to rfd and removed by M5)", "verdict": "conditional-pass: baseline source acceptable only with mandatory mitigations M1-M14 adopted as S1/W gates (amended per Filbert A1/A2); D8 ruled by Mos mos-1363: style-src stays 'self', rfd hash not approved, M5 removes rfd, W2 re-measures 0 violations including host-owned Print",
"findings": [ "findings": [
{ {
"id": "F1", "id": "F1",
@@ -31,15 +31,15 @@
"blocking": "mitigated-by M1", "blocking": "mitigated-by M1",
"evidence": "source", "evidence": "source",
"title": "WOFF1 table decompression trusts orig_length (allocation trap)", "title": "WOFF1 table decompression trusts orig_length (allocation trap)",
"detail": "wuff 0.2.9 decompress_woff1.rs:96 passes table.orig_length (u32 from file) to decompress_z, which calls Vec::with_capacity(size_hint) at :15. Only check is woff_length < orig_length. Reached on document open: engine/io.rs:164-182 open_bytes_in -> load_embedded_fonts -> docx fonts.rs:30 embedded_fonts (no fontKey: stored bytes as is) -> fonts user.rs:143-163 decode_font_file. MAX_FONT_FILE_BYTES 50 MiB caps input; MAX_DECODED_BYTES 128 MiB is checked after decode. user.rs:166 catch_unwind does not help on wasm32-unknown-unknown (panic=abort, no profile override) and allocation failure aborts regardless. Effect: wasm instance trap, unsaved edits lost. Also reachable from Add Font. Not reproduced in the browser." "detail": "wuff 0.2.9 decompress_woff1.rs:96 passes table.orig_length (u32 from file) to decompress_z, which calls Vec::with_capacity(size_hint) at :15. Only check is woff_length < orig_length. Reached on document open: engine/io.rs:164-182 open_bytes_in -> load_embedded_fonts -> docx fonts.rs:30 embedded_fonts (no fontKey: stored bytes as is) -> fonts user.rs:143-163 decode_font_file. MAX_FONT_FILE_BYTES 50 MiB caps input; MAX_DECODED_BYTES 128 MiB is checked after decode. user.rs:166 catch_unwind does not help on wasm32-unknown-unknown (panic=abort, no profile override) and allocation failure aborts regardless. Effect: wasm instance trap, unsaved edits lost. Also reachable from Add Font. Not reproduced in the browser. WOFF2 has its own bounds in wuff (uncompressed_size <= 128 MiB bail, compression ratio <= 100, capacity_hint clamped; decompress_woff2.rs:23,27,85-128), but brotli.rs:60 still allocates vec![0u8; expected_size] up to that bound before the post-decode cap."
}, },
{ {
"id": "F2", "id": "F2",
"severity": "medium", "severity": "low",
"blocking": "mitigated-by M2", "blocking": "mitigated-by M2",
"evidence": "source", "evidence": "source",
"title": "DOCX hyperlink targets are not scheme-filtered on import", "title": "DOCX hyperlink targets are not scheme-filtered on import or DOCX save",
"detail": "docx read/story.rs:485-496 keeps the relationship target as is (javascript:, data:, file:, custom schemes). is_safe_href exists but is used only by the latex/markdown/html exporters. Ctrl+click passes the target to window.open (browser: https://example.invalid/ _blank recorded). DOCX re-export writes it back." "detail": "docx read/story.rs:485-496 keeps any target (javascript:, data:, file:, custom schemes) and docx write/story.rs:335-340 writes it back as an external relationship unfiltered. Activation is already gated: ctrl+click (canvas.rs:2490-2501) calls is_followable_link -> link::is_web_or_mail_link (doc link.rs:7), which allows only http, https and mailto (corrected per Filbert A2; the review first said activation was ungated). is_safe_href filters the markdown, latex, html, odt (odt.rs:399) and rtf (rtf.rs:261) exporters, not DOCX. Exposure: a saved DOCX carries the target to a desktop Office user."
}, },
{ {
"id": "F3", "id": "F3",
@@ -71,7 +71,7 @@
"blocking": "mitigated-by M7", "blocking": "mitigated-by M7",
"evidence": "browser+source", "evidence": "browser+source",
"title": "Outbound navigation via window.open", "title": "Outbound navigation via window.open",
"detail": "Measured: hyperlink ctrl+click https://example.invalid/ _blank; Discord title button https://discord.gg/artcraft _blank (lib.rs:1683-1686, show_discord defaults true lib.rs:376); About links getartcraft.com, discord.gg, github.com _self; sidebar community _blank. Path: ui-egui lib.rs:2244-2245 ctx.open_url -> eframe web open_url. The frame navigating itself to a cross-origin shell URL was blocked by frame-src (shell CSP default-src fallback); nothing reached the target server." "detail": "Measured: hyperlink ctrl+click https://example.invalid/ _blank; Discord title button https://discord.gg/artcraft _blank (lib.rs:1683-1686, show_discord defaults true lib.rs:376); About links getartcraft.com, discord.gg, github.com _self; sidebar community _blank. Path: ui-egui lib.rs:2244-2245 ctx.open_url -> eframe web open_url. The frame navigating itself to a cross-origin shell URL was blocked by frame-src (shell CSP default-src fallback); nothing reached the target server. Document hyperlinks reach open_url only for http, https and mailto (see F2); the Discord and About targets are hard-coded."
}, },
{ {
"id": "F7", "id": "F7",
@@ -192,17 +192,16 @@
"for": [ "for": [
"F1" "F1"
], ],
"requirement": "Reject WOFF/WOFF2 whose declared decoded size (header totalSfntSize and each table origLength) sum exceeds MAX_DECODED_BYTES before any decode, in the adapter or a reviewed WordCraft patch; embedded fonts are subject to the same gate.", "requirement": "Before any decode, refuse a WOFF 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.",
"rejectionTest": "DOCX with an embedded WOFF1 whose table origLength is 0xFFFFFFFF opens with the font refused and the editor still responsive; same file through Add Font is refused." "rejectionTest": "Each of these, embedded in a DOCX and via 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 huge totalSfntSize and brotli uncompressed_size. (a) and (b) fail on the baseline; (c) is a regression case, since wuff bounds WOFF2 at 128 MiB (Filbert A1)."
}, },
{ {
"id": "M2", "id": "M2",
"for": [ "for": [
"F2", "F2"
"F6"
], ],
"requirement": "Hyperlink targets pass a host allowlist (https, http, mailto) at import and at activation; anything else is kept as inert text.", "requirement": "Host-saved DOCX output carries only http, https and mailto external hyperlink targets (others 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.",
"rejectionTest": "DOCX with javascript:, data:, file: and vbscript: hyperlinks: ctrl+click opens nothing and no window.open call is made." "rejectionTest": "DOCX with javascript:, data:, file: and vbscript: hyperlinks, saved through host Save: the output has no such relationship target (fails on the baseline). Regression: ctrl+click on each makes no window.open call; ctrl+click on an https link opens no browsing context under the M7 sandbox."
}, },
{ {
"id": "M3", "id": "M3",
@@ -240,7 +239,8 @@
{ {
"id": "M7", "id": "M7",
"for": [ "for": [
"F6" "F6",
"F2"
], ],
"requirement": "Frame sandbox without allow-popups and allow-top-navigation; show_discord forced false; About/community links removed or routed through the host link gate.", "requirement": "Frame sandbox without allow-popups and allow-top-navigation; show_discord forced false; About/community links removed or routed through the host link gate.",
"rejectionTest": "Each measured window.open trigger (hyperlink, Discord, About x3, sidebar community) produces no new browsing context and no request off-origin." "rejectionTest": "Each measured window.open trigger (hyperlink, Discord, About x3, sidebar community) produces no new browsing context and no request off-origin."
@@ -1,8 +1,8 @@
# Q195 O2 S0: WordCraft source security review # Q195 O2 S0: WordCraft source security review
Status: COMPLETE. Verdict: **CONDITIONAL PASS**. The baseline source is acceptable for the Status: COMPLETE. Verdict: **CONDITIONAL PASS**. The baseline source is acceptable for the
O2 adapter only with mandatory mitigations M1-M14 adopted as gates. One CSP question goes back O2 adapter only with mandatory mitigations M1-M14 adopted as gates. Amended after Filbert's
to Mos under D8 (see "Verdict"). 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. - 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 - Spec: `docs/plans/2026-10-21_q195-writing-editor.md` blob `322ec50f` at e36bce352: D6-D9 and
@@ -142,7 +142,7 @@ The license findings are resolved by M13, the notice bundle. None blocks.
| 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 | | 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 | | 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 | | 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). Ctrl+click, the About links and the Discord buttons all call `window.open`. `show_discord` defaults to true (ui-egui lib.rs:376). | [source][browser] | F2, F6; M2, M7 | | 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 | | 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 | | 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 | | 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 |
@@ -158,7 +158,7 @@ choose. The full text of each finding is in the companion `.json`.
| Id | Severity | Evidence | Finding | | 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. | | 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 | medium | source | DOCX hyperlink targets are kept unfiltered, including `javascript:`, `data:`, `file:` and custom schemes. `is_safe_href` exists but only the latex, markdown and html exporters use it. | | 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. | | 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. | | 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. | | F5 | low | source + agent-reported | vbaProject (package.rs:40/83/87), attachedTemplate (:27) and OLE external relationships are passed through on save. |
@@ -174,6 +174,8 @@ choose. The full text of each finding is in the companion `.json`.
| F15 | low | agent-reported | Non-hermetic build inputs; no build script or proc-macro uses the network. | | 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". | | 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 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 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. the corpus in M14 finding a trap that M1 does not cover.
@@ -185,13 +187,13 @@ unmodified baseline before the mitigation lands.
| Id | Closes | Requirement | Rejection test | | Id | Closes | Requirement | Rejection test |
|---|---|---|---| |---|---|---|---|
| M1 | F1 | Refuse a WOFF or WOFF2 whose declared decoded sizes exceed `MAX_DECODED_BYTES` before decoding, for embedded fonts and Add Font alike. | A DOCX embedding a WOFF1 with `origLength` 0xFFFFFFFF opens with the font refused and the editor responsive. Add Font refuses the same font. | | 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, F6 | Hyperlink targets pass a host allowlist (`https`, `http`, `mailto`) on import and on activation. Anything else stays as inert text. | `javascript:`, `data:`, `file:` and `vbscript:` links: ctrl+click makes no `window.open` call. | | 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. | | 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. | | 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`. | | 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. | | 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 | 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. | | 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. | | 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. | | 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. | | 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. |
@@ -231,11 +233,13 @@ unmodified baseline before the mitigation lands.
- Baseline meets the restrictions: yes. The measured frame header holds with every function - 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. working, and the only `style-src 'self'` violation sits on a path that D7 already removes.
**Held for Mos (D8).** On the unmodified artifact, `style-src 'self'` records one violation: **D8, ruled by Mos (mos-1363, 2026-10-23T00:05Z).** On the unmodified artifact, `style-src
rfd's inline `<style>`. My recommendation is the final header with `style-src 'self'` and no 'self'` records one violation: rfd's inline `<style>`. I recommended the final header with
extra source, with M5 removing rfd from the web path and W2 re-measuring to 0. The measured `style-src 'self'` and no extra source, and returned rfd's hash
fallback, rfd's hash `'sha256-9tSqG2Th9gNn6sbn1ZYMSqakJLznxnsamRDeigez2Mo='`, is a new allowance (`'sha256-9tSqG2Th9gNn6sbn1ZYMSqakJLznxnsamRDeigez2Mo='`) as the D8 delta. Mos ruled:
under D8. I am returning it to Mos as the delta and not recommending it. `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 ## Process disclosures