Skip to content

release: v1.17.0 — security audit 2026-05-09 (30+ FIND remediated)#48

Merged
muchiny merged 87 commits into
mainfrom
security/audit-2026-05-09
May 9, 2026
Merged

release: v1.17.0 — security audit 2026-05-09 (30+ FIND remediated)#48
muchiny merged 87 commits into
mainfrom
security/audit-2026-05-09

Conversation

@muchiny

@muchiny muchiny commented May 9, 2026

Copy link
Copy Markdown
Owner

Summary

Full security audit branch landing — 30+ FIND-### findings remediated, 86 commits, 2 BREAKING default flips. Tag v1.17.0 already pushed (Docker + Release workflows running).

BREAKING

  • tool_groups default-disabled, ships 8-group minimal profile (FIND-024).
  • security.require_elicitation_on_destructive defaults to true (FIND-022).
  • SshConfigDiscovery default off (FIND-023).

Highlights

  • Per-session isolation: PendingRequests, SessionCapabilities, active_requests, runtime/notification/resource_subs/roots, log_level (Vuln 8/9 + FIND-033..038).
  • Secret zeroization: sudo_password, db_password, vault Args.data, SOCKS proxy password, vault/db secrets via stdin/tempfile (FIND-014/028/029/030/031).
  • YAML hardening: saphyr Budget on every parse site + deny_unknown_fields on every config struct (FIND-001/002/004/032 + FIND-017).
  • Auth: JWT signature verification (Vuln 2) + sub/iss/aud spec claims (FIND-007); OAuth keys boot-loaded, shared Arc<OAuthValidator> (FIND-006).
  • Transport: HTTP defaults loopback + body/timeout/request-id/redaction middleware (FIND-005); russh algo pinning + rekey limits (FIND-008); SSH error sanitization (FIND-016).
  • Reliability: replace expect/unwrap with ?/Err on Result fns (FIND-010..013); clippy clean --all-targets --all-features (FIND-019/020); cargo-geiger CI fallback (FIND-021).
  • Supply chain: drop archived shellexpand (FIND-025); patch winrm-rs reqwest feature (FIND-018); monitoring entries for saphyr + tokio-socks (FIND-026/027).

Full inventory in CHANGELOG.md + commit log.

Test plan

  • cargo fmt --check clean
  • cargo check clean
  • cargo clippy -D warnings clean
  • CI green on PR
  • Docker workflow green (tag-driven)
  • Release workflow green (tag-driven)

🤖 Generated with Claude Code

muchiny and others added 30 commits May 9, 2026 02:28
12 vulns triaged by severity and grouped into 4 PRs:
- P0 input validation + audit redaction + path traversal (Tasks 1-7)
- P1 HTTP transport hardening (Tasks 8-9)
- P1 multi-session isolation (Tasks 10-11)
- P2 validator shell-aware normalization (Task 12)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vuln 7 (audit 2026-05-09). protocol arg was interpolated raw into
iptables/ufw/firewall-cmd shell commands. Added validate_protocol()
restricting to {tcp, udp, icmp, icmpv6}; called from both build_allow_command
and build_deny_command.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vuln 6 (audit 2026-05-09). unit_type was interpolated raw into
'systemctl list-units --type={utype}'. Converted build_list_command
to Result, added validate_unit_type() with the documented set of
unit types. Updated ssh_service_list handler to propagate the error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vuln 5 (audit 2026-05-09). Variable KEYS in HashMap<String,String>
were interpolated raw into 'export {k}={v}' shell commands while
values were correctly escaped. Added validate_env_var_name() requiring
POSIX [A-Za-z_][A-Za-z0-9_]*; converted build_template_command to Result.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vuln 12 (audit 2026-05-09). build_user_info_command and
build_group_members_command concatenated raw username/group strings
into LDAP filter syntax. Added ldap_filter_escape() encoding (, ), *,
\, NUL per RFC 4515 §3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vuln 4 (audit 2026-05-09). Hardcoded 'TEMPLATE_EOF' allowed an attacker
to close the heredoc by including a sole-line 'TEMPLATE_EOF' in the
content body, then run arbitrary shell after it. Now generates
'MCP_EOF_{uuid}' per call and verifies it does not appear as a sole
line in the body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vuln 3 (audit 2026-05-09). Audit log was emitting MYSQL_PWD,
PGPASSWORD, Bearer tokens, and webhook URL secrets in plaintext to
both the JSONL file and tracing sinks. Added new_with_sanitizer
constructor on AuditLogger; runs Sanitizer::sanitize over event.command
before tracing emission and file write. Audit log file now opens with
mode 0o600 on Unix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vuln 11 (audit 2026-05-09). validate_root_scope did a string prefix
match without resolving '..', so '/declared-root/../../etc/shadow'
passed. Added normalize_path_lexical() that collapses '.', '..', and
empty components before the prefix check.

