Skip to content

fix(abstractions): normalize allowed hosts, reject userinfo, and validate URL structure - #824

Open
Hardik K (hardikkaurani) wants to merge 2 commits into
microsoft:mainfrom
hardikkaurani:fix/allowed-hosts-validation-and-normalization
Open

Hardik K (hardikkaurani) wants to merge 2 commits into
microsoft:mainfrom
hardikkaurani:fix/allowed-hosts-validation-and-normalization

Conversation

@hardikkaurani

Copy link
Copy Markdown
Contributor

Description

This PR strengthens host validation and normalization in AllowedHostsValidator:

  1. Host normalization & prefix validation:

    • AllowedHostsValidator.__init__ now delegates to set_allowed_hosts to maintain a single normalization path.
    • Trims leading and trailing whitespace from host entries (host.strip()) before storing and checking prefixes.
    • Performs case-insensitive checks for http:// and https:// prefixes (e.g., catching HTTP://... or http://...), raising ValueError consistently.
    • Enforces element type validation (isinstance(host, str)), raising TypeError if a non-string entry is provided.
  2. URL validation & security hardening:

    • Strips leading and trailing whitespace from the candidate URL before parsing.
    • Rejects URLs containing userinfo (o.username or o.password) outright to prevent host spoofing and credential leakage (aligning with the TypeScript implementation).
    • Validates URL structure (scheme, netloc, and hostname) before considering allowed_hosts empty fallback, ensuring malformed URLs are rejected even when allowed hosts are not configured.
  3. Test coverage:

    • Added unit tests covering whitespace trimming, uppercase/padded prefix rejection, non-string element rejection, userinfo rejection, and malformed URL handling.

cc Vincent Biret (@baywet)

Copilot AI balanced review requested due to automatic review settings October 22, 2026 05:35
@hardikkaurani
Hardik K (hardikkaurani) requested a review from a team as a code owner October 22, 2026 05:35

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@hardikkaurani

Copy link
Copy Markdown
Contributor Author

Hi Vincent Biret (@baywet), opened this PR to normalize allowed hosts, reject URLs with userinfo, and validate URL structure. Unit tests and SonarCloud analysis are all passing. Whenever you have time, please review. Thank you!

raise ValueError("Allowed host value cannot contain 'https://' or 'http://' prefix")

self.allowed_hosts: set[str] = {x.lower() for x in allowed_hosts}
self.allowed_hosts: set[str] = set()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we initializing the backing field here since we're already doing this in the setter?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Removed the redundant self.allowed_hosts = set() backing-field initialization from init, relying directly on self.set_allowed_hosts(allowed_hosts) to canonicalize and initialize self.allowed_hosts. Verified in commit 12a6da5.

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.

🟡 Changes recommended

Blank-only allowlists can disable credential destination filtering, and URL rejection edge cases and incompatible Azure tests remain unresolved.

4 open issues

Review effort: Balanced

Comment on lines +45 to +46
if cleaned_host:
normalized_hosts.add(lower_host)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed. The validator now raises ValueError for blank or whitespace-only normalized entries ([""], [" "], and mixed lists with blank entries) rather than silently discarding them. Only an explicitly empty list [] enables unrestricted host fallback semantics. Added regression tests test_raises_on_empty_string_entry, test_raises_on_whitespace_only_entry, and test_raises_on_mixed_valid_and_blank_entry. Verified in commit 12a6da5.

# query='query', fragment='fragment')
o = urlparse(url)
if not all([o.scheme, o.netloc]):
o = urlparse(cleaned_url)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed. Wrapped urlparse(cleaned_url) in a try...except ValueError block returning False to prevent malformed URL exceptions from escaping out of the validator. Added regression test test_returns_false_for_unmatched_ipv6_bracket demonstrating safe rejection for both configured and empty allowlists. Verified in commit 12a6da5.

o = urlparse(url)
if not all([o.scheme, o.netloc]):
o = urlparse(cleaned_url)
if not (o.scheme and o.netloc and o.hostname) or o.username or o.password:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed. Updated userinfo validation to check o.username is not None or o.password is not None, ensuring empty userinfo strings (https://@example.com/path and https://:@example.com/path) are rejected. Added regression tests test_returns_false_for_explicitly_empty_userinfo and test_returns_false_for_userinfo_even_when_allowed_hosts_empty. Verified in commit 12a6da5.

Comment on lines 69 to 70
if not (o.scheme and o.netloc and o.hostname) or o.username or o.password:
return False

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed. Updated test_get_authorization_token_invalid_url and test_derived_scope_rejects_url_without_hostname to assert that validator-rejected URLs return an empty token ("") without calling the credential. Split userinfo and port testing into test_userinfo_url_is_rejected_without_requesting_token (asserting token == "" and received_scopes == []) and test_derived_scope_strips_port (asserting proper scope derivation on legitimate URLs with port). All 23 Azure provider tests pass. Verified in commit 12a6da5.

Copilot AI balanced review requested due to automatic review settings October 23, 2026 14:22

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sonarqubecloud

Copy link
Copy Markdown

@hardikkaurani

Hardik K (hardikkaurani) commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Hi Vincent Biret (@baywet),

Thanks for the review. I've pushed commit [12a6da5](https://github.com/microsoft/kiota-python/commit/12a6da5) addressing the reported feedback.

Changes

  • Backing field initialization: Removed the redundant self.allowed_hosts = set() initialization and relied on self.set_allowed_hosts(allowed_hosts) for initialization.
  • Blank allowlist entries: Empty or whitespace-only host entries now raise ValueError instead of being silently discarded. Only an explicitly empty list ([]) enables the unrestricted destination fallback.
  • Malformed URLs: Added a try...except ValueError guard around URL parsing so malformed URLs, including unmatched IPv6 brackets, are rejected safely.
  • Userinfo validation: Updated validation to reject URLs containing userinfo, including empty username or password fields, by checking whether o.username or o.password is not None.
  • Azure provider regression tests: Updated test_azure_identity_access_token_provider.py to verify that rejected destinations return an empty token without invoking credentials, while preserving separate coverage for scope derivation from valid URLs with ports.

Validation

  • packages/abstractions: 184 tests passed; MyPy reported 0 issues; Pylint scored 10.00/10; YAPF and isort checks passed.
  • packages/authentication/azure: 23 tests passed.
  • Remote CI: license/cla, Changelog PR Guard, PullRequestConflicting, and SonarCloud are passing. Workflows requiring maintainer approval for fork PRs remain queued.

I've also updated the inline review threads with individual replies. When you have a chance, could you please take another look? Thank you for your time and feedback!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants