fix: gate the oauth callback, read every getmeas page, clear advisories (v0.1.3) #6

Merged
jlxq0 merged 4 commits from fix/t_dbf5d5af-audit into main 2026-10-24 02:46:13 +00:00
Owner

Fixes the findings from read-only audit t_b81660de (on 0debb77). Card: t_dbf5d5af. Releases as v0.1.3.

What changed

Dependencies and CI

  • cargo audit reported nine advisories on main. Six of them are on the outbound TLS path to Withings: rustls (four), rustls-webpki and webpki-roots. Bumped rustls 0.23.46, rustls-webpki 0.103.16, webpki-roots 1.0.11, quinn-proto 0.11.19 (lockfile only) and tokio 1.54.1. cargo audit is now clean.
  • ci.yml had no schedule, so a new advisory never turned a run red. It now runs cargo daily. docker is skipped on a schedule, because a scheduled run's ref is main, which is the single :buildcache writer.

/oauth/callback

  • A live probe got 403: the callback is on, guarded only by a static state that appears in every consent URL. New oauth_gate:
    • open for WITHINGS_MCP_OAUTH_WINDOW_SECS after startup (default 600, at most 3600);
    • closes for good on the first adopted authorisation;
    • closes after five wrong states; a missing state isn't counted;
    • every closed case returns the same 404 as "not configured".
  • The exchange slot is a Ticket released on drop, and the exchange runs in a spawned task, so a browser that disconnects mid-exchange can't leave the gate InFlight.
  • The state is compared by digest via ExpectedToken, so its length no longer leaks.
  • A state shorter than 32 characters gets a startup warning. The log line carries only the minimum length.

Privacy

  • TraceLayer's default span recorded the whole URI at DEBUG, which for the callback includes the authorisation code and the state. The span now records the path only.
  • list_measurements now returns only the requested types, so a reading outside the catalogue never reaches a caller even if Withings ignores meastypes.

Reads

  • getmeas more/offset were ignored, so a long range returned its first page with truncated: false.
  • Both reads now follow up to 20 pages and stop on an offset that doesn't advance. They report complete, and the warning says why paging stopped.
  • The page order is unmeasured, and the docs say so.
  • A start after end is invalid_params before any Withings call. Previously Withings answered it with an internal 503.
  • serverInfo reported rmcp and rmcp's version; it now reports withings-mcp 0.1.3.

Metrics

  • withings_mcp_withings_failures_total{stage,code}, with closed label sets.
  • withings_mcp_reauthorisation_required, also set at startup when there is no credential. /health stays a pure liveness probe.

Verification

  • Toolchains match CI: rustc 1.93.0 for the build floor, clippy 1.98.0.
    • cargo fmt --all --check passes.
    • cargo clippy --all-targets --all-features --locked -- -D warnings passes.
    • cargo test --all-features --locked: 117 passed, up from 97.
    • cargo audit and cargo deny check bans licenses sources pass.
    • cargo +1.93.0 check --all-features --locked passes.
  • Mutation checks: reverting each of 12 fixes makes its new test fail. The 12 are: first page only, no page cap, ticket released only on adopt, exchange not spawned, missing state counted, adopt does not close, no window, no lockout, default span, rmcp server info, no type filter, no start>end check.
  • An independent review of b5a2371 found no blockers. Two should-fix items (a stuck InFlight state on cancel, and unmeasured page order in the docs) and several notes were addressed in a93d197 and fd63366. The review then approved head 1f8b4aa.

Not covered here

  • Live proof that pagination works against Withings needs an authenticated read of a range long enough to page.
  • Deploying (the platform manifest digest) and an authenticated client read are separate steps.
  • Nothing here writes to Withings. All test values are invented.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EQ1LZDFTafzEcUuC2A526L

