Skip to content

Guard OAuth endpoints by default, exempting only operator-declared local servers - #172

Merged
M3gA-Mind merged 2 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/sr022-oauth-guard-default-on
Nov 11, 2026
Merged

M3gA-Mind merged 2 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/sr022-oauth-guard-default-on

Conversation

@M3gA-Mind

Copy link
Copy Markdown
Contributor

Summary

The OAuth authorization, registration and token endpoints come from the server being signed in to, and the flow POSTs to them from the host. The guard that refuses non-https and internal addresses (guard_endpoint) was opt-in through OAuthFlow::require_public_endpoints, and the module never opted in. So a hostile registry-installed server could aim the host at a loopback service or the cloud metadata address (169.254.169.254). Token refreshes on connect also went through the free refresh_if_expired, which never re-checks the stored token endpoint.

Secure by default:

  • McpRegistry turns the guard on. The probe of the server itself and every endpoint it advertises must be https and resolve to public addresses.
  • The decision is made per server. McpClientConfig.local_oauth_origins lists the origins of servers the operator declares local, such as a loopback development server. Those may use loopback, private or plain-http endpoints, and each exemption is logged at warn. Origins compare by scheme, lowercased host and explicit port.
  • Connections refresh through OAuthFlow::refresh, which re-checks the stored token endpoint before posting to it.

Related issue

None filed. Found by reading the OAuth flow.

API or behavior changes

Contract minor bump: tinymcp-bus → 1.30. It lands after #164 (1.29). A host built on this bus requires a module at 1.30.

  • McpClientConfig.local_oauth_origins: Vec<String> (serde default, omitted when empty). Empty means every server is guarded.
  • Behaviour: signing in to, detecting or refreshing a server on loopback, a private address or plain http is now refused unless its origin is listed. A host whose users sign in to local development servers must list them. OpenHuman's static and registry servers are remote, so nothing changes there by default.
  • McpRegistry::with_local_oauth_origins and OAuthFlow::with_local_origins. A bare OAuthFlow::new stays unguarded for library callers that check endpoints themselves.

Validation

On our Linux CI runner (toolchain 1.99.1), on this branch from main e098e53:

  • cargo fmt --all -- --check: clean
  • cargo clippy --locked --all-targets --all-features -- -D warnings: 0 errors (main: 0)
  • cargo build --locked --all-targets --all-features; RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features: clean
  • registry::oauth 106/0, registry::ops 83/0, registry::connections 55/0, including the three new tests.
  • cargo test --locked --all-features --workspace: 3 failures, all timing flakes in registry::boot and tinybus_module::directories that main also fails in full-suite runs (main had 7 there). No failure is new on this branch, so the default-on guard broke no existing test.
  • Red controls:
    • With main's unguarded default (OAuthFlow::new(proxy) without require_public_endpoints), a_loopback_server_is_refused_by_the_oauth_guard_unless_declared_local fails: detection succeeds with a token.
    • Ignoring the local list makes the declared-local server refused (token endpoint refused: it resolves to a disallowed address).

Tests

  • a_loopback_server_is_refused_by_the_oauth_guard_unless_declared_local: on a registry built the normal way, detect_auth and oauth_begin refuse a plain-http loopback server, and the same server is probed once declared local.
  • internal_endpoints_are_refused_for_a_remote_and_allowed_for_a_declared_local_server: for a remote, token endpoints at 127.0.0.1, 169.254.169.254 or over http are refused, and so is a registration set at 169.254.169.254. A declared-local server keeps the unguarded clients.
  • origins_compare_by_scheme_host_and_explicit_port.
  • The existing registry tests that sign in to loopback stand-ins declare them local, as an operator would.
  • Red controls: main's unguarded default fails the loopback test; ignoring the local list fails the declared-local test.

Documentation

The tinymcp-bus README contract note, the endpoint_guard module doc, and the field's rustdoc.

Checklist

  • The change is focused on one logical change
  • No new #[allow(...)], #[ignore], or relaxed lints
  • No secrets, tokens, or .env contents in the diff or the description

…cal servers

The OAuth authorization, registration and token endpoints come from the
server being signed in to, and the flow POSTs to them from the host. The
guard that refuses non-https and internal addresses (guard_endpoint) was
opt-in through OAuthFlow::require_public_endpoints, and the module never
opted in. A hostile registry-installed server could aim the host at a
loopback service or the cloud metadata address. Token refreshes on
connect went through the free refresh_if_expired, which never re-checks
the stored token endpoint.

- McpRegistry turns the guard on. The probe of the server itself and every
  advertised endpoint must be https and resolve to public addresses.
- The guard is decided per server. McpClientConfig.local_oauth_origins
  (contract 1.30, empty by default) lists origins of servers the operator
  declares local, such as a loopback development server. Those may use
  loopback, private or plain-http endpoints, and each exemption is logged.
  Origins compare by scheme, lowercased host and explicit port.
- Connections refresh through OAuthFlow::refresh, which re-checks the
  stored token endpoint before posting to it.

Tests: a plain-http loopback server is refused by detect and begin on a
registry built the normal way, and probed once declared local. For a
remote, token endpoints at 127.0.0.1, 169.254.169.254 or over http are
refused, as is a registration set at 169.254.169.254; a declared-local
server keeps the unguarded clients. The registry tests that sign in to
loopback stand-ins declare them local, as an operator would.
@coderabbitai

coderabbitai Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 0b5f896a-333d-4738-994c-b6304b1a1dc0
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@tinysweeper

tinysweeper Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Tiny Sweeper review

Reviewed head: e62623c66e099c1f63c2480ff589469a0af1ddf1

Findings

No active actionable findings.

Advisory notes

  • medium: Pin local_oauth_origins in the wire-form serde test (crates/tinymcp-bus/src/config/types.rs:70)
    Problem: The repository rule is: "Pin every payload type's serde representation in a unit test, since that representation is the wire form." The existing pin, `configured_open_keeps_directo
    Suggested fix:
    No concrete fix was supplied.

Verdict: Ready for maintainer review · Priority: none
Coverage: Reviewed 11 of 11 files.

Diagnostics and supporting details

Full summary

Pull request purpose (title): Guard OAuth endpoints by default, exempting only operator-declared local servers. Purpose (whole-pull-request title): "Guard OAuth endpoints by default, exempting only operator-declared local servers." The pull request makes the OAuth endpoint guard secure by default. The authorization, registration and token endpoints a server advertises are requested from the host, so the previously opt-in guard (OAuthFlow::require_public_endpoints) is now enabled inside McpRegistry and the probe, registration and token clients all consult a per-server decision. A new McpClientConfig.local_oauth_origins list, empty by default, lets an operator declare servers as local (for example loopback development servers); those are exempt and each exemption is logged. Origins compare by scheme, lowercased host and explicit port, and malformed entries are ignored with a warning. Connect-time token refresh no longer uses the unguarded free refresh helper but goes through OAuthFlow::refresh, which re-checks the stored token endpoint. The tinymcp-bus contract moves to 1.30 with a README note and the module wiring passes the config list into the registry. The critique lane reports two findings in its summary, but its findings array is empty in the supplied evidence, so no specific findings can be cited. The security lane answered over the 10 code files with no findings; the README was not security-reviewed as prose. The tests lane answered that the guard behavior is covered but that the new config field itself is not: its wire form is unpinned and its module-level consumer is untested.

Review snapshot

Change surface Files Review signal Count
Production 7 Active findings 0
Tests 3 Noted findings 1
Documentation 1 Resolved findings 0
Configuration 0 Pending checks/questions 0

Completeness: Complete
Test assessment: Test coverage is assessed from changed tests and lane evidence; execution is not claimed without trusted check data.

What changed

The registry now builds its OAuthFlow with require_public_endpoints enabled and gained with_local_oauth_origins, fed at module wiring time from McpClientConfig.local_oauth_origins. OAuthFlow carries a local_origins list plus with_local_origins, an origin_of normalizer (scheme, lowercased host, explicit port) and a guards() predicate used by registration_client, probe_client and token_client, which now take a server_url. Connect-time refresh switched from the free refresh_if_expired to OAuthFlow::refresh. The tinymcp-bus contract bumped to 1.30 and the README, endpoint_guard module doc and field rustdoc were updated. New and adjusted tests cover the default-on guard, the per-server exemption and origin normalization, and existing registry tests that use loopback stand-ins now declare them local. No trusted push delta was available for the latest head, so this describes the whole pull request rather than a push-to-push change.

Features

  • Added — McpClientConfig.local_oauth_origins: Adds a serde-defaulted, empty-by-default list of operator-declared local origins; empty keeps every server guarded, and its absence on the wire stays compatible for existing host configs. (crates/tinymcp-bus/src/config/types.rs pub struct McpClientConfig {, crates/tinymcp-bus/src/config/types.rs impl Default for McpClientConfig {)
  • Added — Per-server exemption and origin normalization in OAuthFlow: The guard decision becomes per server: matching local origins skip the endpoint checks and log a warning, malformed entries are ignored with a warning rather than matching, and scheme/host/port normalization makes equivalent origins compare equal. (crates/tinymcp/src/registry/oauth/flow.rs impl OAuthFlow {, crates/tinymcp/src/registry/oauth/flow.rs pub struct OAuthFlow {)
  • Modified — Connect-time refresh goes through OAuthFlow::refresh: A stored token endpoint is re-checked against the guard before the host posts to it when a session connects, instead of using the unguarded free refresh helper. (crates/tinymcp/src/registry/connections/types.rs impl Connections {, crates/tinymcp/src/registry/connections/types.rs use super::dial::{build_http_auth, credential_safe_dial_url, resolve_final_url};)
  • Modified — Contract version bump to 1.30 with module wiring: Hosts on this bus require a module at 1.30, the module runtime passes the configured local origins into the registry, and the bus README documents the field and the default-on guard. (crates/tinymcp-bus/src/version/mod.rs, crates/tinymcp/src/tinybus_module/service.rs impl McpService {, crates/tinymcp-bus/README.md so a host can tell a tool that only reads from one that changes something)

Tests

  • modified test — The shipped contract version is pinned to 1.30.: Keeps the version constant check in sync with the bump. (crates/tinymcp-bus/src/version/mod_tests.rs use super::{CONTRACT_VERSION, binds, is_compatible};)

Before merge

None.

Agent review details

critique

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: Reviewed 11 files; 2 findings.

security

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: Reviewed 10 files; 0 findings. 1 file was not security-reviewed: crates/tinymcp-bus/README.md (prose or tabular data).

tests

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: The guard-on-by-default behaviour is genuinely covered at the flow and registry levels — internal endpoints refused for a remote, exemption for a declared-local origin, and origin normalisation are all pinned by tests that would fail if the logic regressed. What is not covered is the new `McpClientConfig::local_oauth_origins` field itself: its wire form is unpinned and its only module-level consumer is untested.

commits

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: Commits lane answered that no sensitive content appears in the committed files.
  • Lane summary: No sensitive content in committed files.

description

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: Description lane answered that the description matches the diff, including the default-on guard, the local-origins exemption, the switch to OAuthFlow::refresh, the 1.30 bump and the new tests; it also noted the probe path is skipped for a declared-local server, consistent with the stated exemption.
  • Lane summary: This change introduces a default-on OAuth endpoint guard that refuses non-https, loopback, private and link-local addresses, with an operator-declared local-origins exemption list. The design makes a sensible security tradeoff, the guard is applied consistently through the flow (including the refresh path that previously bypassed it), and the existing tests that touched loopback servers are properly updated to declare them local. I found no reportable defect.
Evidence and run details
  • Models: ladder/vectors-oai3, deepseek-v4.1-flash, deepseek-v4-flash
  • Spend: $0.060302
  • Completion usage: incomplete; some provider completion costs are unavailable.
  • Tokens: 1719859 input · 202833 output · 632064 cached · 675 embedding
  • Updated: 2026-11-11T12:58:09Z
Head State Pass summary
2a4de884e046 ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 2026-11-11T11:15:04Z)
e62623c66e09 ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 2026-11-11T12:58:09Z)

tinysweeper 0.1.2

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tinysweeper found nothing blocking. Approving.

Diagnostics

Total spend: $0.0174

@M3gA-Mind
M3gA-Mind merged commit 2286e78 into tinyhumansai:main Nov 11, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant