Skip to content

URL-encode client_id in Azure IMDS token request#2787

Open
ikow wants to merge 2 commits intomongodb:masterfrom
ikow:fix/url-encode-azure-imds-client-id
Open

URL-encode client_id in Azure IMDS token request#2787
ikow wants to merge 2 commits intomongodb:masterfrom
ikow:fix/url-encode-azure-imds-client-id

Conversation

@ikow
Copy link
Copy Markdown

@ikow ikow commented Apr 29, 2026

Summary

Apply urllib.parse.quote() to the client_id parameter in _get_azure_response() before interpolating it into the Azure IMDS URL, consistent with the existing treatment of resource (which is already encoded via quote() in _OIDCAzureCallback.__init__).

Motivation

The resource parameter is URL-encoded at the call site (auth_oidc_shared.py:103), but client_id is interpolated raw via f-string (_azure_helpers.py:32). This inconsistency means a client_id containing URL-special characters (&, =, #, etc.) could unintentionally alter the query string structure.

For comparison, the Node.js driver uses the safe url.searchParams.append('client_id', username) API for the same operation (src/client-side-encryption/providers/azure.ts:127), which automatically handles encoding.

Changes

  • pymongo/_azure_helpers.py: Apply quote() to client_id before URL interpolation
  • test/test_azure_helpers.py: Add test_client_id_is_url_encoded to verify special characters are properly percent-encoded

Test

All 13 tests in test/test_azure_helpers.py pass, including the new encoding test.

The `_get_azure_response()` function constructs the Azure IMDS URL by
interpolating `client_id` via f-string without URL encoding. While
`resource` is already encoded (via `quote()` at the call site in
`auth_oidc_shared.py`), `client_id` is not, creating an inconsistency.

Apply `urllib.parse.quote()` to `client_id` before interpolation,
consistent with the handling of `resource` and with the Node.js driver's
use of `url.searchParams.append()` for the same parameter.

Add a test to verify special characters in `client_id` are properly
percent-encoded and cannot introduce additional query parameters.
@ikow ikow requested a review from a team as a code owner April 29, 2026 20:58
@ikow ikow requested a review from Jibola April 29, 2026 20:58
@evergreen-ci-prod
Copy link
Copy Markdown

There is an existing patch(es) for this commit SHA:

Please note that the status that is posted is not in the context of this PR but rather the (latest) existing patch and that may affect some tests that may depend on the particular PR. If your tests do not rely on any PR-specific values (like base or head branch name) then your tests will report the same status. If you would like a patch to run in the context of this PR and abort the other(s), comment 'evergreen retry'.

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 7, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.91%. Comparing base (0adf6df) to head (31c6f4f).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2787      +/-   ##
==========================================
+ Coverage   82.49%   87.91%   +5.42%     
==========================================
  Files         140      141       +1     
  Lines       24177    24407     +230     
  Branches     4131     4176      +45     
==========================================
+ Hits        19945    21458    +1513     
+ Misses       3359     2052    -1307     
- Partials      873      897      +24     
Flag Coverage Δ
auth-aws-rhel8-test-auth-aws-rapid-web-identity-python3.14-cov 35.01% <50.00%> (+<0.01%) ⬆️
auth-aws-win64-test-auth-aws-rapid-web-identity-python3.14-cov 35.01% <50.00%> (+<0.01%) ⬆️
auth-enterprise-macos-test-standard-auth-latest-python3.11-auth-ssl-sharded-cluster-cov 43.75% <50.00%> (?)
auth-enterprise-rhel8-test-standard-auth-latest-python3.11-auth-ssl-sharded-cluster-cov 43.75% <50.00%> (+<0.01%) ⬆️
auth-enterprise-win64-test-standard-auth-latest-python3.11-auth-ssl-sharded-cluster-cov 43.78% <50.00%> (+0.01%) ⬆️
auth-oidc-local-ubuntu-22-test-auth-oidc-default 48.61% <50.00%> (?)
compression-snappy-rhel8-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 59.35% <50.00%> (+<0.01%) ⬆️
compression-snappy-rhel8-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 61.48% <50.00%> (-0.03%) ⬇️
compression-snappy-rhel8-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 61.10% <50.00%> (-0.02%) ⬇️
compression-snappy-rhel8-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 59.01% <50.00%> (-0.01%) ⬇️
compression-zlib-rhel8-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 59.35% <50.00%> (-0.01%) ⬇️
compression-zlib-rhel8-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 61.51% <50.00%> (-0.02%) ⬇️
compression-zlib-rhel8-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 61.10% <50.00%> (+<0.01%) ⬆️
compression-zlib-rhel8-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 59.00% <50.00%> (-0.01%) ⬇️
compression-zstd-rhel8-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 59.35% <50.00%> (-0.01%) ⬇️
compression-zstd-rhel8-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 61.51% <50.00%> (+0.03%) ⬆️
compression-zstd-rhel8-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 61.10% <50.00%> (+<0.01%) ⬆️
compression-zstd-rhel8-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 59.00% <50.00%> (+<0.01%) ⬆️
compression-zstd-ubuntu-22-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 58.98% <50.00%> (+<0.01%) ⬆️
coverage-report-coverage-report 87.87% <100.00%> (+6.66%) ⬆️
disable-test-commands-rhel8-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 59.34% <50.00%> (-0.01%) ⬇️
disable-test-commands-rhel8-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 61.48% <50.00%> (+<0.01%) ⬆️
disable-test-commands-rhel8-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 61.10% <50.00%> (+<0.01%) ⬆️
disable-test-commands-rhel8-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 59.01% <50.00%> (-0.01%) ⬇️
encryption-crypt_shared-macos-test-non-standard-latest-python3.13-noauth-nossl-standalone-cov 53.08% <50.00%> (?)
encryption-crypt_shared-macos-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 54.80% <50.00%> (?)
encryption-crypt_shared-macos-test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov 54.60% <50.00%> (?)
encryption-crypt_shared-rhel8-test-non-standard-latest-python3.13-noauth-nossl-standalone-cov 53.08% <50.00%> (?)
encryption-crypt_shared-rhel8-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 54.67% <50.00%> (?)
encryption-crypt_shared-rhel8-test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov 54.58% <50.00%> (?)
encryption-crypt_shared-win64-test-non-standard-latest-python3.13-noauth-nossl-standalone-cov 52.96% <50.00%> (?)
encryption-crypt_shared-win64-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 54.84% <50.00%> (?)
encryption-crypt_shared-win64-test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov 54.63% <50.00%> (?)
encryption-macos-test-non-standard-latest-python3.13-noauth-nossl-standalone-cov 53.08% <50.00%> (?)
encryption-macos-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 54.73% <50.00%> (?)
encryption-macos-test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov 54.58% <50.00%> (?)
encryption-pyopenssl-rhel8-test-non-standard-latest-python3.13-noauth-nossl-standalone-cov 53.75% <50.00%> (?)
encryption-pyopenssl-rhel8-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 55.34% <50.00%> (?)
encryption-pyopenssl-rhel8-test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov 55.26% <50.00%> (?)
encryption-rhel8-test-non-standard-latest-python3.13-noauth-nossl-standalone-cov 53.07% <50.00%> (?)
encryption-rhel8-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 54.66% <50.00%> (?)
encryption-rhel8-test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov 54.58% <50.00%> (?)
encryption-win64-test-non-standard-latest-python3.13-noauth-nossl-standalone-cov 52.95% <50.00%> (?)
encryption-win64-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 54.70% <50.00%> (?)
encryption-win64-test-non-standard-latest-python3.14t-noauth-ssl-replica-set-cov 54.64% <50.00%> (?)
load-balancer-test-non-standard-latest-python3.14-auth-ssl-sharded-cluster-cov 48.40% <50.00%> (?)
mongodb-latest-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 61.47% <50.00%> (+<0.01%) ⬆️
mongodb-latest-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 59.36% <50.00%> (+0.01%) ⬆️
mongodb-latest-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 59.51% <100.00%> (+<0.01%) ⬆️
mongodb-latest-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 59.60% <100.00%> (+<0.01%) ⬆️
mongodb-latest-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 61.41% <50.00%> (+<0.01%) ⬆️
mongodb-rapid-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 61.48% <50.00%> (+<0.01%) ⬆️
mongodb-rapid-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 59.33% <50.00%> (-0.02%) ⬇️
mongodb-rapid-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 59.50% <100.00%> (+<0.01%) ⬆️
mongodb-rapid-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 59.59% <100.00%> (+<0.01%) ⬆️
mongodb-rapid-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 61.43% <50.00%> (+0.01%) ⬆️
mongodb-v4.2-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 57.19% <50.00%> (+<0.01%) ⬆️
mongodb-v4.2-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 55.61% <50.00%> (+<0.01%) ⬆️
mongodb-v4.2-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 57.06% <100.00%> (+<0.01%) ⬆️
mongodb-v4.2-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 57.17% <100.00%> (+<0.01%) ⬆️
mongodb-v4.2-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 57.35% <50.00%> (+<0.01%) ⬆️
mongodb-v4.4-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 59.57% <50.00%> (-0.01%) ⬇️
mongodb-v4.4-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 57.59% <50.00%> (-0.01%) ⬇️
mongodb-v4.4-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 57.58% <100.00%> (+<0.01%) ⬆️
mongodb-v4.4-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 57.65% <100.00%> (+<0.01%) ⬆️
mongodb-v4.4-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 59.35% <50.00%> (-0.02%) ⬇️
mongodb-v5.0-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 59.76% <50.00%> (+<0.01%) ⬆️
mongodb-v5.0-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 57.74% <50.00%> (-0.01%) ⬇️
mongodb-v5.0-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 57.77% <100.00%> (+<0.01%) ⬆️
mongodb-v5.0-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 57.89% <100.00%> (-0.02%) ⬇️
mongodb-v5.0-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 59.59% <50.00%> (+<0.01%) ⬆️
mongodb-v6.0-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 59.77% <50.00%> (+<0.01%) ⬆️
mongodb-v6.0-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 57.74% <50.00%> (-0.02%) ⬇️
mongodb-v6.0-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 57.79% <100.00%> (+<0.01%) ⬆️
mongodb-v6.0-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 57.94% <100.00%> (+<0.01%) ⬆️
mongodb-v6.0-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 59.75% <50.00%> (+0.01%) ⬆️
mongodb-v7.0-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 59.80% <50.00%> (-0.02%) ⬇️
mongodb-v7.0-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 57.73% <50.00%> (+<0.01%) ⬆️
mongodb-v7.0-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 57.84% <100.00%> (+<0.01%) ⬆️
mongodb-v7.0-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 57.93% <100.00%> (-0.02%) ⬇️
mongodb-v7.0-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 59.74% <50.00%> (+0.01%) ⬆️
mongodb-v8.0-test-server-version-python3.10-async-auth-ssl-sharded-cluster-min-deps-cov 61.49% <50.00%> (+0.01%) ⬆️
mongodb-v8.0-test-server-version-python3.10-async-noauth-nossl-standalone-min-deps-cov 59.35% <50.00%> (-0.02%) ⬇️
mongodb-v8.0-test-server-version-python3.10-sync-auth-ssl-sharded-cluster-min-deps-cov 59.50% <100.00%> (+<0.01%) ⬆️
mongodb-v8.0-test-server-version-python3.10-sync-noauth-nossl-replica-set-min-deps-cov 59.59% <100.00%> (+<0.01%) ⬆️
mongodb-v8.0-test-server-version-python3.11-async-noauth-nossl-replica-set-cov 61.41% <50.00%> (-0.01%) ⬇️
no-c-ext-rhel8-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 60.55% <50.00%> (+0.01%) ⬆️
no-c-ext-rhel8-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 62.68% <50.00%> (-0.05%) ⬇️
no-c-ext-rhel8-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 62.30% <50.00%> (+<0.01%) ⬆️
no-c-ext-rhel8-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 60.21% <50.00%> (+0.01%) ⬆️
ocsp-rhel8-test-ocsp-ecdsa-valid-cert-server-staples-latest-python3.14-cov 34.21% <50.00%> (?)
ocsp-rhel8-test-ocsp-rsa-valid-cert-server-staples-latest-python3.14-cov 34.23% <50.00%> (?)
pyopenssl-macos-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 61.75% <50.00%> (?)
pyopenssl-rhel8-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 61.75% <50.00%> (?)
pyopenssl-win64-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 61.66% <50.00%> (?)
stable-api-accept-v2-rhel8-auth-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 59.34% <50.00%> (-0.05%) ⬇️
stable-api-accept-v2-rhel8-auth-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 59.01% <50.00%> (+0.01%) ⬆️
stable-api-require-v1-rhel8-auth-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 59.32% <50.00%> (+<0.01%) ⬆️
stable-api-require-v1-rhel8-auth-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 60.95% <50.00%> (+<0.01%) ⬆️
stable-api-require-v1-rhel8-auth-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 58.97% <50.00%> (-0.02%) ⬇️
storage-inmemory-rhel8-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 59.37% <50.00%> (+0.03%) ⬆️
storage-inmemory-rhel8-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 59.00% <50.00%> (-0.01%) ⬇️
test-macos-arm64-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 59.33% <50.00%> (+0.01%) ⬆️
test-macos-arm64-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 61.46% <50.00%> (-0.03%) ⬇️
test-macos-arm64-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 61.09% <50.00%> (-0.09%) ⬇️
test-macos-arm64-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 58.97% <50.00%> (-0.02%) ⬇️
test-macos-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 59.34% <50.00%> (?)
test-macos-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 61.46% <50.00%> (?)
test-macos-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 61.09% <50.00%> (?)
test-macos-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 58.99% <50.00%> (?)
test-numpy-macos-arm64-test-numpy-python3.14-python3.14-cov 32.61% <ø> (?)
test-numpy-macos-test-numpy-python3.14-python3.14-cov 32.59% <ø> (?)
test-numpy-rhel8-test-numpy-python3.14-python3.14-cov 32.61% <ø> (ø)
test-numpy-win32-test-numpy-python3.14-python3.14-cov 32.59% <ø> (ø)
test-numpy-win64-test-numpy-python3.14-python3.14-cov 32.59% <ø> (ø)
test-win32-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 59.22% <50.00%> (-0.01%) ⬇️
test-win32-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 61.48% <50.00%> (+0.03%) ⬆️
test-win32-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 61.03% <50.00%> (-0.04%) ⬇️
test-win32-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 58.87% <50.00%> (+0.01%) ⬆️
test-win64-test-standard-latest-python3.11-async-noauth-nossl-standalone-cov 59.21% <50.00%> (-0.02%) ⬇️
test-win64-test-standard-latest-python3.12-async-noauth-ssl-replica-set-cov 61.48% <50.00%> (+0.02%) ⬆️
test-win64-test-standard-latest-python3.13-async-auth-ssl-sharded-cluster-cov 61.07% <50.00%> (+<0.01%) ⬆️
test-win64-test-standard-latest-python3.14-async-noauth-nossl-standalone-cov 58.86% <50.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

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.

4 participants