Fixes the findings from read-only audit t_b81660de (on `0debb77`). Card: t_dbf5d5af. Releases as v0.1.3. ## What changed **Dependencies and CI** - `cargo audit` reported nine advisories on `main`. Six of them are on the outbound TLS path to Withings: rustls (four), rustls-webpki and webpki-roots. Bumped rustls 0.23.46, rustls-webpki 0.103.16, webpki-roots 1.0.11, quinn-proto 0.11.19 (lockfile only) and tokio 1.54.1. `cargo audit` is now clean. - `ci.yml` had no schedule, so a new advisory never turned a run red. It now runs `cargo` daily. `docker` is skipped on a schedule, because a scheduled run's ref is `main`, which is the single `:buildcache` writer. **`/oauth/callback`** - A live probe got `403`: the callback is on, guarded only by a static `state` that appears in every consent URL. New `oauth_gate`: - open for `WITHINGS_MCP_OAUTH_WINDOW_SECS` after startup (default 600, at most 3600); - closes for good on the first adopted authorisation; - closes after five wrong states; a missing state isn't counted; - every closed case returns the same `404` as "not configured". - The exchange slot is a `Ticket` released on drop, and the exchange runs in a spawned task, so a browser that disconnects mid-exchange can't leave the gate `InFlight`. - The state is compared by digest via `ExpectedToken`, so its length no longer leaks. - A state shorter than 32 characters gets a startup warning. The log line carries only the minimum length. **Privacy** - `TraceLayer`'s default span recorded the whole URI at `DEBUG`, which for the callback includes the authorisation code and the state. The span now records the path only. - `list_measurements` now returns only the requested types, so a reading outside the catalogue never reaches a caller even if Withings ignores `meastypes`. **Reads** - `getmeas` `more`/`offset` were ignored, so a long range returned its first page with `truncated: false`. - Both reads now follow up to 20 pages and stop on an offset that doesn't advance. They report `complete`, and the warning says why paging stopped. - The page order is unmeasured, and the docs say so. - A `start` after `end` is `invalid_params` before any Withings call. Previously Withings answered it with an internal `503`. - `serverInfo` reported `rmcp` and rmcp's version; it now reports `withings-mcp` 0.1.3. **Metrics** - `withings_mcp_withings_failures_total{stage,code}`, with closed label sets. - `withings_mcp_reauthorisation_required`, also set at startup when there is no credential. `/health` stays a pure liveness probe. ## Verification - Toolchains match CI: rustc 1.93.0 for the build floor, clippy 1.98.0. - `cargo fmt --all --check` passes. - `cargo clippy --all-targets --all-features --locked -- -D warnings` passes. - `cargo test --all-features --locked`: 117 passed, up from 97. - `cargo audit` and `cargo deny check bans licenses sources` pass. - `cargo +1.93.0 check --all-features --locked` passes. - Mutation checks: reverting each of 12 fixes makes its new test fail. The 12 are: first page only, no page cap, ticket released only on adopt, exchange not spawned, missing state counted, adopt does not close, no window, no lockout, default span, rmcp server info, no type filter, no start>end check. - An independent review of `b5a2371` found no blockers. Two should-fix items (a stuck `InFlight` state on cancel, and unmeasured page order in the docs) and several notes were addressed in `a93d197` and `fd63366`. The review then approved head `1f8b4aa`. ## Not covered here - Live proof that pagination works against Withings needs an authenticated read of a range long enough to page. - Deploying (the platform manifest digest) and an authenticated client read are separate steps. - Nothing here writes to Withings. All test values are invented. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01EQ1LZDFTafzEcUuC2A526L
Audit t_b81660de against 0debb77 found:

- cargo audit red with nine advisories, four of them in rustls and two
  more in rustls-webpki/webpki-roots on the outbound TLS path. Bumped
  rustls 0.23.46, rustls-webpki 0.103.16, webpki-roots 1.0.11,
  quinn-proto 0.11.19 (lockfile only) and tokio 1.54.1.
- ci.yml had no schedule, so the advisories never reached a red run.
  It now runs `cargo` daily; `docker` is skipped on a schedule because
  that ref is the one `:buildcache` writer.
- /oauth/callback answered 403 in production: on, and guarded only by a
  static state that sits in every consent URL. oauth_gate keeps it open
  for WITHINGS_MCP_OAUTH_WINDOW_SECS (default 600) after startup, closes
  it on the first adopted authorisation and after five wrong states, and
  answers every closed case with the "not enabled" 404. A state shorter
  than 32 characters is warned about at startup, by length only.
- TraceLayer's default span records the whole URI at DEBUG, which for
  the callback is the authorisation code and the state. The span now
  records the path.
- getmeas `more`/`offset` were ignored, so a long range returned its
  first page with truncated=false. Both reads follow up to 20 pages,
  stop on an offset that does not advance, and carry `complete`.
- list_measurements returned whatever Withings sent; it now keeps only
  the requested types, so a reading outside the catalogue never reaches
  a caller even if Withings ignores `meastypes`.
- start > end reached Withings and came back as an internal 503; it is
  now invalid_params before any call.
- serverInfo reported rmcp and rmcp's version; it reports withings-mcp
  and this crate's version.
- New metrics withings_mcp_withings_failures_total{stage,code} and
  withings_mcp_reauthorisation_required, since /health must stay a pure
  liveness probe.

Tests use invented values only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EQ1LZDFTafzEcUuC2A526L
Independent review of b5a2371:

- The exchange slot was released only when the handler returned, so a
  browser disconnecting mid-exchange left the gate InFlight: 409 past
  the window until a restart. The slot is now a Ticket released on
  drop, and the exchange runs in a spawned task that owns it, so an
  abandoned request still finishes adopting and closes the callback.
- A request with no state counted as a guess, so any five scanner hits
  closed the callback; it is now a 403 that is not counted.
- The state is compared by digest via ExpectedToken, like the bearer,
  so its length no longer leaks through the comparison.
- WITHINGS_MCP_OAUTH_WINDOW_SECS is capped at 3600.
- withings_mcp_reauthorisation_required is set at startup when there
  is no credential, not only after the first tool call.
- The partial-read warning names why it stopped; the 20-page cap has a
  test with an always-advancing offset.
- README and AGENTS.md say page order is unmeasured, so complete=false
  means the newest readings may be missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EQ1LZDFTafzEcUuC2A526L
Review notes on a93d197: an_endless_range_stops_at_the_page_limit hung
rather than failed with the cap removed, and a JoinError on the callback
completion left no log line. The log carries only is_panic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EQ1LZDFTafzEcUuC2A526L
chore(release): 0.1.3
All checks were successful
CI / cargo (pull_request) Successful in 2m55s
CI / docker (pull_request) Successful in 55s
1f8b4aae03
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EQ1LZDFTafzEcUuC2A526L
jlxq0 merged commit ca2c30d78a into main 2026-10-24 02:46:13 +00:00
jlxq0 deleted branch fix/t_dbf5d5af-audit 2026-10-24 02:46:14 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jlxq0/withings-mcp!6
No description provided.