Skip to content

security: remediate September 2026 audit findings (WebSocket, sessions, identity, auth core) - #26

Merged
JedimEmO merged 8 commits into
masterfrom
security/hardening-2026-09
Sep 5, 2026
Merged

security: remediate September 2026 audit findings (WebSocket, sessions, identity, auth core)#26
JedimEmO merged 8 commits into
masterfrom
security/hardening-2026-09

Conversation

@JedimEmO

@JedimEmO JedimEmO commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Summary

Remediates every finding from the September 2026 security review (Codex) plus a follow-up gap sweep, then four more review rounds against the fixes themselves. 24 original issues plus 10 residuals, each with a regression test named by its ID. Codex has signed off on the final state.

Full per-crate detail is in CHANGELOG.md under Unreleased (six dated sections, most recent first).

WebSocket (ras-jsonrpc-bidirectional-server / -macro / -client / -types)

  • W1 Subscriptions are re-authorized on credential re-validation; PermissionChangePolicy (drop or close). Topic-routed messages are tagged and re-checked at the socket write (egress gate), closing the revocation race.
  • W2 max_message_size / max_frame_size enforced at the transport, not after buffering.
  • W3 SubscriptionLimits (per message, per connection, topic length, global). Enforced inside ConnectionContext::subscribe, the only mutation path, so they hold from any handler callback and with any ConnectionManager. Manager keeps state and topic index consistent under one guard.
  • W4 KeepaliveConfig (server ping + idle timeout). Zero durations no longer panic.
  • W5 WITH_PERMISSIONS routed through AuthProvider::check_permissions over WebSocket.
  • Connection cap enforced atomically with a semaphore permit; bounded default (10 000). connection_permits and subscription_accounting are required WebSocketService methods.
  • C1 AuthConfig::JwtParams removed (token in URL). C2 custom params percent-encoded. Browser auth now works: client offers ras-jsonrpc + token.<jwt> subprotocols, server selects the first so the token is never echoed.

Sessions (ras-identity-session)

  • S1 No write-locked sweep on every verify_session. S2 iss/aud required by default (SessionConfig::new(secret, iss, aud), allow_unscoped_tokens() opt-out). S3 secret entropy + placeholder checks. S4 iat/nbf future-dated rejection. S5 max_sessions_per_user.

Identity (ras-identity-local, ras-identity-oauth2)

  • I1 password_hash / client_secret no longer serialized. I2 redacted LocalAuthPayload Debug. I3 OAuth2 state store evicts instead of refusing (login lockout). I4 Argon2 on spawn_blocking, password length cap. I5 optional callback code, fixed ProviderDenied. I6 fixed message for upstream HTTP errors. I7 constant-time binding compare. I8 metadata_claims allow-list. I9 https-only endpoints by default.

Auth core / files / REST (ras-auth-core, ras-file-core, ras-file-macro, ras-rest-macro)

  • A1 weak CSRF modes renamed dangerous_* with deprecated aliases and startup warning. A2 AuthError no longer Serialize; Display no longer lists held grants. F1 sanitize_filename + RFC 5987 filename*. F2 generic bodies for extractor rejections, detail sanitized (sanitize_log_detail) before logging.

Version bumps

Crate To
ras-auth-core, ras-identity-local, ras-identity-oauth2, ras-jsonrpc-bidirectional-server, ras-jsonrpc-bidirectional-client 0.3.0
ras-identity-session 0.4.0
ras-jsonrpc-bidirectional-macro, ras-file-core, ras-file-macro 0.2.1
ras-rest-macro 0.3.1

Test plan

  • cargo test --workspace: 922 passed, 0 failed
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo fmt --all --check clean
  • cargo deny check advisories ok (yanked chacha20 lock entry refreshed)
  • WASM client builds (--features wasm --target wasm32-unknown-unknown)
  • Mutation-checked: W2 transport limit, revocation egress gate, add/remove interleaving, custom-manager limits, checked subscribe — each test fails with its fix removed

Not addressed: lru / paste advisories come only from ratatui in the example TUI (not a published crate); moving it to ratatui 0.30 is out of scope here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NZEonxsxS45oAEC7bUTfnL

