Files
guru-connect/reports/review-2026-06-05/SYNTHESIS-three-way.md
Mike Swanson ded99c5882
Some checks failed
Build and Test / Security Audit (push) Successful in 9m2s
Build and Test / Build Server (Linux) (push) Failing after 16m9s
Build and Test / Build Agent (Windows) (push) Successful in 19m25s
Build and Test / Build Summary (push) Has been skipped
docs(review): three-way (Claude+Gemini+Grok) codebase review + remediation plan
Independent reviews of server/agent/dashboard by Gemini 3.1 Pro and
Grok 4.3, each reading source via its own read_file tool, plus a Claude
synthesis and a phased remediation plan. Top convergent finding:
secrets/tokens in WebSocket URL query strings across agent + dashboard.
See SYNTHESIS-three-way.md and REMEDIATION-PLAN.md.
2026-06-05 17:20:30 -07:00

11 KiB

GuruConnect — Three-Way Independent Review (Claude + Gemini + Grok)

Date: 2026-06-05 Reviewers: Gemini gemini-3.1-pro-preview, Grok 4.3, Claude (synthesis + cross-check) Scope: server (42 Rust files), agent (34 Rust files), dashboard (66 TS/TSX files). Each model read the source via its own read_file tool (no embedding). Grok's server pass was split A/B because 42 files exceeded its read-file capacity. Raw reports: gemini-{server,agent,dashboard}.md, grok-server-a.md, grok-server-b.md, grok-agent.md, grok-dashboard.md (same folder).

Findings where both external models independently agree are marked [CONVERGENT] — higher confidence. Claude's cross-checks/adjustments are in italics.


The one cross-cutting theme: secrets in WebSocket URL query strings

This is the single highest-signal finding — flagged independently as CRITICAL on all three surfaces:

  • Agent → relay (agent/src/transport/websocket.rs): api_key/cak_, support_code, machine_uid in the WS URL query. [CONVERGENT — both models' #1 for the agent]
  • Dashboard → relay (dashboard/src/features/sessions/JoinSessionModal.tsx buildViewerUrl): the session-scoped viewer token (the actual remote-control capability) in ?token=. [CONVERGENT — both models' #1 for the dashboard]

Query strings leak into reverse-proxy/relay access logs, browser history, and copy-paste. Over wss:// the body is encrypted but the URL is routinely logged.

Claude cross-check: this is partly by current designCLAUDE.md security rules explicitly mandate "Viewer WebSocket: JWT token required in token query parameter." So this isn't an accidental bug; it's a spec decision both external models reject. Recommendation: revisit that spec rule — move auth to the first post-connect protobuf frame or Sec-WebSocket-Protocol. Server-side the viewer token is already hardened (minted, session-scoped, signature+purpose+session_id-binding checked, blacklist-checked on the WS plane — confirmed in grok-server-b), so the residual risk is purely the transport channel. Fix this once, on both planes.


CRITICAL

# Area Finding Source
C1 agent + dashboard Secrets/tokens in WS query string (see theme above) [CONVERGENT] Gemini+Grok, both surfaces
C2 server/src/api/downloads.rs Unauthenticated download endpoints: hardcoded prod relay URL + "managed-agent" default API-key fallback; support-download docstring says "embeds code" but only renames the file (temp sessions get no embedded config). [CONVERGENT] Gemini-server (key), Grok-server-a (key + URL + docstring gap)
C3 server/src/api/downloads.rs Response::builder()…body(…).unwrap() on attacker-controlled Content-Disposition filename (sanitize_filename doesn't escape "/control chars) → unauthenticated panic / worker DoS. Grok-server-a
C4 agent/src/transport/websocket.rs `block_in_place(
C5 agent/src/update.rs Auto-update verified by SHA-256 only over the same channel as the binary — no signature. A malicious/MITM relay can push SYSTEM-level code to the whole fleet. [CONVERGENT] Gemini-agent (#1) + Grok-agent (HIGH)

Claude note on C5: code carries an explicit TODO acknowledging the missing signature, and there's a debug-only dev_insecure_tls escape hatch. Both models rate this top-tier; it's the highest-impact agent issue after the query-string transport.


HIGH

# Area Finding Source
H1 server/src/api/auth.rs (login/change_password) No rate limiting / lockout on the human login path, despite sophisticated per-(site_code,ip) + timing-equalizer protection on the machine-enroll path. Unlimited online brute-force / credential stuffing. [CONVERGENT] Gemini-server + Grok-server-a
H2 bootstrap admin password First-run writes the generated admin plaintext password to .admin-credentials in CWD, with an info! log fallback on write failure. [CONVERGENT] Gemini-server + Grok-server-b (Grok's "most important")
H3 server/src/api/auth_logout.rs:116 revoke_user_tokens is a 501 stub but the comment claims partial behavior — an admin "revoke any user's tokens" endpoint that does nothing. Grok-server-a
H4 server/src/auth/token_blacklist.rs:85 cleanup_expired re-runs full JWT signature verification on every blacklisted token (stores whole tokens in RAM) instead of comparing a stored exp. Grok-server-a
H5 self-service role guard Server does not block self-role-demotion (only self-delete is blocked); the lock-out guard is client-side only in EditUserModal.tsx. [CONVERGENT] Gemini-dashboard + Grok-dashboard (both cite the in-code server comment)
H6 dashboard auth lifetime JWT in sessionStorage, blindly attached as Bearer; no exp handling, refresh, or idle timeout — XSS/workstation compromise → long-lived privileged access + ability to mint per-session control tokens. [CONVERGENT] Gemini-dashboard + Grok-dashboard
H7 agent/src/session/mod.rs consent Attended-session consent (MessageBoxW) is .awaited inside the main loop → for up to ~60s no heartbeats / status / stop-signal processing. [CONVERGENT] Gemini-agent + Grok-agent
H8 agent/src/credential_store.rs cak_ store ACL set by shelling out to bare icacls (PATH-search) from a SYSTEM context → LPE if icacls.exe is hijacked; silent weaker store on failure. Gemini-agent (+ Grok-agent notes the silent-failure variant)

Claude cross-check on H2: the server deploys on Linux (Ubuntu 22.04 per CLAUDE.md), so the 0o600 chmod is effective and Grok's "Windows ACL is a no-op" framing doesn't apply to the deployed server. The real residual risk is the plaintext-on-disk + the info! log-fallback path. I'd keep this HIGH, not CRITICAL.


MEDIUM (consolidated)

  • Weak password policy — min length 8 only, no complexity/upper-bound/breach check. [CONVERGENT] (Gemini-server, Grok-server-a)
  • Input validation caps — unbounded/uncontrolled machine_uid, hostname, site_code, password length on unauthenticated enroll; no control-char stripping before DB / rate-limit keys. (Grok-server-a)
  • Agent embedded-config trustGURUCONFIG blob (server_url + enrollment_key) appended to the EXE is read with an unchecked length field and from_slice, no signature/allowlist → attacker blob can repoint the agent to a hostile relay. (Grok-agent)
  • support_code derived from EXE filename with no checksum/strength, then sent in the query string. (Grok-agent)
  • Audit-trail correctnessCONNECTION_REJECTED_* events insert a random session_id (dangling vs connect_sessions) instead of NULL like the enrollment/removal paths. (Grok-server-b) Ties into the earlier in-product audit assessment: the write path works but has rough edges.
  • Panic sites on response/header constructionprometheus_metrics registry.lock().unwrap()/encode().unwrap(); security_headers from_static(...).unwrap(). (Grok-server-b)
  • Declared-but-missing viewer input rate limitVIEWER_INPUT_EVENTS_PER_SEC = 200 constant + comment promise coalescing/drop, but the path is a plain mpsc(64) with only backpressure; [CONVERGENT-ish] Gemini-server raised the same aggregate-flood concern. (Grok-server-b, Gemini-server)
  • Non-atomic user update — dashboard does updateUser then setUserPermissions as two calls; partial-failure leaves drift. [CONVERGENT] (Gemini-dashboard, Grok-dashboard) → wants a single transactional PATCH /api/users/:id.
  • Hand-maintained TS type mirrors of Rust API structs → silent type drift. [CONVERGENT] (both dashboard reviews) → generate via ts-rs/specta.
  • Premature clearSession() on any getMe failure (incl. network status 0) → flaky-connection logout loops. (Gemini-dashboard)
  • In-memory rate-limit/lockout state — reset on every restart/deploy → attacker gets a fresh brute-force window. (Grok-server-b; Gemini-server LOW)
  • H.264 MFT robustness / capture resource release — complex unsafe COM streaming-state machine, per-frame silent drops, best-effort .ok() releases (leak/double-release risk); per-frame capture() not under the same catch_unwind as init. (Grok-agent)
  • Error-body passthrough — dashboard surfaces raw server error text in toasts → potential internal-detail leak. (Grok-dashboard)

LOW / maintainability

  • Redundant post-decode exp checks (leeway=0) in jwt.rs. (Grok-server-a)
  • set_client_access stores client UUIDs without existence check (dangling refs). (Grok-server-a)
  • Large #[allow(dead_code)] surface ("TODO(native-remote-control)", Phase 4) — risk that future activation enables un-reviewed paths. (Grok both server + agent)
  • Audit-log table grows unbounded, no pruning/retention. (Gemini-server)
  • react-query polling is aggressive with no document.hidden pause/jitter. (Grok-dashboard)
  • stubs.ts dead scaffolding still in the barrel export. (Grok-dashboard)
  • Duplicated error envelopes (ErrorResponse vs ApiError) + repeated role allow-lists across handlers. (Grok-server-a)
  • Deprecated document.execCommand("copy") clipboard fallback. (Gemini-dashboard)

What all three reviewers independently praised (don't regress these)

Argon2id + timing-equalizer on the unauthenticated enroll path; fully parameterized sqlx (no SQLi surface); per-agent cak_ identity binding that stops a client from seizing another machine's session; atomic single-use support-code consumption at bind time; trusted-proxy-aware client_ip extractor as the single root for rate-limit + audit; consent gate before any viewer join/stream; reattach/supersede/reap TOCTOU hardening via recheck-under-write-lock; bounded WS message sizes on both planes; minted session-scoped viewer tokens with blacklist + session-binding checks; DPAPI + ACL credential store with readback verification; thoughtful one-time-secret UX (codes/keys/passwords) and dialog focus management on the dashboard.


  1. C1 / query-string secrets — one architectural change, both planes; revisit the CLAUDE.md viewer-token-in-query rule. Highest leverage.
  2. C2 + C3 — harden the unauthenticated downloads.rs surface (remove default key + hardcoded URL, kill the .unwrap() panic, fix support-embed gap). Cheap, high exposure.
  3. H1 — add login rate-limit/lockout (reuse the enroll limiter). Cheap, high value.
  4. C5 / update signing — embed a public key, sign the update manifest+binary. Fleet-wide RCE prevention.
  5. C4 + H7 — fix agent async/sync mixing and move consent off the main loop.
  6. H2, H3, H4, H5, H6 — bootstrap-secret handling, finish/remove the logout stub, cheap blacklist cleanup, server-side self-demotion guard, dashboard token lifetime.
  7. MEDIUM/LOW as capacity allows; prioritize embedded-config signing and the audit session_id-NULL fix.