Canonical for threat-model and safety rules. Cite violations as "violates rule N in docs/security.md". Charter: docs/principles.md.
- Local-only, offline-first trust model. No outbound calls except the signed updater check and user-initiated
openUrllinks. The IPC surface is local-trust only. - Custom IPC commands replace
tauri-plugin-fsscope. File access is intentionally unscoped at the plugin layer and gated by command-level guards (size, binary, canonicalization). Every custom command enforces its own bounds. - Rendered content is structurally sanitized by default. Markdown renders through
rehype-rawpaired withrehype-sanitizeconfigured bysrc/components/viewers/markdown/sanitizeSchema.ts— the schema is the canonical XSS boundary (script/iframe/object/embed/form structurally dropped, on* handlers stripped, inlinestylenot allow-listed). AnydangerouslySetInnerHTMLoutside the markdown pipeline is paired with an explicit sanitizer or produces output from a library whose output is known-safe. - Atomic writes, never partial sidecars. Comment persistence uses temp-write + rename so a crash or watcher race never leaves a half-written
.review.yaml. The only acceptable failure is "no write". - Fail closed, log, continue. Command handlers return
Result<_, String>and log; React renders behindErrorBoundary; the Rust panic hook logs before propagating; promise rejections route to the log. A viewer never crashes to a blank window on malformed input.
- Every Rust command that opens a file enforces a 10 MB hard cap. The cap is a single module-level
const MAX_SIZE: usize = 10 * 1024 * 1024;shared by both readers. Bothread_text_file_innerandread_binary_file_innergo through a single private helperread_file_cappedthat pre-checksmetadata().len()on the open handle (fstat — TOCTOU-safe vs. path swap) before allocating, then pairs it with aFile::take(MAX_SIZE + 1)+ post-read length check so special files (/dev/zero, FIFOs) reportinglen() == 0cannot slip through. (commands/fs/read.rs:44-67definesread_file_capped::53-55fstat pre-check,:56-62boundedVec::with_capacity+take(MAX_SIZE+1)read,:63-65post-read length check.) read_text_filerejects binaries by scanning the first 512 bytes for NUL, and only succeeds on valid UTF-8. (commands/fs/read.rs:91-94NUL scan;:98-101UTF-8 decode — both inread_text_file_inner, afterread_file_cappedhas bounded the read.)- Size pre-check uses
file.metadata()on the open handle (fstat, not a second path lookup) and is paired withFile::take(MAX+1)+ post-read length check to defend against special files (/dev/zero, FIFOs, named pipes, network FS) reportinglen() == 0while streaming unbounded bytes. Mirrorscore/sidecar.rs::read_capped. The cap coversload_sidecarandpatch_comment; sidecar YAML further rejects anchors/aliases before parsing (see rule 8). read_dircanonicalizes the requested path and rejects any request whose canonical form differs from the canonicalized input. (commands/fs/dir.rs:43-55.)
- Sidecar writes are temp-file + atomic rename; a failed rename cleans up the temp file. (
core/sidecar.rs:91-101.) - Saving an empty comment list deletes the sidecar rather than writing an empty YAML. (
core/sidecar.rs:74-79.) - Sidecar loading prefers YAML over JSON and treats a missing file as
Ok(None), never an error. (core/sidecar.rs:42-62.) - Malformed YAML/JSON surfaces as a typed error (
SidecarError::YamlParse/JsonParse), not a panic. (core/sidecar.rs:45,57.) Sidecar YAML additionally rejects anchors (&name) and aliases (*name) before parsing — defense-in-depth against billion-laughs amplification past the 10 MB byte cap. The writer never emits anchors, so refusal is wholesale. (core/sidecar.rs::reject_yaml_anchors.) Regression tests on this parser boundary follow rule 28 indocs/test-strategy.md.
- The
get_launch_argshandler drains per-window pending args from theWindowRegistryso each window's launch args are consumed exactly once. (registry.rs::drain_args;commands/launch.rs::get_launch_args.) - Single-instance and macOS-open callbacks route args exclusively through
route_args_through_registry, which either focuses existing windows or creates new ones with args queued per-window. (lib.rs::route_args_through_registry.) - CLI argument parsing canonicalizes every path via
core::paths::canonicalize_no_verbatim(which strips Windows\\?\verbatim prefixes — see issue #89) and silently drops paths that fail. (commands/launch.rs:68-125; sidecar resolution undercore/paths.rs::resolve_sidecaradds stricter folder-root containment.) 11a. Every Rust→TS path crosses the IPC boundary in non-verbatim form. All canonicalization in commands, watcher, scanner, and CLI goes throughcore::paths::canonicalize_no_verbatim.std::fs::canonicalizeandPath::canonicalizemethod-form are forbidden in code that produces an IPC payload, persists a path, or compares against a frontend-supplied path — they leak the Windows\\?\verbatim prefix and desynchronise from the bare-form paths the frontend already holds (read_diroutput, dialog results, persistedroot/ tabs /recentItems). Producer-side fix only — renderers must not paper over a regression with defensive strips. Issue #89.
- Markdown rendering uses
rehype-raw(to admit inline HTML for GitHub-style<details>/<sub>/<kbd>etc.) ONLY when paired withrehype-sanitizeconfigured by the schema insrc/components/viewers/markdown/sanitizeSchema.ts. The schema is the canonical XSS boundary:script/iframe/object/embed/formare absent fromtagNames(and so dropped);on*event handler attributes are absent from per-tag and*attribute lists (and so stripped).styleis allow-listed onspanandmathonly as a narrow KaTeX exception (KaTeX emits inlinestyleattributes for math layout); a customrehype-katex-stylepreprocessor (src/components/viewers/markdown/rehype-katex-style.ts) walks the tree and STRIPSstylefrom anyspan/mathwhose className does not start withkatexAND whose nearest ancestor is not katex-classed, so raw HTML cannot smuggle styles through the KaTeX hole. Plugin order inMarkdownViewer.tsxMUST berehypeRaw→rehypeFootnotePrefix→rehypeKatex(lazy) →rehypeKatexStyle→rehypeSanitize(sanitizeSchema)→ downstream plugins so user HTML cannot piggy-back through later transforms. Footnote ids/hrefs are kept in sync via the defaultclobberPrefix: "user-content-";rehypeFootnotePrefixstrips a redundant prefix thatremark-gfmitself sometimes emits, so sanitize re-adds exactly one prefix. 12a. HTML-preview iframe sandbox is hard-locked toallow-same-originonly — never paired withallow-scripts.HtmlPreviewView(src/components/viewers/HtmlPreviewView.tsx) setssandbox="allow-same-origin"unconditionally: CSS and fonts render, no JavaScript runs, and the host can reach intocontentDocumentto install a click handler that routes anchors throughrouteLinkClick(src/lib/url-policy.ts). Combiningallow-same-originwithallow-scriptswould re-grant the iframe full host-origin access (DOM, storage, IPC) — defeating the sandbox entirely; the toggle does not exist. No script is ever spliced into the iframe'ssrcDocand nopostMessagebridge runs. - Markdown anchor clicks open only
http(s)URLs, blockingfile://,javascript:, etc. (MarkdownViewer.tsx:146-148.) - Local image
srcis piped throughconvertFileSrcso the WebView loads viaasset:, never rawfile://. (MarkdownViewer.tsx:302-309.) - Mermaid runs with
securityLevel: "strict". (mermaid-singleton.ts:56.) SourceView'sdangerouslySetInnerHTMLpayload comes only from Shiki output,escapeHtml, or search highlight built fromescapeHtml-segmented pieces. (SourceView.tsx:184-190;useSourceHighlighting.ts:8-10.)
-
The CSP disallows inline scripts,
object,frame-ancestors, and whitelistsasset:(and the Windowshttp://asset.localhostform — Tauri 2.x switched the WebView2 asset scheme fromhttps://tohttp://, see@tauri-apps/apiconvertFileSrcdocstring) forimg-srcandmedia-src.media-srcis required for inline<audio>/<video>HTML elements that markdown content may embed — the same chokepoint (convertAssetUrl) is used for<img>,<audio>, and<video>. Audio and video files themselves have no dedicated viewers; they route throughBinaryPlaceholder. Asset-protocol scope is narrowed at runtime, not declared globally.tauri.conf.jsonships a static seedassetProtocol.scope: ["/__mdownreview_seed__/__never__"]— a path-shaped placeholder that matches no real file (Tauri requires at least one entry to enable the protocol; an empty array would disable it). At window registration timecrate::window_scope::extend_window_scopecallsapp.asset_protocol_scope().allow_directory(...)to extend the runtime scope:ScopeGrant::Folderadds the canonical folder recursively,ScopeGrant::FilesParentsadds each unique file-parent directory non-recursively (siblings beyond the requested files MUST NOT become silently readable). Asset-scope grants are distinct from watcher-allowlist seeds (issue #359 / AC3):register_window_file(called on every user-initiated tab-open) seeds the watcher allowlist ONLY — opening a file does NOT grant access to embedded relative-path image siblings.extend_window_scope_files(called by the "Allow for this session" banner click and by CLI/single-instance forwarding) grants BOTH asset-scope AND watcher-seed viaScopeGrant::FilesParents. This split implements the banner-opt-in security model: a tab opens without granting access to sibling files until the user explicitly clicks the banner. Chokepoint sites:register_window_file(src-tauri/src/commands/window_register.rs) — watcher-only;extend_window_scope_files(src-tauri/src/commands/window_register.rs) — full grant;route_args_through_registryAddToWindowarm (src-tauri/src/launch_routing.rs) — extends an existing window viaScopeGrant::FilesParentsfor forwarded files (CLI / single-instance /RunEvent::Opened);route_args_to_window(src-tauri/src/launch_routing.rs, target-aware variant called bycommands::drag_drop::handle_dropped_paths) — same grant, applied for the drag-drop flow so dropped files / folders extend the dropped-on window's scope beforeopen-file-tab/args-receivedis emitted;reset_window_scope_for_test(test-only,#[cfg(debug_assertions)]-gated) — clearstree_watched_dirs[label],watched_paths[label], andextra_watched_dirs[label]for the calling window viacrate::window_scope::reset_window_scope; used bye2e/native/fixtures.tsbetween specs to prevent cross-spec state leak (asset-protocol scope is intentionally NOT cleared — it is additive in Tauri v2 with no public revoke; issue #366).open_file_via_test(test-only,#[cfg(debug_assertions)]-gated) — enqueues a single file as launch args and emitsargs-receivedto drive the renderer'suseLaunchArgsBootstrap→store.openFile→register_window_filechain end-to-end; does NOT extend asset-protocol scope (banner opt-in viaextend_window_scope_filesremains the asset-scope chokepoint); used bye2e/native/09-outside-file-open.spec.tsto mirror CLI / OS file-open without spawning a second binary instance (issue #369). EveryWindowRegistry::registersite routes through this single chokepoint — main-window setup, additional-window setup, single-instance / OS file-open / second-instance forwarding (route_args_through_registry), drag-drop (route_args_to_windowviacommands::drag_drop), the renderer'sregister_window_folderIPC, and the test-onlyset_root_via_testcommand.allow_directoryis additive; failures are LOGGED viatracing(targetwindow-scope) and never propagated, per the Reliable pillar — failure to extend a grant must not abort window registration. Watcher-allowlist three-slot stratification (issue #369). InsideWatcherStatethe allowlist is split by lifecycle into three peer slots —tree_watched_dirs[label],watched_paths[label], andextra_watched_dirs[label]— each with a single writer and a single mutation rule.tree_watched_dirscarries renderer-projected workspace dirs and uses REPLACE semantics, owned by theupdate_tree_watched_dirsIPC and seeded with the workspace root byregister_window_folder.watched_pathscarries per-tab canonical files plus their.review.yaml/.review.jsonsidecar variants and uses REBUILD semantics, owned byupdate_watched_filesand seeded by a newseed_window_file(label, canonical)writer thatregister_window_file_innerinvokes when a tab opens an outside-workspace file.extra_watched_dirscarries persistent per-window directory grants, uses ADDITIVE semantics, and is owned byseed_window_extra_dirs(label, dirs)— the only caller is theFilesParentsarm incrate::window_scope::extend_window_scope(the banner opt-in for inline images outside the workspace). All three slots are unioned on read byis_path_allowed(watched_pathsexact match →tree_watched_dirsstarts_with→extra_watched_dirsstarts_with) and byis_path_or_parent_allowed's parent-canonical fallback.remove_windowandreset_window_scopeclear all three per-window maps.mrsf_targetscontinues to exact-match againsttree_watched_dirsONLY (soextra_watched_dirsandwatched_pathscannot leak.mrsf.yamlevents across windows). This split closes the race documented in #369 — before the split,register_window_file_innerand theFilesParentsarm both seededtree_watched_dirsindirectly, which the renderer's periodicupdate_tree_watched_dirsREPLACE then clobbered; with each slot owned by exactly one writer, the REPLACE is non-interfering. It also closes a Medium-severity sibling-read regression: opening~/Downloads/note.mdno longer allowlists~/Downloads/secret.txt. (src-tauri/src/window_scope.rs,src-tauri/tauri.conf.json:18.) 17a. No inline<style>elements in shipped HTML. Extends rule 17.index.htmland any other HTML asset Tauri serves must NOT contain inline<style>elements. Reason: Tauri's codegen (tauri-utils::html::inject_nonce_token) adds aSTYLE_NONCE_TOKENplaceholder to every existing<style>element, which the runtime (tauri::manager::replace_csp_nonce) substitutes with a fresh nonce AND appends to thestyle-srcdirective in the response CSP. Per CSP Level 3 algorithm "Does a source list allow all inline behavior for type" (§7 in the current W3C Editor's Draft), the presence of any nonce or hash source instyle-srccauses user agents to ignore'unsafe-inline'for BOTH<style>elements ANDstyle=attributes — silently breaking Shiki token highlighting (MarkdownViewer,SourceView), KaTeX math layout, Mermaid SVG diagrams, and any Reactstyle={{...}}prop. The authoritative CSP (src-tauri/tauri.conf.jsonapp.security.csp) isstyle-src 'self' 'unsafe-inline'and remains so; we restore its effective behaviour by not triggering Tauri's nonce-injection path. FOUC mitigation (issue #265) is preserved by thehtml, body { background-color: var(--color-bg) }rule keyed on[data-theme]insrc/styles/app.css— it ships in the entry CSS chunk loaded via the render-blocking<link>Vite injects in<head>; even though Vite places the module script before the<link>in the builtdist/index.html, the stylesheet is still render-blocking and module scripts auto-defer until both DOM parse and stylesheet load complete, so first paint always uses the correct theme background. The inline<script>inindex.htmlthat synchronously sets[data-theme]fromlocalStorageis allowed: Tauri SHA-256-hashes inline scripts intoscript-srcand the script's own hash satisfies the directive. Regression tests:src/__tests__/index-html-no-inline-style.test.tsparses both the sourceindex.htmlAND the builtdist/index.html(when present) and asserts zero<style>elements in each. NOTE: the OS-paintedbackground_colorset onWebviewWindowBuilderis now resolved from persistedOnboardingState.theme(viacommands::config::resolve_window_bg) rather than the hardcodedWINDOW_BGconstant from PR #265. The capability ACL surface is unchanged —set_themeis a custom#[mdr_command]registered throughtauri_specta::collect_commands!, not a Tauri plugin permission, so no entries are added tosrc-tauri/capabilities/. Production-CSP enforcement is currently NOT regression-tested at the e2e layer — the native test harness loads fromdevUrlnotfrontendDist, so Tauri's runtime CSP path is not exercised; tracked as a Gap below. Rejected alternatives: (a)app.security.dangerousDisableAssetCspModification: ["style-src"]— the per-directive form is a Tauri-supported declarative opt-out and is functionally adequate, but it is less discoverable and less self-documenting than removing the trigger inindex.html: future maintainers readingtauri.conf.jsonwould not know why styling works without also reading rule 17a. Removing the trigger keeps Tauri's hardening on for any future asset that does contain a legitimate<style>. (b) Hand-authored Shiki palette CSS — duplicate source of truth that drifts on Shiki upgrades and conflicts with the Lean and Never-Increase-Engineering-Debt meta-principles. (c)@shikijs/transformers+ Constructable Stylesheets — sound architectural improvement but does not address the underlying nonce-injection trigger and leaves KaTeX / Mermaid still broken; tracked as a follow-up under the Gaps below. 17b. The system-locations DENY list applies to content-initiated loads only, not to user-initiated opens. The classifier incore::security::system_locationsreturnsTier::Systemfor sensitive prefixes (/etc/,~/.ssh/,C:\Users\<user>\AppData\…,C:\Windows\, UNC, etc.; the const tables insystem_locations.rsare the canonical audit surface). The user-intent override applies UNIFORMLY across everyTier::Systemsub-location and flavor (POSIX / Windows / UNC) — there is no per-prefix carve-out. A deliberate user gesture (file picker, CLI argv, OS double-click, drag-drop, tree click, banner "Allow once" click) is the highest-confidence signal we have; second-guessing it with sub-rules would be ad-hoc policy and brittle to maintain. Chokepoint asymmetry table:Chokepoint Initiator Classifies? Tier::SystemactionuseLinkRouter(renderer, consumingcommands::path_classify)content (markdown anchor click, HTML preview link) yes block + warn core::html_assets::resolve_local_assetscontent (HTML-preview <img src>/<link href>inlining)yes ( is_system_blocked)skip asset (preserve original tag) Asset-protocol scope ( <img>/<iframe>/<audio>/<video>loads at runtime)content (Tauri asset:// loader) no (relies on scope-narrowing) load only if asset_protocol_scopewas widened by a user-initiated chokepointcommands::window_register::register_window_fileuser (file picker, CLI argv, OS double-click, tree click, single-instance forward) yes (informational only — surfaces to renderer for read-only badge) accept commands::window_register::extend_window_scope_filesuser (banner "Allow once" click — sole renderer caller) yes (informational) accept commands::window_register::register_window_folderuser (folder picker) no (containment via watcher allowlist alone) accept commands::fs::update_tree_watched_dirsrenderer (tree expansion projecting open folders) no accept (containment via watcher allowlist) commands::fs::ensure_readablesystem (post-allowlist read defense-in-depth) no (trusts upstream user-intent gate) accept window_scope::extend_window_scope(direct call)user (CLI / single-instance / drag-drop / RunEvent::Openedvialaunch_routing)no accept (atomic asset-scope + watcher seed) Content-initiated chokepoints are where a hallucinating LLM or malicious document could smuggle the user toward credential stores via
[bait](../../Users/me/.ssh/id_rsa)— the threat model that motivated issue #338's tier-3 hard block. User-initiated chokepoints carry explicit user intent and override the content-policy DENY list. The integrity guards remain at every chokepoint (..-rejection, verbatim-rejection, relative-rejection viacanonicalize_no_verbatim); the asymmetry concerns only theTier::Systempolicy bit.register_window_filereturns thePathClassificationto the renderer so a "system-location" / "outside-workspace" badge can surface as informational metadata — the renderer renders it but never converts it into a refused open. Chokepoint-discipline tension with rule 1: rule 1 ofdocs/architecture.mdnames a single Rust function per IPC; rule 17b layers a per-surface policy divergence on top of that single chokepoint. Each chokepoint is still owned by one Rust function; only the policy bit differs. Rationale: a user opening a.mdfile under%LOCALAPPDATA%\<vendor>\artifacts\(e.g. AI-tool outputs) has expressed unambiguous intent that no content-policy heuristic can second-guess; the muscle-memory-phishing argument that justified tier-3 for content links does not apply to a deliberate file picker / CLI gesture. (src-tauri/src/commands/window_register.rs,src-tauri/src/commands/fs/mod.rs::ensure_readable,src-tauri/src/core/html_assets.rs::resolve_local_assets,src-tauri/src/core/security/system_locations.rs.) -
The window requests only the minimal Tauri capability set: log, dialog open, clipboard write-text, opener open-url, updater. (
capabilities/default.json:5-16.) -
The updater verifies payloads via the configured minisign public key. (
tauri.conf.json:55.) -
set_root_via_testcompiles out of release via#[cfg(debug_assertions)]. (commands/launch.rs:31-33.)
- The watcher watches parent directories (not individual files) to survive atomic-rename saves, and emits only for paths on the current watch list. (
watcher.rs:146-169, 80-102.) Debounce window: rule 4 indocs/performance.md. - Watcher bookkeeping stores both canonical and raw paths so deleted files (which cannot canonicalize) still match. (
watcher.rs:295-324.) - Closing a tab evicts that path from
lastSaveByPathso stale timestamps cannot suppress a later event. (store/index.ts:156-159.)
- Release builds forward only
warn/errorfrom WebViewconsole.*to the log. (lib.rs:75-77.) - Log retention is enforced by a pre-init
log-rotatorTauri plugin (src-tauri/src/log_rotation.rs) registered beforetauri-plugin-log: each app launch archives the previousmdownreview.logtomdownreview.<UTC stamp>.log, then prunes the log directory to at most 10mdownreview*.logfiles (active + archives combined, oldest mtime first). The active file is never deleted. Pruning usesfs::remove_file, which removes a symlink as the link itself rather than following it — a malicious symlink dropped into the log dir cannot cause data loss outside the dir. The match pattern accepts both our startup-archive separator (.) andtauri-plugin-log's intra-session size-rotation separator (_), so the plugin's 5 MB intra-session cap (RotationStrategy::KeepAll) does not leak unbounded archives. - Rust panics are logged with location via a panic hook installed in
setup. (lib.rs:109-123.)
fetch_remote_assetenforces five bounds before returning bytes to the renderer: (a) URL must parse and use schemehttps(http,file,javascript,datarejected); (b) connect + read timeouts are 10 s each via a single sharedreqwest::Client; (c) body is streamed and aborted on overflow at an 8 MB cap; (d)Content-Type(sans parameters) must match the image allowlistimage/{png,jpeg,gif,webp,svg+xml,avif}; (e) HTTP status must equal 200. Bytes are handed to the frontend and converted to ablob:URL — the CSPimg-src/connect-srcdirectives are never widened to permit remote origins. (commands/remote_asset.rs.)reveal_in_folderonly accepts paths that pass the workspace allowlist before any OS handler is spawned: the input is canonicalised and matched againstWatcherState.watched_paths(open-tab files) or anytree_watched_dirsancestor — anything outside (including..traversals or symlinks pointing out of the workspace) is rejected withSystemError::PathOutsideWorkspace. Per-platform spawns use fixed argv (Windowsexplorer /select,<path>; macOSopen -R <path>; Linuxxdg-open <parent>) — no shell is invoked and no user-controlled flag string is concatenated. (commands/system.rs.)
- Workspace-write IPC enforces five bounds before any byte hits disk. The two commands are
write_workspace_text(commands/fs_write.rs:197) andwrite_workspace_binary(commands/fs_write.rs:241); they share a privateensure_writablehelper (commands/fs_write.rs:122). Bounds: (a) the user-supplied filename has no:(NTFS Alternate Data Stream defence — runs before any canonicalization since the byte string is what we're guarding); (b) the parent directory canonicalizes inside an active workspace folder (mirrors rule 4 for read paths and usesWatcherState::is_path_or_parent_allowedfor parent-relaxed semantics); (c) the destination filename has a lowercased-suffix match in the workspace-write allowlist[".excalidraw", ".excalidrawlib", ".excalidraw.png", ".excalidraw.svg"]; (d) the payload size is ≤ 10 MB (symmetric with rule 1's read cap —write_workspace_textchecks UTF-8 byte length,write_workspace_binaryrejects pre-decode if the base64 string exceeds the size required to represent 10 MB); (e) the write goes throughcore::atomic::write_atomic(mirrors rule 5 for sidecars, now extended to user files). A failure at any bound returns a typedWorkspaceWriteErrordiscriminator and writes nothing — reaching the on-disk side requires every bound to pass. Architecture rule 32 indocs/architecture.mdnames this the second IPC chokepoint of the project (the first being read-sidecommands/fs.rs). 29a. Read-side NTFS Alternate Data Stream guard symmetric to bound (a) above.commands/launch.rs::path_has_no_adsis invoked byparse_launch_argsfor every CLI argv, single-instance forwarded path,RunEvent::Openedpath on macOS, and drag-drop path before any canonicalize / metadata call. The guard rejects any:in the path outside the Windows drive-letter prefix (soC:\Users\foois fine,C:\Users\foo.md:hiddenis rejected). Two asymmetries with the write-side guard at bound (a) are deliberate: (i) the read-side check operates on the whole canonicalised path string (catching ADS markers in non-leaf components an attacker could embed, e.g.C:\Users\dir:tag\foo.md), whereas the write-side checks only the leaf filename; (ii) on POSIX a colon in a filename is rare but legal, and the read-side rejects it for cross-platform symmetry — better to over-reject one POSIX edge case than miss the Windows variant. Drive-letter false-positive avoidance: the function strips a leading<letter>:prefix only on Windows. Coverage tests atsrc-tauri/src/commands/launch.rs::tests::path_has_no_ads_*andparse_launch_args_rejects_ntfs_ads_path.
- IPC chokepoint: rule 1 in
docs/architecture.md. window.onerrorat module scope beforecreateRoot: rule 1 indocs/design-patterns.md.ErrorBoundarywrapping: rule 2 indocs/design-patterns.md.read_dirsidecar filtering: rule 22 indocs/architecture.md.
- UI-redress via narrow
styleallow-list (KaTeX).styleis permitted onspan/mathso KaTeX can lay out math, but only when the element (or an ancestor) is katex-classed (rehype-katex-style). The remaining residual risk is a katex-classed wrapper crafted in raw HTML to host an oversized transparent overlay over interactive UI. Defenses in depth: CSPimg-srcblocks remote fetches and there is noscript-srcexposure here; the page chrome lives outside the markdown DOM; React's reconciler keeps event handlers bound to our own components, not user HTML. - No path-origin restriction on mutation commands.
add_comment,edit_comment,delete_comment,update_comment,add_reply,get_file_commentsaccept anyfile_pathstring; a confused renderer call could write<any_path>.review.yaml. Mitigation: allowlist against open tabs/root. check_path_existsandread_binary_filelack the canonicalization guard used byread_dir(commands/fs/mod.rs:56-62check_path_exists;commands/fs/read.rs:115-122read_binary_file). A symlink could redirect image loads outside the workspace.- Sidecar
selected_textandtexthave no per-field length limit (core/types.rs:17-45); the file-level 10 MB cap (rule 3) bounds total sidecar size, but a single comment can still occupy most of that budget. - Full file paths are logged unredacted (across
commands/*.rstracing::error!sites andwatcher.rs:158). Shared logs leak workspace structure and usernames. - MRSF schema version gate.
load_sidecarrejects sidecars with unsupported major versions (> 1) viareject_unsupported_version. Minor versions within major 1 are accepted per spec §5. - Mermaid SVG injected via direct
innerHTML(MermaidRenderer.tsx:129) relies on upstreamsecurityLevel: "strict"with no defense in depth. - Supply-chain rule is not codified. No
deny.toml/cargo-denyor npm audit gate in CI. patch_commentincore/sidecar.rsis public and internally reachable. Future wiring withoutwith_sidecar_mutwould bypass atomic-save.- Launch-args race on macOS "Open With" (
lib.rs:258-287): ifget_launch_argsfires between theis_nonecheck and emit, files can be silently lost. - Production-CSP runtime regression test (rule 17a). The native test harness (
e2e/native/global-setup.ts:163-183) starts a Vite dev server and points the debug Tauri binary atdevUrl, so Tauri'sinject_nonce_token+replace_csp_noncepath is never exercised in CI. The unit test atsrc/__tests__/index-html-no-inline-style.test.tscatches re-introduction of<style>in source AND indist/index.html(when built); the production-runtime CSP path remains verified manually only. Closing this gap requires a build-backed native harness (release-modefrontendDistload) or a synthetic test that serves the bundled HTML with a Tauri-shaped CSP header — out of scope of the original CSP fix. 'unsafe-inline'retained onstyle-src(rule 17a follow-up). Shiki emits per-tokenstyle="color:#…"; KaTeX MathML emits inline transforms; Mermaid SVGs embed inline styles. Narrowingstyle-srcto'self'requires a Shiki chokepoint via@shikijs/transformers+ Constructable Stylesheets, KaTeX MathML output mode, and a Mermaid post-processor that lifts inline styles into adopted stylesheets. All three are pre-requisites; no single one is sufficient on its own.- Shiki call-site duplication (architectural debt; not a security concern but cited here for cross-reference completeness).
src/components/viewers/markdown/MarkdownComponentsMap.tsxandsrc/hooks/useSourceHighlighting.tseach call Shiki'scodeToHtmldirectly with their own language-load retry and theme-mode logic; a single chokepoint insrc/lib/shiki.tswould let the'unsafe-inline'narrowing above land cleanly. Seedocs/architecture.mdGaps for the canonical entry.