JedimEmO and others added 8 commits September 5, 2026 08:57
WebSocket (ras-jsonrpc-bidirectional-server/-macro/-client):
- W1 re-authorize held subscriptions on credential re-validation; add
  PermissionChangePolicy (DropSubscriptions | Close); mirror context
  subscriptions into the manager's topic index
- W2 enforce max_message_size/max_frame_size at the transport
- W3 SubscriptionLimits (per message, per connection, topic length)
- W4 KeepaliveConfig (server ping + idle timeout)
- W5 route WITH_PERMISSIONS checks through AuthProvider::check_permissions
- C1 remove AuthConfig::JwtParams (token in URL); C2 percent-encode
  CustomParams; browser auth via ras-jsonrpc + token.<jwt> subprotocols,
  server selects ras-jsonrpc so the token is never echoed

Sessions (ras-identity-session):
- S1 drop inline write-locked cleanup from verify/begin_session
- S2 iss/aud required by default (allow_unscoped_tokens opt-out)
- S3 secret entropy + placeholder substring checks
- S4 reject future iat/nbf; S5 max_sessions_per_user

Identity (ras-identity-local, ras-identity-oauth2):
- I1 skip_serializing password_hash / client_secret
- I2 redact LocalAuthPayload Debug; I4 Argon2 on spawn_blocking with
  password length cap
- I3 evict oldest pending OAuth2 flow instead of refusing (login lockout)
- I5 optional callback code, fixed ProviderDenied error; I6 fixed message
  for upstream HTTP errors; I7 constant-time binding compare
- I8 metadata_claims allow-list; I9 https-only endpoints by default

Auth core / files / REST (ras-auth-core, ras-file-core/-macro, ras-rest-macro):
- A1 rename weak CSRF modes to dangerous_*, deprecate old names, warn
- A2 drop Serialize from AuthError; Display no longer lists held grants
- F1 sanitize_filename + RFC 5987 filename* in attachment()
- F2 generic bodies for axum path/query/multipart rejections

Version bumps: ras-auth-core 0.3.0, ras-identity-session 0.4.0,
ras-identity-local 0.3.0, ras-identity-oauth2 0.3.0,
ras-jsonrpc-bidirectional-server 0.3.0, -client 0.3.0, -macro 0.2.1,
ras-file-core 0.2.1, ras-file-macro 0.2.1, ras-rest-macro 0.3.1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZEonxsxS45oAEC7bUTfnL
… test

Raw-socket test sends a frame header announcing an oversized payload without
the body: the transport limit rejects it immediately, whereas a post-buffer
check would wait for the payload. Verified to fail with the upgrade limits
removed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZEonxsxS45oAEC7bUTfnL
…anitizing

Second review pass on the WebSocket and REST hardening:
- egress gate: topic-routed messages are tagged (OutboundMessage) and
  re-checked against the connection's subscriptions right before the socket
  write, so a broadcast that raced re-authorization is dropped
- connection cap enforced with a semaphore permit held for the connection's
  lifetime; bounded default of 10 000; generated builder exposes every knob
- global subscription cap via ConnectionManager::total_subscription_count
- zero revalidation/keepalive durations no longer panic tokio intervals
- extractor rejection text sanitized (control chars, 256-byte cap) before
  it reaches tracing in generated REST and file handlers
- chacha20 lock entry refreshed off the yanked 0.10.1

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZEonxsxS45oAEC7bUTfnL
- DefaultConnectionManager::add_subscription enforces topic length,
  per-connection and global caps itself (atomic counter reserve); handler
  rolls back anything the manager refuses, so custom handlers cannot exceed
- add/remove_subscription hold the connection entry guard across both the
  connection state and topic index updates: no stale index entries
- WebSocketService::connection_permits is required; advisory admission
  fallback removed
- sanitize_log_detail counts the ellipsis inside the 256-byte budget
- tests: concurrent global-cap admission, 500-round add/remove interleaving
  (fails against the old ordering), greedy custom handler rollback

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZEonxsxS45oAEC7bUTfnL
…he manager

The service owns a SubscriptionAccounting counter shared by all of its
connections (WebSocketService::subscription_accounting, required). After
every handle_subscribe the handler reconciles the context against topic
length, the per-connection cap and the global cap (atomic reserve), and
only accepted topics reach the manager. Slots are released on unsubscribe,
re-validation drops and disconnect.