ssh_file_read already routes through scoped_paths -> validate_root_scope
in the StandardToolHandler pipeline (standard_tool.rs:281), so fixing
the validator alone closes the bypass for that handler too. Sibling
ToolHandler-based handlers (ssh_file_write, ssh_ls, ...) call
validate_root_scope directly and inherit the same fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…public bind

Vuln 1 (audit 2026-05-09). HttpTransportConfig::default() bound
0.0.0.0:3000 with OAuth disabled and an Origin guard that explicitly
forwarded requests without an Origin header — full unauthenticated
RCE for any non-browser network attacker.

Changes:
- Default bind: 127.0.0.1:3000 (transport + YAML config)
- New allow_unsafe_bind field on HttpTransportConfig (defaults false)
- serve() refuses non-loopback bind unless OAuth is enabled or
  allow_unsafe_bind is explicitly true
- New --insecure-bind CLI flag for serve-http
- origin_guard rejects requests with no Origin header (was forwarded
  before, defeating anti-DNS-rebinding intent)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vuln 2 (audit 2026-05-09). Previous validator decoded the JWT payload
and read claims without verifying the signature, exp, nbf, or alg.
Replaced with jsonwebtoken-backed verification: RSA + ECDSA + RSA-PSS
algorithms only (HMAC family rejected to prevent alg-confusion), exp/nbf
checked with 30s leeway, iss/aud enforced, required_scopes enforced.

Static-keys mode only in this commit; load_jwks() accepts a parsed JWKS
document but the HTTP fetch and Arc<OAuthValidator> wiring through Axum
extensions are deferred to a follow-up. OAuth-enabled deployments must
populate keys via OAuthValidator::set_static_keys until that wiring
lands; in the meantime the per-request middleware validator has an
empty key map and rejects every token with "Unknown JWT signing key".

Stub-tests that asserted Ok on tokens with alg=none and forged
signatures have been removed — they encoded the previous insecure
behaviour. Added six new tests covering signature, exp, iss, scope,
alg=none rejection, and a happy-path accept. RSA test fixtures live
under tests/fixtures/oauth/ (synthetic, test-only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t 1)

Vuln 8 (audit 2026-05-09) part 1. Switched server-initiated request
ids from monotonic 'srv-{N}' to 'srv-{uuid_v4_simple}'. Even if a
multi-session deployment leaks the map, ids cannot be brute-forced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vuln 8 (audit 2026-05-09) part 2. Moved PendingRequests from a single
server-wide Arc to a per-session allocation in serve_session. Threaded
through ToolContext so handlers reach the session-local map. Added
multisession_isolation integration test verifying client B cannot
resolve client A's pending request.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vuln 9 (audit 2026-05-09). client_supports_elicitation /
client_supports_sampling / client_supports_roots were single per-server
AtomicBools that flipped to true on the first client's initialize and
were never reset. In daemon multi-session mode, a client that did NOT
advertise elicitation could still trigger destructive tools and
auto-confirm via global state from a prior client.

Moved all three flags into a per-session SessionCapabilities struct
(src/mcp/session_capabilities.rs). serve_session allocates a fresh
Arc<SessionCapabilities> per connection. handle_initialize writes its
client's capabilities to the session-local struct. ToolContext snapshots
the booleans for handlers; check_destructive_elicitation reads the
session_caps directly so the gate consults THIS session only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ist match

Vuln 10 (audit 2026-05-09). Default blacklist regexes use \s+ between
command words (rm\s+-rf\s+/), so an MCP client sending
'rm${IFS}-rf${IFS}/' bypassed the regex and ran 'rm -rf /' on the
remote host once shell expansion happened. validate() and
validate_builtin() now collapse ${IFS}, $IFS, $'\t', $'\n', $' ',
and '\<NL>' to single spaces before running the blacklist regexes;
the whitelist still matches the raw command for byte-for-byte
strictness.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Accumulated formatting drift from per-task commits. No semantic changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cached per-crate upstream docs for 10 audit-critical deps and produced a
drift summary against in-tree code. Drift findings flagged P0–P2 feed
the Task 8/9/10/11 scans:

- P0: serde-saphyr loaders use bare from_str (no Budget) — billion-laughs
  vector on src/config/loader.rs and src/domain/runbook.rs
- P0/P1: axum HTTP transport missing TimeoutLayer, DefaultBodyLimit
  override, SetSensitiveHeaders, RequestIdLayer
- P1: jsonwebtoken Validation missing set_required_spec_claims
  (sub/iss/aud only validated when present)
- P1: russh client::Config::Preferred not explicitly set — relies on
  upstream Default which may include weaker legacy algos

No-drift findings (russh check_server_key, rustls dangerous(),
serde_yaml removal, secrecy substitution by Zeroizing) documented for
audit completeness.
Phase B+C of full security audit campaign per
docs/superpowers/plans/2026-05-09-full-security-audit.md (Task 5).

Skill: audit-context-building:audit-context-building (trailofbits, v1.1.0).
Six function-analyzer subagents dispatched in parallel on the highest-risk
surfaces. Each completed the per-function microstructure checklist
(Purpose, Inputs+Assumptions, Outputs+Effects, Block-by-Block analysis with
First Principles + 5 Whys + 5 Hows, Cross-Function Dependencies) under the
skill's quality thresholds (>=3 invariants/fn, >=5 assumptions, >=3 risk
considerations, >=1 First Principles, >=3 combined 5-Whys/5-Hows).

Surfaces analysed:
- src/security/validator.rs (raw + builtin gate, normalize_for_blacklist_match)
- src/mcp/pending_requests.rs (Vuln 8 patched surface, per-session isolation)
- src/mcp/session_capabilities.rs (Vuln 9 patched surface, AtomicBool flags)
- src/mcp/transport/oauth.rs (JWT validator, alg-confusion mitigation)
- src/ssh/client.rs (Handler::check_server_key, auth paths, jump-host RAII)
- src/domain/runbook.rs (YAML loader, apply_template, command construction)

Pure context only — no findings, no severities, no PoCs per skill non-goals.
Open questions per surface feed Tasks 8, 9, 11, 13.
Phase B+C of full security audit per
docs/superpowers/plans/2026-05-09-full-security-audit.md (Task 6).

The trailofbits entry-point-analyzer skill explicitly excludes
non-smart-contract codebases ("Do NOT use this skill for non-smart-
contract codebases"), so this surface mapping is produced via direct
programmatic classification: file-name pattern matching + handler-body
grep for credential keywords + body grep for destructive_hint markers
+ raw-exec vs builtin-exec discrimination by checking for use_case
delegation. Method documented in audit/2026-05-09/surface/entry-points.md
header.

Risk score = writes_files*4 + executes_shell_raw*4 + handles_creds*3
+ destructive*3 + executes_shell_builtin*2 + reads_files*1.

Bucket distribution (358 non-mod.rs handlers — actual count is 358,
not 357 as documented in CLAUDE.md):
- P0 (>=10): 97 (raw exec, file writes, cred-bearing destructive ops)
- P1 (7-9): 21
- P2 (4-6): 222 (majority — read-only credential queries, builtin exec)
- P3 (1-3): 18
- P4 (0): 0 (every handler scored at least 1)

Output cross-checked: ls of src/mcp/tool_handlers/ excluding mod.rs
= 358 = CSV row count.

Feeds Tasks 8, 9, 11 for targeted scans on the P0 + P1 surfaces.
Phase D of full security audit per docs/superpowers/plans/2026-05-09-full-security-audit.md (Task 8).

Skill: static-analysis:semgrep (trailofbits, v1.2.1).
Engine: Semgrep OSS 1.162.0 (Pro not available — no cross-file taint).
Mode: important-only (severity MEDIUM+, post-filter category=security + MEDIUM/HIGH confidence+impact).
Scope: src/security/, src/ssh/, src/mcp/, src/domain/, src/config/, src/winrm/, src/psrp/ (502 .rs files).
User-approved scan plan via AskUserQuestion hard gate per skill protocol.

Four parallel scanner subagents dispatched:
- r/rust (4 rules)               -> 0 findings
- p/security-audit (2 multilang) -> 0 findings (pack does not cover Rust)
- r/generic.secrets (48 rules)   -> 5 findings (all FP: sanitizer self-test fixtures)
- trailofbits/semgrep-rules/rust -> 4 findings (panic-in-Result class)

Merged SARIF: 9 findings.

True positives (4):
- src/ssh/retry.rs L204 + L282 — panic in Result-returning fn (hot path)
- src/ssh/pool.rs L395         — panic in Result-returning fn (pool drain)
- src/mcp/tool_handlers/ssh_file_write.rs L237 — panic in handler

False positives (5):
- src/security/sanitizer.rs L38, L1021, L1190, L1239, L1289 — deliberate
  test vectors so the sanitizer can verify it redacts those patterns.

The four Task-4 context7 drift findings (serde-saphyr no Budget, axum
missing security middleware, jsonwebtoken missing set_required_spec_claims,
russh Default Preferred algos) and the five Task-5 context-summary focus
areas (validator bypass, path traversal, plain-String creds, cross-session
shared state, audit log raw command) are NOT pattern-matchable by the
available OSS Rust rulesets. They are restated in static-analysis.md as
custom-rule candidates for Task 12 (semgrep-rule-creator).
Single-source-of-truth backlog for every concrete problem the
2026-05-09 audit surfaces. Each finding has a stable FIND-NNN id
(append-only), source attribution, fix recommendation, and status.

Initial seed (21 open findings + 5 confirmed FP + 12 OQ):
- P0 (6): serde-saphyr Budget gaps x4, axum middleware stack, OAuth
  validator key-population production wiring
- P1 (3): jwt set_required_spec_claims, russh Preferred algos +
  Limits, EdDSA allowlist gap
- P2 (8): 4 panic-in-Result (semgrep TOB), SOCKS password not
  Zeroizing, originator_address hardcoded, sanitize_ssh_error
  coverage gap, deny_unknown_fields missing
- P3 (4): cargo outdated broken (winrm-rs/reqwest), 2 pre-existing
  clippy errors blocking ci-full, cargo-geiger nkeys extract issue
- FP (5): all in src/security/sanitizer.rs (sanitizer self-test
  fixtures legitimately containing example secrets)
- OQ (12): consolidated from per-surface Open Questions in
  audit/2026-05-09/surface/context-summary.md

Append protocol for Tasks 9-17 documented at end of file.
This tracker is the actionable backlog; audit/2026-05-09/FINDINGS.md
will be the Task-16-generated derivative report.
6 sibling vulnerabilities identified by enumerating every McpServer
field for the Vuln 8/9 pattern (process-singleton mutable collection
keyed without session scope, mutated by per-session handlers).

CRITICAL — FIND-038 (P0): src/mcp/server.rs:91 active_requests
HashMap<String, CancellationToken> keyed on caller-chosen JSON-RPC
request id. Concurrent client B can cancel client A's in-flight
request by sending notifications/cancelled with A's request id.
Direct cross-session DoS — same defect class as Vuln 8.

P1 quality-of-service / cross-session contamination:
- FIND-033 runtime_max_output_chars (last-writer-wins)
- FIND-034 notification_tx slot (notifications route to wrong session)
- FIND-036 resource_subscriptions HashMap (URI-keyed, not session-keyed)
- FIND-037 roots Vec (last-writer-wins per fetch_roots)

P3:
- FIND-035 log_level cross-session mute

Verified secure (no finding): client_info, all *::ConnectionPool /
WinRmPool / PsrpPool (intentional cross-session by design),
sanitizer/registry static collections.

Recommended fix shape for all 6: same as Vuln 8/9 — move field out of
McpServer, allocate per-session Arc in serve_session, clone into
spawned tasks, audit read/write sites.
muchiny and others added 22 commits May 9, 2026 23:10
…file (FIND-024)

BREAKING CHANGE: Unlisted tool groups now default to DISABLED. Operators
who relied on "unlisted = enabled" must explicitly enable each group in
config.yaml under `tool_groups.groups`. The minimal default profile is
[core, file_ops, directory, process, monitoring, network, systemd, sessions].

Pre-FIND-024, `tool_groups: { groups: {} }` registered every group (75
groups / 357 handlers), exposing AD/LDAP/Vault/K8s/AWS/ESXi/HyperV /
Windows-only tools to operators who only needed `docker` + `service`.

The `is_group_enabled` resolver now falls through unlisted groups to
membership in the new `MINIMAL_DEFAULT_GROUPS` const (eight groups
covering raw exec, file ops, ls/find, process mgmt, system metrics,
network diagnostics, systemd services, and persistent tmux sessions).
Explicitly listed groups (`true` or `false`) still win over the default
profile.

Test migration: introduced `create_all_enabled_registry()` and
`all_enabled_tool_groups_config_for_test()` test helpers; ~85 tests in
mcp::registry, mcp::meta_tools, and mcp::server that relied on the old
"all groups enabled" semantic now use these helpers. Production paths
(`server.rs:221`, `cli/runner.rs`) are unaffected — they already drive
the registry from operator-supplied `config.tool_groups`.

Added FIND-024 regression test
(`test_default_registry_only_contains_minimal_profile`) that asserts
every tool in the default registry belongs to a `MINIMAL_DEFAULT_GROUPS`
group, every minimal group registers at least one handler, and the
default registry is strictly smaller than the full inventory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shellexpand crate was archived upstream on 2026-02-25 — no further
security patches. Replace it with a thin dirs::home_dir-based helper
in a new src/path_utils.rs module exposing home_expand,
home_expand_string, and home_expand_or_input (the latter preserves
shellexpand's "never fail" contract by passing through unchanged when
home cannot be resolved).

Migrated 10 call sites (CLI runner ×3, config loader ×2, ssh_config
×1, ssh client ×1, file-transfer handlers ×3). dirs is already a
direct dep so no new transitive surface.

Verification:
- cargo build --lib clean
- cargo test --lib: 7017/7017 pass (incl. 6 new path_utils tests)
- cargo clippy --lib -- -D warnings clean
- cargo machete clean (no new unused deps)
- shellexpand fully removed from Cargo.lock

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SshVaultWriteArgs.data carried vault key=value secret pairs as plain
Vec<String>; the heap allocation persisted until Args drop, leaving
secret bytes resident.

- SshVaultWriteArgs.data: Vec<Zeroizing<String>>
- VaultCommandBuilder::build_write_command takes &[Zeroizing<String>]
- shell_escape call site unchanged (Deref coercion: &Zeroizing<String>
  -> &String -> &str)
- 7 test fixtures across vault.rs and ssh_vault_write.rs updated to
  wrap their literals in Zeroizing::new
- Test assertions on Args.data converted via .iter().map(|s| s.as_str())

Note: this only addresses LOCAL heap residency. The secret values
still transit shell argv on the REMOTE host (visible in `ps eww` /
`/proc/PID/environ`) — that is FIND-031, addressed in Task 21 by
piping data via stdin.
Sprint 2 Task 21. Secrets no longer transit shell argv or environ on
the remote host.

Vault (`build_write_command`):
- `key=value` data switched from argv to a stdin heredoc with a
  randomized `VAULT_DATA_EOF_<uuid>` terminator (mirrors the
  `template_apply` pattern from 2da5d55). Single-quoted terminator
  disables shell expansion; the body lives in the kernel pipe
  buffer, never `ps eww`.

Database (`build_query_command`, `build_dump_command`,
`build_restore_command`):
- `MYSQL_PWD=...` env var replaced by `mysql --defaults-extra-file=$TMPF`
  reading a 0600 tempfile that holds `[client]\npassword=...`.
- `PGPASSWORD=...` env var replaced by `PGPASSFILE=$TMPF` pointing at
  a 0600 `~/.pgpass`-format file (`host:port:db:user:password`,
  with libpq `:`/`\\` escaping).
- Tempfile creation: `mktemp` (race-free), `chmod 600` before write
  (no TOCTOU), cleanup `trap 'shred -u "$TMPF" 2>/dev/null || rm -f
  "$TMPF"' EXIT` (handles BusyBox/Alpine where `shred` is missing).

Tests:
- Six FIND-031 regression tests in `database.rs` covering query, dump,
  and restore for both MySQL and PostgreSQL — assert no password
  appears post-redirect (the `ps eww`-visible portion) and that
  `MYSQL_PWD=` / `PGPASSWORD=` env vars are gone.
- Three FIND-031 regression tests in `vault.rs` — argv-leak split on
  `<<`, stdin-heredoc shape, randomized-terminator (call twice and
  assert different strings).
- Handler assertions in `ssh_db_query.rs` and `ssh_db_dump.rs` flipped
  from `PGPASSWORD=` to `PGPASSFILE=$TMPF`.

7027 lib tests green; `cargo clippy --lib -- -D warnings` clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s (FIND-019/020)

- tests/security_audit_redaction.rs:84: inline format args
  (clippy::uninlined_format_args, FIND-019)
- src/mcp/transport/oauth.rs:531 sign_token(&Value): pass by reference
  (clippy::needless_pass_by_value, FIND-020); 9 call sites updated
- tests/ssh_preferred_algos.rs:117: struct-update syntax instead of
  field reassignment after Default::default()
  (clippy::field_reassign_with_default; same class, introduced by FIND-008
  test additions, batched here)

cargo clippy --workspace --all-targets --all-features -- -D warnings: clean.
cargo test --lib: 7027 passed.
`winrm-rs 1.0.0` on crates.io declares `reqwest ^0.13` with feature
`webpki-roots`, which reqwest 0.13.3 marked obsolete. cargo outdated
fails to resolve the dep graph as a result, blocking version-audit
tooling.

Upstream fix: ~/winrm-rs commit 573dadf drops the obsolete feature
(rustls + socks remain; webpki-roots ships transitively via rustls
0.23+). Pin via [patch.crates-io] until winrm-rs > 1.0 is published.

`cargo outdated` now resolves; `cargo test --lib` 7027 pass.
cargo geiger --all-features fails to extract nkeys-0.4.5 (aws-sdk
transitive) on cold cargo cache. Makefile target now:
- pre-fetches the crate graph
- falls back to --forbid-only on extraction failure (acceptable since
  #![forbid(unsafe_code)] is enforced workspace-wide)
- corrects --output-format from `ascii` to `Ascii` (cargo-geiger 0.13
  parses the value case-sensitively)

audit/2026-05-09/baseline/cargo-geiger.txt re-captured at 2730 lines,
all entries `?` (no unsafe forbidden status known) — expected for the
forbid-only path.
`McpServer.log_level` was a server-singleton `Arc<AtomicU8>`. Any
session's `notifications/setLevel` overwrote it, so client B could
silently mute client A's `notifications/message` stream
(cross-session denial-of-observability).

- SessionContext gains `log_level: Arc<AtomicU8>` (mirrors the per-session
  storage pattern from FIND-033/034/036/037).
- serve_session() builds the per-session McpLogger against the session's
  slot, so notifications are gated by THIS client's threshold.
- handle_logging_set_level writes the session's slot; falls back to the
  server-wide field for legacy non-session call paths (tests).
- Server-wide field retained as fallback for the same backward-compat
  reason.
- Regression tests in tests/per_session_log_level.rs cover (a) default
  starts at Warning, (b) two sessions hold independent storage with
  distinct Arc allocations.

cargo test --lib: 7027 pass.
cargo test --test per_session_log_level: 2/2 pass.
cargo clippy --workspace --all-targets --all-features -- -D warnings: clean.
Three integration suites still asserted the pre-audit defaults:

- tests/config_validation.rs::test_valid_config_preserves_defaults:
  expected ssh_config.enabled = true; FIND-023 flipped it to false.
- tests/mcp_conformance.rs (2 tests): used ToolGroupsConfig::default()
  to drive a 357-tool registry; FIND-024 flipped default to the 8-group
  minimal profile. Switched to create_all_enabled_registry() (test helper).
- tests/tool_filtering.rs (~13 tests): same root cause. Switched fixtures
  to all_enabled_tool_groups_config_for_test() so the disable-X tests
  still exercise the full registry as their starting point.

cargo test --tests: 0 FAILED (was 4 FAILED). All 7027 lib + integration
tests green.
Audit complete, all FIND-### remediation merged to security branch.
Drop audit/, docs/audit-2026-05-09-findings.md, security-fixes plan.
Findings tracker history preserved in git log + CHANGELOG.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps Cargo + dxt manifest + server-card to 1.17.0. CHANGELOG promotes
[Unreleased] to [1.17.0] with full FIND-### inventory (BREAKING flips
on FIND-022 + FIND-024, per-session isolation, secret zeroization,
saphyr Budget, JWT/OAuth, russh hardening). Includes rustfmt sweep
across security fix sites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation rust security tools domain labels May 9, 2026
CI runners don't have a sibling /work/mcp-ssh-bridge/winrm-rs checkout.
Switch to git ref pinning the FIND-018 fix on muchiny/winrm-rs at
573dadf5 (drops obsolete reqwest webpki-roots feature).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@muchiny muchiny merged commit 034830b into main May 9, 2026
19 of 20 checks passed
@muchiny muchiny deleted the security/audit-2026-05-09 branch May 30, 2026 23:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation domain rust security tools

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant