Skip to content

Fix unauthenticated probes reusing authenticated transport state - #2

Open
sanjayy0612 wants to merge 6 commits into
hackwither:mainfrom
sanjayy0612:main
Open

Fix unauthenticated probes reusing authenticated transport state#2
sanjayy0612 wants to merge 6 commits into
hackwither:mainfrom
sanjayy0612:main

Conversation

@sanjayy0612

Copy link
Copy Markdown

Summary

Unauthenticated MCP exposure probes were reusing the session created with
--auth-header. This caused false positives for persistent transports,
especially WebSocket.

Root cause

WithNoAuth() can omit the Authorization header from an individual HTTP
request, but it cannot make an already-authenticated WebSocket connection
anonymous. WebSocket authentication happens during the handshake.

As a result, mcp-unauth-tools-list could receive tools over an authenticated
WebSocket and incorrectly report them as accessible without authentication.

The same session-state problem could affect legacy SSE sessions and inherited
MCP session IDs.

Changes

  • Add transport-aware creation of fresh anonymous MCP sessions.
  • Use a separate unauthenticated session for:
    • mcp-unauth-tools-list
    • mcp-resources-prompts-exposure
    • OAuth Bearer-challenge detection
  • Preserve the original authenticated session for authenticated enumeration and
    other probes.
  • Skip anonymous checks when a session cannot provide a separate anonymous
    connection.
  • Keep the existing no-tool-invocation safety boundary unchanged.

Tests

Added a regression test using an authentication-required WebSocket MCP server.

The test verifies that:

  1. Authenticated WebSocket initialization succeeds.
  2. Authenticated tools/list succeeds.
  3. The anonymous exposure probe creates a separate unauthenticated connection.
  4. The authenticated response is not incorrectly reported as anonymous exposure.

Verification

git diff --check passes.

The Go verification commands could not be executed in the development
environment because go and gofmt were not available on PATH.

Requested commands:

  • gofmt -l .
  • go vet ./...
  • go test ./...
  • go build ./...

Copilot AI lite review requested due to automatic review settings August 16, 2026 12:49

Copilot AI 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.

Pull request overview

Fixes false-positive “unauthenticated exposure” findings caused by reusing authenticated transport state (especially for persistent transports like WebSocket/SSE) by introducing transport-aware creation of fresh anonymous sessions and updating the relevant probes to use them.

Changes:

  • Add AnonymousSession() support to MCP session implementations (streamable HTTP, SSE legacy, WebSocket) to create fresh unauthenticated connections without inherited session state.
  • Update unauthenticated exposure probes (tools list, resources/prompts exposure, OAuth bearer-challenge detection) to use separate anonymous sessions instead of WithNoAuth().
  • Add a WebSocket regression test ensuring unauthenticated probing does not reuse an authenticated WebSocket connection.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
internal/probe/mcp/session.go Add timeout retention and an anonymous streamable-HTTP session constructor.
internal/probe/mcp/session_ws.go Add timeout retention and anonymous WebSocket session constructor.
internal/probe/mcp/session_sse.go Add timeout retention and anonymous legacy-SSE session constructor.
internal/probe/mcp/checks.go Introduce anonymous-session helper and update unauthenticated probes to use fresh sessions.
internal/probe/mcp/checks_test.go Add regression test for authenticated WebSocket not being reused by unauth probes.
Suppressed comments (1)

internal/probe/mcp/checks.go:1112

  • resourcesPromptsExposureProbe creates a fresh anonymous session, which may allocate a new persistent WebSocket connection. The session isn't closed after the probe completes, potentially leaking connections/goroutines. Defer Close() when the returned session implements io.Closer.
func (p *resourcesPromptsExposureProbe) Run(ctx context.Context, s probe.Session, r *report.Report) error {
	unauthSess, err := anonymousSession(s)
	if err != nil {
		return nil
	}
	for _, method := range []string{"resources/list", "prompts/list"} {
		raw, err := unauthSess.Do(ctx, method, map[string]any{})

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 3 to 15
@@ -9,9 +10,64 @@ import (
"time"

"github.com/hackwither/reap/internal/probe"
"github.com/hackwither/reap/internal/probe/common"
"github.com/hackwither/reap/internal/report"
)
Comment thread internal/probe/mcp/checks_test.go Outdated
Comment on lines +24 to +27
key := r.Header.Get("Sec-WebSocket-Key")
hj, _ := w.(http.Hijacker)
conn, buf, err := hj.Hijack()
if err != nil { return }
Comment on lines +76 to 82
// AnonymousSession establishes a separate WebSocket handshake without auth.
// WithNoAuth cannot change the credentials of an already-upgraded socket.
func (s *WSSession) AnonymousSession() (probe.Session, error) {
return NewWSSession(s.targetURL, "", s.timeout)
}

func (s *WSSession) TargetURL() string { return s.targetURL }
Comment on lines 749 to +756
func (p *unauthToolsListProbe) Run(ctx context.Context, s probe.Session, r *report.Report) error {
// Re-issue tools/list explicitly WITHOUT the auth header, regardless of
// whether the initial handshake used one. This answers the specific
// question: "can an anonymous caller enumerate tools?"
raw, err := s.Do(ctx, "tools/list", map[string]any{}, probe.WithNoAuth())
// Use a fresh connection so no Authorization header, MCP session ID, or
// authenticated persistent transport state can affect this observation.
unauthSess, err := anonymousSession(s)
if err != nil {
return nil
}
raw, err := unauthSess.Do(ctx, "tools/list", map[string]any{})
@sanjayy0612

Copy link
Copy Markdown
Author

Addressed locally: the missing JSON import, safer WebSocket test-server error handling, and cleanup for persistent anonymous WebSocket/SSE sessions. go test ./... passes

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.

2 participants