Fix SSRF via nullable-bool allowlist fail-open in SessionsPythonPlugin#14153
Open
tmacam wants to merge 3 commits into
Open
Fix SSRF via nullable-bool allowlist fail-open in SessionsPythonPlugin#14153tmacam wants to merge 3 commits into
tmacam wants to merge 3 commits into
Conversation
…thonPlugin
The domain allowlist validation in SendAsync used a confusing nullable-bool
expression that silently allowed all hosts when AllowedDomains was null:
if (!this._settings.AllowedDomains?.Contains(uri.Host) ?? false)
Due to C# operator precedence, `!` binds before `??`, so the null case
(unconfigured AllowedDomains) evaluated to `false` — skipping the check
entirely and permitting requests to any URL, including Azure IMDS
(169.254.169.254) and internal VNet services.
Replace with explicit logic that defaults to the configured endpoint's own
host when AllowedDomains is null. This preserves backward compatibility
(legitimate requests still reach their configured session pool) while
closing the fail-open path.
Add three regression tests covering the null-AllowedDomains scenarios.
Ref: MSRC 125929 / VULN-200115
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates the SessionsPythonPlugin domain allowlist validation to remove a nullable-bool precedence pitfall, aiming to prevent a fail-open scenario when AllowedDomains is left unconfigured, and adds unit tests to cover the intended behavior/regression.
Changes:
- Replaces the nullable/
??allowlist check inSendAsyncwith explicit “effective allowed domains” logic. - Adds tests for
AllowedDomains = nullbehavior and a regression test tied to the referenced MSRC case. - Normalizes file headers (BOM removal) in the touched files.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| dotnet/src/Plugins/Plugins.Core/CodeInterpreter/SessionsPythonPlugin.cs | Reworks host allowlist validation logic in SendAsync to avoid nullable-bool precedence fail-open. |
| dotnet/src/Plugins/Plugins.UnitTests/Core/SessionsPythonPluginTests.cs | Adds new tests covering the null-AllowedDomains scenario and a regression case. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The .editorconfig requires charset=utf-8-bom for .cs files. The prior edit stripped the byte-order mark, causing the check-format CI job to fail with a CHARSET error. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Restore UTF-8 BOM on SessionsPythonPlugin.cs and SessionsPythonPluginTests.cs so the check-format (dotnet format) CHARSET gate passes; .editorconfig requires charset=utf-8-bom for .cs files. - Use StringComparer.OrdinalIgnoreCase for the host allowlist comparison to match DNS semantics and the convention used by other plugins (HttpPlugin, etc.). - Rewrite the misleading null-AllowedDomains block test to genuinely exercise the explicit-allowlist path (ItShouldBlockEndpointHostNotInAllowedDomainsAsync), removing the dead variable and rambling commentary. - Consolidate the duplicated/ineffective regression test into ItShouldAllowConfiguredEndpointHostWhenAllowedDomainsIsNullAsync, which now also asserts the request is sent to the configured endpoint host. - Add a case-insensitive allowlist test case and correct the code comment so it no longer overstates the null-default as IMDS protection. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces a confusing, easy-to-misread nullable-bool operator-precedence expression in
SessionsPythonPlugin.SendAsync()that governed the domain allowlist, and hardens/clarifies the surrounding validation.Problem
The original validation expression:
parses as
(!nullable_bool) ?? falsedue to C# operator precedence:AllowedDomains?.Contains()!result?? falsenullnullnullfalse["a.io"], host ="a.io"truefalsefalse["a.io"], host ="b.io"falsetruetrueThe expression is correct for the explicit-allowlist rows but is opaque and relies on
bool?null-propagation through!and??, which is a well-known footgun and hard to audit for a security-sensitive check.Fix
Replace the expression with explicit, readable logic and make the host comparison case-insensitive (DNS semantics; matches
HttpPlugin/WebFileDownloadPlugin):Threat model / scope (updated after review)
An earlier version of this description claimed the null-
AllowedDomainsdefault blocks IMDS. That is not accurate for this plugin and has been corrected:SendAsync,uri = new Uri(_poolManagementEndpoint, relativePath)and every caller passes a hardcoded relative path ("executions","files", …), souri.Hostis invariantly_poolManagementEndpoint.Host.{ endpoint.Host }) is a readability / backward-compatibility replacement for the old(!x) ?? falseexpression — it does not add a restriction and does not, by itself, block IMDS.AllowedDomains, which is now correct and case-insensitive.Why this is safe
AllowedDomainssee no behavior change — requests go to the endpoint they configured at construction time.AllowedDomainsis configured, regardless of casing.Tests
ItShouldRespectAllowedDomainsAsync(+ newFAKE-TEST-HOST.iocase)ItShouldAllowConfiguredEndpointHostWhenAllowedDomainsIsNullAsyncAllowedDomainspermits the configured endpoint host, and the request is actually sent thereItShouldBlockEndpointHostNotInAllowedDomainsAsyncKnown follow-up (out of scope here)
Redirect-based SSRF is not closed by this change:
HttpClientfollows 3xx redirects by default and the allowlist is only checked against the initial URI. Because the client comes from the injectedIHttpClientFactory, this plugin cannot forceAllowAutoRedirect = falseon an already-constructed client without changing the construction contract. This is pre-existing and best handled as a focused follow-up (named/typed client registration with a non-redirecting handler).References