←Research
Researchvulnerability7 min read

Softeria ms-365-mcp-server PR #456 validates redirect_uri before Microsoft Entra forwarding

Softeria's ms-365-mcp-server forwarded client-supplied OAuth redirect_uri values to Microsoft Entra without local validation. PR #456 adds scheme checks, loopback-only HTTP defaults and an exact-match allowlist for hosted deployments.

Softeria ms-365-mcp-server PR #456 validates redirect_uri before Microsoft Entra forwarding

Softeria's ms-365-mcp-server forwarded a client-supplied OAuth redirect_uri from its HTTP-mode /authorize endpoint into the Microsoft Entra authorization URL before doing any local validation. I identified the issue, submitted PR #456 and the fix was merged on 9 May 2026. The change is included in version 0.107.1.

The vulnerability is a bounded but real CWE-601 open redirect risk in an OAuth flow. Microsoft Entra still validates redirect URIs against the application registration, so a correctly configured tenant blocks the attack upstream. The problem appears when a hosted deployment uses broad or wildcard reply-URL patterns. In that configuration, the MCP server becomes the unvalidated forwarding point between a phishing link and an attacker-controlled OAuth callback.

The vulnerable data flow

The affected path sits in src/server.ts, in the HTTP-mode /authorize handler. Around line 327 in the diff, the handler reads OAuth parameters from the incoming request:

const clientCodeChallengeMethod = url.searchParams.get('code_challenge_method');
const state = url.searchParams.get('state');

Before the fix, the handler then built the Microsoft authorization URL and forwarded supported parameters. The client-controlled redirect_uri value was part of that forwarding path. There was no local check for scheme, origin, loopback status or an operator-defined allowlist before the request left the server.

The attack chain is conventional OAuth abuse rather than an exotic MCP-specific primitive:

  1. An attacker sends a victim a link to the hosted MCP server's /authorize endpoint with redirect_uri=https://attacker.example.com/cb.
  2. The MCP server builds a Microsoft Entra authorization request containing that redirect_uri and redirects the victim onward.
  3. If the Entra app registration accepts the supplied URI because it is permissively configured, Microsoft returns the authorization code to the attacker's callback.
  4. The attacker exchanges the code through the token flow and gains access to the user's Microsoft 365 data within the granted scopes.

That final conditional matters. This is not a bypass of Entra's registered redirect URI checks. Microsoft documents redirect URI restrictions for the platform, and those checks remain the final enforcement point. The server-side bug is that ms-365-mcp-server added no defence-in-depth of its own, despite being the component that accepts the public /authorize request.

For local single-user use, this may never become exploitable. For hosted multi-user MCP deployments, it is a more plausible failure mode. Operators frequently widen redirect URI configuration while trying to support multiple clients, staging hosts or external callback URLs. Once that happens, the server's pass-through behaviour gives an attacker a clean place to inject the callback.

What PR #456 changed

The fix adds a new module, src/lib/redirect-uri-validation.ts, with two exported functions: parseAllowlist() and isAllowedRedirectUri().

The validation logic is intentionally small. It rejects empty values, malformed URLs and any scheme other than http: or https:. That blocks obvious dangerous schemes such as javascript:, data: and file: before they can be forwarded upstream.

if (url.protocol !== 'http:' && url.protocol !== 'https:') {
  return false;
}

When an explicit allowlist is configured, validation becomes exact-match only:

if (allowlist && allowlist.length > 0) {
  return allowlist.includes(value);
}

The allowlist comes from MS365_MCP_ALLOWED_REDIRECT_URIS, parsed as a comma-separated list. This is the production control. A hosted deployment can mirror the redirect URIs registered in Entra and reject everything else at the MCP server before Microsoft is involved.

When no allowlist is set, the default behaviour preserves local development compatibility:

  • https:// redirect URIs are allowed
  • http://localhost, http://127.0.0.1 and http://[::1] are allowed
  • arbitrary remote http:// origins are rejected

That default is a compromise. It does not claim to be a complete production policy. It keeps common local clients working while removing the weakest cases: dangerous schemes and non-loopback cleartext HTTP callbacks.

The new enforcement point

The important change in src/server.ts starts at line 328 in the diff. The handler now reads redirect_uri early and validates it before the Microsoft authorization URL is constructed:

const redirectUriParam = url.searchParams.get('redirect_uri');
if (redirectUriParam) {
  const allowlist = parseAllowlist(process.env.MS365_MCP_ALLOWED_REDIRECT_URIS);
  if (!isAllowedRedirectUri(redirectUriParam, allowlist)) {
    logger.warn('Rejected /authorize request with disallowed redirect_uri', {
      redirect_uri: redirectUriParam,
    });
    res.status(400).json({
      error: 'invalid_request',
      error_description: 'redirect_uri is not allowed',
    });
    return;
  }
}

The placement is what gives the fix its value. Rejected values receive a 400 invalid_request response and the function returns before any upstream redirect occurs. The log entry also gives operators something observable when probing or phishing attempts hit the endpoint.

The documentation change in docs/deployment.md is not decorative. It explains that Entra still has the final say, but the server now rejects obviously unsafe values first. It also states the operational recommendation plainly: production deployments should set MS365_MCP_ALLOWED_REDIRECT_URIS and use exact matches.

Tests and release outcome

The new test/redirect-uri-validation.test.ts file contains seven tests covering the security boundary:

CaseExpected result
javascript:alert(1)Rejected
data:text/html,<script>alert(1)</script>Rejected
malformed URLRejected
default https:// URIAllowed
default HTTP loopback URIAllowed
default remote http:// URIRejected
explicit allowlist mismatchRejected

The allowlist test also confirms an important policy detail: once an allowlist is configured, even loopback is rejected unless it appears in the list. That avoids an ambiguous mixed mode where production policy silently inherits development exceptions.

The initial PR needed a build fix after review. The formatting failure was in src/server.ts, and I addressed it with Prettier. The maintainer also asked for deployment documentation covering MS365_MCP_ALLOWED_REDIRECT_URIS, which became the new "Redirect URI Validation" section. After that, the seven unit tests passed and the release bot included the PR in 0.107.1.

Why this matters in MCP systems

This bug is not about a model hallucinating a tool call or prompt injection hidden in a document. It is more ordinary than that: an OAuth parameter crossed a trust boundary without local validation. That ordinariness is the point.

MCP servers are increasingly becoming authentication brokers for high-value services. A Microsoft 365 MCP server is not just a thin API wrapper. It sits between AI clients, OAuth flows, tokens and organisational data. A redirect validation mistake in that position can become an account access problem, even when the immediate code defect looks like simple URL forwarding.

The pattern is close to the one in my AIPex localhost daemon case study. There, a local WebSocket bridge assumed the caller was legitimate because the service was bound to loopback. Here, the OAuth handler could assume Microsoft Entra would enforce the important part. In both cases, an adjacent platform control existed, but the MCP component still needed to enforce its own boundary.

It also fits the broader MCP security problem described in the MCP attack surface writeup. Much of the public discussion around MCP focuses on malicious tools and indirect prompt injection. Those are real problems. They are not the only ones. These servers also inherit every familiar web security class: SSRF, path traversal, origin validation failure, unsafe redirects and token handling mistakes. The AI label does not retire the old checklist.

The operational lesson

The safest redirect URI policy is boring: exact strings, registered in one place, mirrored in the application and reviewed when deployments change. PR #456 gives ms-365-mcp-server operators that control through MS365_MCP_ALLOWED_REDIRECT_URIS. It also narrows the default case enough that local development still works without leaving the most obvious footguns in place.

OAuth security often fails in the gap between two components that each assume the other one is responsible. Entra validates app registrations. The MCP server accepts the public request. The fix closes part of that gap. The uncomfortable part is how often AI infrastructure is now being built exactly in those gaps, stitching powerful services together faster than their trust boundaries are named.

Newsletter

One email a week. Security research, engineering deep-dives and AI security insights - written for practitioners. No noise.