Skip to content

fix(http): refuse HTTPS→HTTP redirect downgrades - #3

Open
senamakel wants to merge 7 commits into
mainfrom
mcp-redirect-downgrade
Open

fix(http): refuse HTTPS→HTTP redirect downgrades#3
senamakel wants to merge 7 commits into
mainfrom
mcp-redirect-downgrade

Conversation

@senamakel

@senamakel senamakel commented Aug 22, 2026

Copy link
Copy Markdown
Member

Summary

The HTTP transport client followed every redirect up to MAX_REDIRECTS via
reqwest::redirect::Policy::limited. That policy follows an HTTPS→HTTP
downgrade without complaint, and reqwest only strips Authorization /
Cookie on a cross-origin hop — so a same-host downgrade carries a bearer
in cleartext, and any custom-header or query-parameter credential follows on
any hop. This is a CWE-319 (cleartext transmission of sensitive information)
gap: a passive network attacker who can inject a 3xx can move a credentialed
request to plaintext.

This replaces the limited policy with a custom one that:

  • follows vanity-URL redirects (servers are commonly published behind one),
  • caps the chain at MAX_REDIRECTS (unchanged behaviour), and
  • refuses an HTTPS→HTTP downgrade, surfacing it as a redirect error rather
    than a silent leak.

The decision is a pure function (redirect_decision) of the origin scheme,
target scheme, and hop count, so the rule is unit-testable without standing up
a redirect server.

Scope

This is the per-request client path (McpHttpClient). The dial-time
resolution path (registry::connections::dial::credential_safe_dial_url)
already refuses HTTPS→HTTP downgrades by falling back to the original URL —
this closes the complementary gap on the authenticated request path. The two
are now consistent: a downgrade is refused at resolution and at request time.

Public API / behavior changes

None to the public surface. A request that would have followed an
HTTPS→HTTP downgrade now errors instead of silently downgrading — the
intended behaviour.

Validation

The four contract commands, run from the repo root:

  • cargo fmt --all -- --check — clean

  • cargo clippy --all-targets --all-features -- -D warnings — clean on the
    changed code. (One pre-existing unknown-lint warning for
    clippy::unused_async_trait_impl in tinybus_module/service.rs surfaces
    only on a clippy newer than the @stable CI pins; it is untouched by this
    PR and present on main.)

  • cargo build --all-targets --all-features — green

  • cargo test --all-features — 661 lib + 4 + 149 + 10 + 18, all pass; 0
    failed. New tests:

    • follows_an_https_to_https_redirect
    • refuses_an_https_to_http_downgrade
    • does_not_refuse_a_plain_http_redirect_that_started_on_http
    • caps_the_redirect_chain_at_max_redirects

Related

Surfaced as a security finding on openhuman PR tinyhumansai/openhuman#5671
(MCP client/registry extraction into this crate).

Summary by CodeRabbit

  • Bug Fixes
    • Improved HTTP redirect handling for safer and more predictable navigation.
    • Prevented redirects that downgrade secure HTTPS connections to HTTP.
    • Added support for approved same-origin redirects while preserving authentication details.
    • Limited redirect chains to five steps to prevent excessive or looping redirects.

senamakel and others added 7 commits August 22, 2026 20:10
When the HTTP server returns a response without a Content-Type header, the transport now defaults to treating the body as plain text instead of failing. This improves compatibility with servers that omit the header for simple text responses.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
When the HTTP transport receives a request without a Content-Type header, the server now defaults to treating the body as JSON instead of failing. This improves compatibility with clients that omit the header when sending JSON payloads.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
When the HTTP transport receives a response without a Content-Type header, it now defaults to treating the body as plain text instead of failing. This improves robustness when interacting with servers that omit the header.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
When the HTTP transport receives a request without a Content-Type header, the server now defaults to treating the body as JSON instead of failing. This improves compatibility with clients that omit the header when sending JSON payloads.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
When the HTTP transport receives a request without a Content-Type header, the server now defaults to treating the body as JSON instead of returning an error. This improves compatibility with clients that omit the header while sending JSON payloads.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 686c69c8-e40d-4f50-ac5a-00b8f7cbfeec

📥 Commits

Reviewing files that changed from the base of the PR and between 2236b39 and b88cf60.

📒 Files selected for processing (2)
  • crates/tinymcp/src/transport/http/mod.rs
  • crates/tinymcp/src/transport/http/test.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The HTTP transport now uses a custom redirect policy. It follows up to five redirects, rejects HTTPS-to-HTTP downgrades, and tests secure, HTTP, downgrade, and excessive redirect scenarios.

Changes

HTTP redirect security

Layer / File(s) Summary
Redirect policy and client wiring
crates/tinymcp/src/transport/http/mod.rs
The HTTP client uses a custom policy. The policy permits allowed redirects, rejects HTTPS-to-HTTP transitions, and limits chains to five hops.
Redirect policy validation
crates/tinymcp/src/transport/http/test.rs
Tests cover HTTPS redirects, HTTP redirects, HTTPS-to-HTTP rejection, and redirect chains that exceed the limit.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to b88cf

The client now rejects HTTPS-to-HTTP redirects while retaining the existing redirect limit, preventing credential exposure over plaintext without changing the public API. No actionable merge-blocking risk remains; the PR is merge-ready after normal checks.

Sequence Diagram(s)

sequenceDiagram
  participant McpHttpClientBuilder
  participant reqwest HTTP client
  participant redirect policy
  participant redirect target

  McpHttpClientBuilder->>reqwest HTTP client: Configure custom redirect policy
  reqwest HTTP client->>redirect target: Send request
  redirect target-->>reqwest HTTP client: Return redirect
  reqwest HTTP client->>redirect policy: Evaluate target and hop count
  redirect policy-->>reqwest HTTP client: Follow redirect or return error
Loading

Poem

I’m a rabbit guarding the route,
HTTPS hops now stay safe throughout.
Five jumps, then rest in the hay,
Plaintext detours lose their way.
Hop, hop—secure paths all day!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: rejecting HTTPS-to-HTTP redirect downgrades.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 2 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@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.

$0.0000 · 0 in / 0 out · 248 embedded · openrouter/openai/text-embedding-3-small

@tinysweeper

tinysweeper Bot commented Aug 22, 2026

Copy link
Copy Markdown

How this change flows

1 changed behaviour across 2 relationships. 2 surrounding behaviours are shown (60 graph nodes walked). 50 further behaviours left out to keep the diagram readable.

flowchart LR
  n0["McpHttpClientBuilder<br/>changed"]:::changed
  n1["build"]:::impacted
  n2["builder"]:::impacted
  n1 -->|calls| n2
  n2 -->|uses| n0
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading

Green: changed behaviour. Grey: surrounding behaviour. Arrows name the call, use, implementation, or test relationship. Orange: has findings. Red: has a finding that blocks the merge.

tinysweeper 0.1.0

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