Integration test plugs a fully permissive custom ConnectionManager and a
greedy handler into build_with_manager; fails with the server-side check
removed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZEonxsxS45oAEC7bUTfnL
…on path

The context carries the service's SubscriptionPolicy (limits, shared
accounting, manager). subscribe() enforces topic length and the
per-connection cap under the write guard, reserves a global slot
atomically and mirrors into the manager, returning an error on refusal;
unsubscribe() releases both. The info field is private, so no unchecked
path remains: subscriptions from on_connect or handle_request are limited
and counted, and teardown releases only what was reserved (no underflow).

Removes the handler-level reconciliation. Regression test subscribes
greedily from on_connect and handle_subscribe; fails against an unchecked
subscribe.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZEonxsxS45oAEC7bUTfnL
Notify::notify_waiters drops wake-ups for tasks not yet parked, which made
the atomic-admission test flaky on slow CI runners. Use a watch channel so a
late waiter still observes the release.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZEonxsxS45oAEC7bUTfnL
…ts in tests

- Test job runs cargo-nextest (per-test process isolation, per-test
  timings, JUnit artifact); retries disabled via .config/nextest.toml so a
  flaky test fails instead of hiding; 2 min per-test hang cap (3 in CI)
- Coverage job runs llvm-cov via nextest for HEAD and the merge-base on
  PRs, posts a sticky per-crate table with deltas and the files that
  dropped most (.github/scripts/coverage_summary.py); no external service
- Weekly / on-demand flake-detector workflow runs the suite 5x at three
  parallelism levels
- Two tests replaced bounded sleep-polls with 10 s deadline waits
- README documents nextest and coverage

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZEonxsxS45oAEC7bUTfnL
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

Test coverage (lines)

Total: 92.71% (+0.97 vs base) — 18464/19915 lines

Crate Lines Coverage Δ
crates/core/ras-auth-core 1081/1162 93.03% +0.98
crates/core/ras-identity-core 113/113 100.00% ±0.00
crates/core/ras-observability-core 97/97 100.00% ±0.00
crates/core/ras-transport-core 497/534 93.07% ±0.00
crates/core/ras-version-core 90/90 100.00% ±0.00
crates/identity/ras-identity-local 544/591 92.05% +0.36
crates/identity/ras-identity-oauth2 1717/1761 97.50% +0.42
crates/identity/ras-identity-session 929/992 93.65% +3.50
crates/observability/ras-observability-otel 115/115 100.00% ±0.00
crates/rest/ras-file-core 281/360 78.06% +15.34
crates/rest/ras-file-macro 1154/1291 89.39% ±0.00
crates/rest/ras-rest-core 153/153 100.00% ±0.00
crates/rest/ras-rest-macro 1498/1626 92.13% +0.19
crates/rpc/bidirectional/ras-jsonrpc-bidirectional-client 1513/1694 89.32% -0.11
crates/rpc/bidirectional/ras-jsonrpc-bidirectional-macro 453/494 91.70% ±0.00
crates/rpc/bidirectional/ras-jsonrpc-bidirectional-server 2745/2974 92.30% +5.03
crates/rpc/bidirectional/ras-jsonrpc-bidirectional-types 505/537 94.04% -0.35
crates/rpc/ras-jsonrpc-core 92/97 94.85% -0.10
crates/rpc/ras-jsonrpc-macro 916/1001 91.51% ±0.00
crates/rpc/ras-jsonrpc-types 168/168 100.00% ±0.00
crates/specs/ras-openrpc-types 3672/3857 95.20% ±0.00
crates/specs/ras-permission-manifest 131/208 62.98% ±0.00
Files with coverage drops ≥ 1 point
File Coverage Δ
rust-api-stack/crates/rpc/bidirectional/ras-jsonrpc-bidirectional-server/src/connection.rs 93.10% -6.22
rust-api-stack/crates/identity/ras-identity-oauth2/src/state.rs 95.83% -1.98
rust-api-stack/crates/identity/ras-identity-oauth2/src/client.rs 96.93% -1.03

@JedimEmO
JedimEmO merged commit a2943d2 into master Sep 5, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant