Python: Harden HttpPlugin request validation#13969
Python: Harden HttpPlugin request validation#13969SergeyMenshykh wants to merge 1 commit intomicrosoft:mainfrom
Conversation
- Deny-all by default: add allow_all_domains flag requiring explicit opt-in - Block redirects when allowed_domains is set to prevent domain bypass - Add URL scheme validation (http/https only) - Fix empty hostname bypass and redirect logic edge cases Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR hardens the Python HttpPlugin by making outbound HTTP requests opt-in (either via an allowlist of domains or an explicit “allow all domains” switch), tightening URL validation to only permit http/https, and adjusting redirect behavior to reduce SSRF bypass risk.
Changes:
- Change default behavior to block all requests unless
allowed_domainsis provided orallow_all_domains=True. - Add scheme validation (only
http/https) and disable redirects when domain restrictions are configured. - Update and expand unit tests to cover the new security behavior and regressions.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| python/semantic_kernel/core_plugins/http_plugin.py | Enforces default-deny URL policy, restricts URL schemes, and controls redirect behavior based on domain configuration. |
| python/tests/unit/core_plugins/test_http_plugin.py | Updates existing tests for the opt-in behavior and adds regression/security tests for default-deny, scheme filtering, and redirect handling. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| - When ``allowed_domains`` is set, HTTP redirects are disabled to prevent | ||
| redirect-based domain bypass (SSRF). |
| @@ -179,8 +179,8 @@ async def test_allowed_domains_case_insensitive(): | |||
|
|
|||
|
|
|||
| async def test_allowed_domains_none_allows_all(): | |||
| self._validate_url(url) | ||
|
|
||
| async with aiohttp.ClientSession() as session, session.get(url, raise_for_status=True) as response: | ||
| allow_redirects = self.allow_all_domains or self.allowed_domains is None | ||
| async with ( | ||
| aiohttp.ClientSession() as session, | ||
| session.get(url, raise_for_status=True, allow_redirects=allow_redirects) as response, |
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 88%
✓ Correctness
The PR correctly implements default-deny, scheme validation, and an explicit allow_all_domains opt-in. The redirect-disable logic for domain-restricted configurations is sound. However, the
allow_redirectsexpression in all four HTTP methods contains a vestigialor self.allowed_domains is Noneclause that carries forward old semantics (whereallowed_domains=Nonemeant 'allow all'), contradicting the new default-deny model. While currently unreachable because_validate_urlblocks requests first, this expression would incorrectly enable redirects if the deny-all path is ever relaxed.
✓ Security Reliability
This PR is well-designed security hardening for the HttpPlugin. The default-deny posture, scheme validation (blocking file:/, ftp://, etc.), redirect disabling with domain restrictions (SSRF protection), and explicit opt-in for unrestricted access are all sound security improvements. The redirect logic (
allow_redirects = self.allow_all_domains or self.allowed_domains is None) is correct across all configuration combinations — in the default case where both are falsy/None,_validate_urlblocks before the redirect setting is ever used. No security or reliability issues found.
✓ Test Coverage
The PR adds strong regression tests for the new security defaults (deny-all, scheme blocking, redirect disabling). However, there is an asymetry in redirect-behavior coverage: the 'redirects disabled with allowed_domains' path is verified for all four HTTP methods (GET/POST/PUT/DELETE), but the 'redirects allowed with allow_all_domains=True' path is only verified for GET. Since each method independently computes
allow_redirectson a separate line, a typo or copy-paste error in POST/PUT/DELETE would go undetected. This is a non-blocking gap.
✗ Design Approach
The redirect hardening mostly goes in the right direction, but the new flag precedence leaves one SSRF bypass open: when both
allowed_domainsandallow_all_domains=Trueare set, the request methods re-enable redirect following even though the class-level security contract says redirects must be disabled wheneverallowed_domainsis configured.
Automated review by SergeyMenshykh's agents
| await plugin.get("https://example.com/path") | ||
|
|
||
| _, kwargs = mock_get.call_args | ||
| assert kwargs["allow_redirects"] is True |
There was a problem hiding this comment.
This test only verifies allow_redirects=True for GET. The corresponding 'redirects disabled' tests cover all four methods (GET/POST/PUT/DELETE) individually, but the 'redirects allowed' path only covers GET. Each HTTP method in http_plugin.py computes allow_redirects on its own line, so a copy-paste error in post(), put(), or delete() would go undetected. Consider adding parametrized or individual tests asserting allow_redirects is True for the other three methods when allow_all_domains=True.
Improve input validation and request handling in the Python HttpPlugin.
Changes