From 36f1258852a00d97a4fb27fd37d7e93d88d1f798 Mon Sep 17 00:00:00 2001 From: yucheng Date: Thu, 24 Sep 2026 08:06:30 +0000 Subject: [PATCH 1/6] fix(fips): use sha256 for mcp advisory locks, oci digest and saml algorithms Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/llms/oci/common_utils.py | 10 +---- litellm/proxy/_experimental/mcp_server/db.py | 4 +- .../management_endpoints/sso/saml_sso.py | 2 + tests/integration/_support/database.py | 18 ++++++++ tests/integration/mcp/test_mcp_management.py | 44 +++++++++++++++++++ .../integration/mcp/test_mcp_user_env_vars.py | 38 +++++++++++++++- .../mcp_server/test_mcp_env_vars.py | 18 ++++++++ .../management_endpoints/test_saml_sso.py | 15 +++++++ 8 files changed, 137 insertions(+), 12 deletions(-) diff --git a/litellm/llms/oci/common_utils.py b/litellm/llms/oci/common_utils.py index 3f703564b5a..3d973d9af7c 100644 --- a/litellm/llms/oci/common_utils.py +++ b/litellm/llms/oci/common_utils.py @@ -91,15 +91,7 @@ class OCIRequestWrapper: def sha256_base64(data: bytes) -> str: - # SHA-256 is used here to compute the x-content-sha256 header required by the - # OCI HTTP signing specification (RSA-SHA256 request signing), not for password - # or secret hashing. This is the correct and mandated algorithm for this purpose. - # See: https://docs.oracle.com/en-us/iaas/Content/API/Concepts/signingrequests.htm - # - # ``usedforsecurity=False`` declares non-security intent to static analyzers - # (CodeQL ``py/weak-sensitive-data-hashing``) — without it the request body - # gets flagged as "password-like data" via taint tracking. - digest: Final = hashlib.sha256(data, usedforsecurity=False).digest() # noqa: S324 + digest: Final = hashlib.sha256(data).digest() # noqa: S324 # OCI request-signing content digest return base64.b64encode(digest).decode() diff --git a/litellm/proxy/_experimental/mcp_server/db.py b/litellm/proxy/_experimental/mcp_server/db.py index 70c6e6f4bf3..ab9e83e8ba5 100644 --- a/litellm/proxy/_experimental/mcp_server/db.py +++ b/litellm/proxy/_experimental/mcp_server/db.py @@ -593,7 +593,7 @@ def _mcp_identifier_lock_keys(*identifiers: str | None) -> tuple[int, ...]: so concurrent requests for the same pair always lock in the same order.""" return tuple( int.from_bytes( - hashlib.blake2b(f"mcp_identifier:{normalized}".encode(), digest_size=8).digest(), + hashlib.sha256(f"mcp_identifier:{normalized}".encode()).digest()[:8], "big", signed=True, ) @@ -2301,7 +2301,7 @@ async def merge_user_env_vars( """ allowed: Final = set(allowed_names) lock_key: Final = int.from_bytes( - hashlib.blake2b(f"{user_id}:{server_id}".encode(), digest_size=8).digest(), + hashlib.sha256(f"{user_id}:{server_id}".encode()).digest()[:8], "big", signed=True, ) diff --git a/litellm/proxy/management_endpoints/sso/saml_sso.py b/litellm/proxy/management_endpoints/sso/saml_sso.py index 12e1f1a03f3..a8dee1d7db2 100644 --- a/litellm/proxy/management_endpoints/sso/saml_sso.py +++ b/litellm/proxy/management_endpoints/sso/saml_sso.py @@ -201,6 +201,8 @@ class SAMLAuthHandler: "wantAssertionsSigned": SAMLAuthHandler._bool_env("SAML_WANT_ASSERTIONS_SIGNED", True), "wantMessagesSigned": SAMLAuthHandler._bool_env("SAML_WANT_MESSAGES_SIGNED", False), "authnRequestsSigned": SAMLAuthHandler._bool_env("SAML_AUTHN_REQUESTS_SIGNED", False), + "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256", "wantNameId": True, "requestedAuthnContext": False, "rejectUnsolicitedResponsesWithInResponseTo": False, diff --git a/tests/integration/_support/database.py b/tests/integration/_support/database.py index e7f0ebdf603..d25d66de3df 100644 --- a/tests/integration/_support/database.py +++ b/tests/integration/_support/database.py @@ -1,3 +1,4 @@ +import hashlib import os from typing import Final @@ -14,3 +15,20 @@ def read_rows( with psycopg.connect(database_url or os.environ["DATABASE_URL"], row_factory=dict_row) as connection: connection.execute("SET TRANSACTION READ ONLY") return ROWS.validate_python(connection.execute(query, parameters).fetchall()) + + +def advisory_lock_key(*parts: str) -> int: + return int.from_bytes(hashlib.sha256(":".join(parts).encode()).digest()[:8], "big", signed=True) + + +def legacy_advisory_lock_key(text: str) -> int: + return int.from_bytes(hashlib.blake2b(text.encode(), digest_size=8).digest(), "big", signed=True) + + +def advisory_waiters(lock_key: int) -> list[dict[str, JsonValue]]: + unsigned: Final = lock_key & 0xFFFFFFFFFFFFFFFF + return read_rows( + "SELECT pid FROM pg_locks WHERE locktype = 'advisory' AND objsubid = 1 " + "AND classid = %s::oid AND objid = %s::oid AND NOT granted", + (str(unsigned >> 32), str(unsigned & 0xFFFFFFFF)), + ) diff --git a/tests/integration/mcp/test_mcp_management.py b/tests/integration/mcp/test_mcp_management.py index bde18840d7d..aaeff80abe7 100644 --- a/tests/integration/mcp/test_mcp_management.py +++ b/tests/integration/mcp/test_mcp_management.py @@ -1,9 +1,13 @@ +import concurrent.futures +import os import uuid from pathlib import Path from typing import Final +import psycopg import yaml from integration._support.client import Gateway, eventually +from integration._support.database import advisory_lock_key, advisory_waiters, legacy_advisory_lock_key from integration._support.mcp import ( McpCaller, call_tool, @@ -285,3 +289,43 @@ def test_config_declared_server_behaves_like_database_server_but_is_read_only(ga assert declared_id in _servers(candidate) assert call_tool(candidate, key, declared_id, declared_names["add"], ADD).status_code == 200 assert len(tool_calls(declared_peer.drain())) == 1 and tool_calls(database_peer.drain()) == () + + +def test_create_waits_on_the_sha256_advisory_lock_for_the_identifier(gateway: Gateway) -> None: + with mcp_peer() as peer, gateway.scenario() as scenario: + name: Final = "mgmt" + uuid.uuid4().hex[:8] + lock_key: Final = advisory_lock_key(f"mcp_identifier:{name.lower()}") + with ( + concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool, + psycopg.connect(os.environ["DATABASE_URL"], autocommit=True) as holder, + ): + holder.execute("SELECT pg_advisory_lock(%s::bigint)", (lock_key,)) + pending: Final = pool.submit( + gateway.request, + "POST", + "/v1/mcp/server", + {"server_name": name, "alias": name, **peer.registration()}, + ) + eventually(lambda: advisory_waiters(lock_key), lambda rows: len(rows) == 1, seconds=20) + assert not pending.done() + holder.execute("SELECT pg_advisory_unlock(%s::bigint)", (lock_key,)) + response: Final = pending.result(timeout=30) + assert response.status_code == 201, response.text + identity: Final = str(response.json()["server_id"]) + scenario.cleanups.callback(forget_mcp, gateway, identity) + assert _servers(gateway)[identity]["server_name"] == name + + +def test_create_does_not_wait_on_the_legacy_blake2b_lock_id(gateway: Gateway) -> None: + with mcp_peer() as peer, gateway.scenario() as scenario: + name: Final = "mgmt" + uuid.uuid4().hex[:8] + lock_key: Final = legacy_advisory_lock_key(f"mcp_identifier:{name.lower()}") + with psycopg.connect(os.environ["DATABASE_URL"], autocommit=True) as holder: + holder.execute("SELECT pg_advisory_lock(%s::bigint)", (lock_key,)) + response: Final = gateway.request( + "POST", "/v1/mcp/server", {"server_name": name, "alias": name, **peer.registration()} + ) + assert response.status_code == 201, response.text + identity: Final = str(response.json()["server_id"]) + scenario.cleanups.callback(forget_mcp, gateway, identity) + assert _servers(gateway)[identity]["server_name"] == name diff --git a/tests/integration/mcp/test_mcp_user_env_vars.py b/tests/integration/mcp/test_mcp_user_env_vars.py index d9cecaccadb..26140d318b6 100644 --- a/tests/integration/mcp/test_mcp_user_env_vars.py +++ b/tests/integration/mcp/test_mcp_user_env_vars.py @@ -1,3 +1,4 @@ +import os import signal import uuid from collections.abc import Mapping @@ -8,9 +9,10 @@ from pathlib import Path from typing import Final import httpx +import psycopg import pytest from integration._support.client import Gateway, Scenario, eventually, object_value, string_value -from integration._support.database import read_rows +from integration._support.database import advisory_lock_key, advisory_waiters, legacy_advisory_lock_key, read_rows from integration._support.mcp import McpPeer, call_tool, mcp_peer, register_mcp, tool_names from integration._support.process import owned_proxy_process from pydantic import JsonValue, TypeAdapter @@ -357,3 +359,37 @@ def test_concurrent_stores_of_different_variables_do_not_lose_an_update(gateway: with ThreadPoolExecutor(max_workers=2) as pool: for _ in range(5): race_once(pool) + + +def test_store_waits_on_the_sha256_advisory_lock_for_the_user_and_server(gateway: Gateway) -> None: + with mcp_peer() as peer, gateway.scenario() as scenario: + identity: Final = register_user_var_server(scenario, peer, TOKEN) + user: Final = scenario.user() + key: Final = scenario.key(user_id=user, object_permission=grants(identity)) + wait_for_tools(gateway, key, identity) + lock_key: Final = advisory_lock_key(user, identity) + with ( + ThreadPoolExecutor(max_workers=1) as pool, + psycopg.connect(os.environ["DATABASE_URL"], autocommit=True) as holder, + ): + holder.execute("SELECT pg_advisory_lock(%s::bigint)", (lock_key,)) + pending: Final = pool.submit(store, gateway, key, identity, {TOKEN: "held-value"}) + eventually(lambda: advisory_waiters(lock_key), lambda rows: len(rows) == 1, seconds=20) + assert not pending.done() + holder.execute("SELECT pg_advisory_unlock(%s::bigint)", (lock_key,)) + response: Final = pending.result(timeout=30) + assert response.status_code == 200, response.text + assert set_names(env_status(gateway, key, identity)) == {TOKEN: True} + + +def test_store_does_not_wait_on_the_legacy_blake2b_lock_id(gateway: Gateway) -> None: + with mcp_peer() as peer, gateway.scenario() as scenario: + identity: Final = register_user_var_server(scenario, peer, TOKEN) + user: Final = scenario.user() + key: Final = scenario.key(user_id=user, object_permission=grants(identity)) + lock_key: Final = legacy_advisory_lock_key(f"{user}:{identity}") + with psycopg.connect(os.environ["DATABASE_URL"], autocommit=True) as holder: + holder.execute("SELECT pg_advisory_lock(%s::bigint)", (lock_key,)) + response: Final = store(gateway, key, identity, {TOKEN: "free-value"}) + assert response.status_code == 200, response.text + assert set_names(env_status(gateway, key, identity)) == {TOKEN: True} diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_env_vars.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_env_vars.py index 93b894f7645..5c17b9665f4 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_env_vars.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_env_vars.py @@ -6,6 +6,8 @@ connection. The DB-backed per-user flow is exercised in higher-level tests in tests/mcp_tests. """ +import hashlib + import pytest # Look up these names lazily on every access. Tests in this directory call @@ -16,6 +18,7 @@ import pytest # stop matching the new class. Accessing the attribute through the module # always picks up the current version. import litellm.proxy._experimental.mcp_server.utils as _mcp_utils +from litellm.proxy._experimental.mcp_server.db import _mcp_identifier_lock_keys, merge_user_env_vars def _u(name: str): @@ -1721,3 +1724,18 @@ async def test_missing_user_env_vars_error_renders_in_mcp_call_tool(): assert "CorporateDB" in text assert "CORP_USERNAME" in text assert "fill_env_vars=srv-99" in text + + +@pytest.mark.asyncio +async def test_merge_user_env_vars_locks_the_sha256_key_for_user_and_server(env_vars_salt_key): + prisma = _transactional_env_vars_prisma() + await merge_user_env_vars(prisma, "alice", "srv-1", {"TOKEN": "x"}, allowed_names=["TOKEN"]) + + expected = int.from_bytes(hashlib.sha256(b"alice:srv-1").digest()[:8], "big", signed=True) + assert list(prisma.db._store.locks) == [expected] + + +def test_mcp_identifier_lock_keys_use_sha256_of_the_lowercased_identifier(): + keys = _mcp_identifier_lock_keys("Name", "name", None) + + assert keys == (int.from_bytes(hashlib.sha256(b"mcp_identifier:name").digest()[:8], "big", signed=True),) diff --git a/tests/test_litellm/proxy/management_endpoints/test_saml_sso.py b/tests/test_litellm/proxy/management_endpoints/test_saml_sso.py index 635e6332958..49599ec67c1 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_saml_sso.py +++ b/tests/test_litellm/proxy/management_endpoints/test_saml_sso.py @@ -22,6 +22,7 @@ from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.x509.oid import NameOID +from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils from starlette.datastructures import URL @@ -749,3 +750,17 @@ class TestSAMLAuthnCookieSecureFlag: redirect = await SAMLAuthHandler.build_login_redirect(request, cache) cookie = redirect.headers["set-cookie"] assert "Secure" not in cookie + + +@pytest.mark.asyncio +async def test_effective_sp_settings_use_sha256_signature_and_digest(saml_env): + cache = DualCache() + idp_settings = await SAMLAuthHandler._load_idp_settings(cache) + settings = SAMLAuthHandler._build_settings(_fake_request(), idp_settings) + + security = settings["security"] + assert security["signatureAlgorithm"] == "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" + assert security["digestAlgorithm"] == "http://www.w3.org/2001/04/xmlenc#sha256" + effective = OneLogin_Saml2_Settings(settings, sp_validation_only=True).get_security_data() + assert effective["signatureAlgorithm"] == security["signatureAlgorithm"] + assert effective["digestAlgorithm"] == security["digestAlgorithm"] From 2c53a7e324bc87a08085975b63667049cef95184 Mon Sep 17 00:00:00 2001 From: yucheng Date: Thu, 24 Sep 2026 08:34:34 +0000 Subject: [PATCH 2/6] chore(fips): finalize test locals and refresh codeql comment Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 7 +++---- .../_experimental/mcp_server/test_mcp_env_vars.py | 7 ++++--- .../proxy/management_endpoints/test_saml_sso.py | 13 ++++++------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9a85ced57f6..76069b7c42c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -62,10 +62,9 @@ jobs: # a content-integrity hash, not a password or secret hash. SHA-256 is mandated # by Oracle for this header; see # https://docs.oracle.com/en-us/iaas/Content/API/Concepts/signingrequests.htm - # The `usedforsecurity=False` flag on the hashlib.sha256 call already declares - # non-security intent, but CodeQL's taint flow still re-fires when callers - # further up the stack are modified. The suppression is scoped to this one - # file/rule pair via SARIF post-filtering so every other callsite of + # The sha256 call carries no usedforsecurity=False flag (FIPS builds reject the hint), so + # CodeQL's taint flow fires whenever callers further up the stack are modified. The + # suppression is scoped to this one file/rule pair via SARIF post-filtering so every other callsite of # py/weak-sensitive-data-hashing in the repository continues to be analyzed. # The same query fires on the HIBP k-anonymity lookup in # litellm/proxy/auth/password_policy.py, where the password's SHA-1 is only diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_env_vars.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_env_vars.py index 5c17b9665f4..c536d893ec2 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_env_vars.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_env_vars.py @@ -7,6 +7,7 @@ tests in tests/mcp_tests. """ import hashlib +from typing import Final import pytest @@ -1728,14 +1729,14 @@ async def test_missing_user_env_vars_error_renders_in_mcp_call_tool(): @pytest.mark.asyncio async def test_merge_user_env_vars_locks_the_sha256_key_for_user_and_server(env_vars_salt_key): - prisma = _transactional_env_vars_prisma() + prisma: Final = _transactional_env_vars_prisma() await merge_user_env_vars(prisma, "alice", "srv-1", {"TOKEN": "x"}, allowed_names=["TOKEN"]) - expected = int.from_bytes(hashlib.sha256(b"alice:srv-1").digest()[:8], "big", signed=True) + expected: Final = int.from_bytes(hashlib.sha256(b"alice:srv-1").digest()[:8], "big", signed=True) assert list(prisma.db._store.locks) == [expected] def test_mcp_identifier_lock_keys_use_sha256_of_the_lowercased_identifier(): - keys = _mcp_identifier_lock_keys("Name", "name", None) + keys: Final = _mcp_identifier_lock_keys("Name", "name", None) assert keys == (int.from_bytes(hashlib.sha256(b"mcp_identifier:name").digest()[:8], "big", signed=True),) diff --git a/tests/test_litellm/proxy/management_endpoints/test_saml_sso.py b/tests/test_litellm/proxy/management_endpoints/test_saml_sso.py index 49599ec67c1..fac24d37fb0 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_saml_sso.py +++ b/tests/test_litellm/proxy/management_endpoints/test_saml_sso.py @@ -10,6 +10,7 @@ makes a test fail. import base64 import datetime import time +from typing import Final, cast import pytest from fastapi import HTTPException, Request @@ -26,8 +27,6 @@ from onelogin.saml2.settings import OneLogin_Saml2_Settings from onelogin.saml2.utils import OneLogin_Saml2_Utils from starlette.datastructures import URL -from typing import cast - from litellm.caching.dual_cache import DualCache from litellm.caching.in_memory_cache import InMemoryCache from litellm.caching.redis_cache import RedisCache @@ -754,13 +753,13 @@ class TestSAMLAuthnCookieSecureFlag: @pytest.mark.asyncio async def test_effective_sp_settings_use_sha256_signature_and_digest(saml_env): - cache = DualCache() - idp_settings = await SAMLAuthHandler._load_idp_settings(cache) - settings = SAMLAuthHandler._build_settings(_fake_request(), idp_settings) + cache: Final = DualCache() + idp_settings: Final = await SAMLAuthHandler._load_idp_settings(cache) + settings: Final = SAMLAuthHandler._build_settings(_fake_request(), idp_settings) - security = settings["security"] + security: Final = settings["security"] assert security["signatureAlgorithm"] == "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" assert security["digestAlgorithm"] == "http://www.w3.org/2001/04/xmlenc#sha256" - effective = OneLogin_Saml2_Settings(settings, sp_validation_only=True).get_security_data() + effective: Final = OneLogin_Saml2_Settings(settings, sp_validation_only=True).get_security_data() assert effective["signatureAlgorithm"] == security["signatureAlgorithm"] assert effective["digestAlgorithm"] == security["digestAlgorithm"] From 97356f883ecb472241c6c0ac38d972f35c2c7707 Mon Sep 17 00:00:00 2001 From: yucheng Date: Thu, 24 Sep 2026 08:44:31 +0000 Subject: [PATCH 3/6] chore(fips): neutral wording for the codeql oci suppression comment Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 76069b7c42c..2a3df89fac7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -62,7 +62,7 @@ jobs: # a content-integrity hash, not a password or secret hash. SHA-256 is mandated # by Oracle for this header; see # https://docs.oracle.com/en-us/iaas/Content/API/Concepts/signingrequests.htm - # The sha256 call carries no usedforsecurity=False flag (FIPS builds reject the hint), so + # The sha256 call carries no usedforsecurity=False hint (kept out for FIPS review), so # CodeQL's taint flow fires whenever callers further up the stack are modified. The # suppression is scoped to this one file/rule pair via SARIF post-filtering so every other callsite of # py/weak-sensitive-data-hashing in the repository continues to be analyzed. From 0236c42c6715322f0623671f7fe2170142d247b0 Mon Sep 17 00:00:00 2001 From: yucheng Date: Thu, 24 Sep 2026 08:54:28 +0000 Subject: [PATCH 4/6] ci(codeql): suppress weak-hash alert on mcp advisory lock ids Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2a3df89fac7..86320e05861 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -71,13 +71,18 @@ jobs: # a lookup key into the haveibeenpwned range API (the protocol mandates # SHA-1) and the digest itself never leaves the proxy beyond its first 5 # characters. - - name: Filter SARIF (OCI sha256, HIBP sha1) + # It also fires on the MCP advisory-lock derivations in + # litellm/proxy/_experimental/mcp_server/db.py, which hash identifier + # strings (user_id, server_id, server names) into Postgres advisory lock + # keys — neither password nor secret hashing. + - name: Filter SARIF (OCI sha256, HIBP sha1, MCP lock ids) if: matrix.language == 'python' uses: advanced-security/filter-sarif@2da736ff05ef065cb2894ac6892e47b5eac2c3c0 # v1.1 with: patterns: | -litellm/llms/oci/common_utils.py:py/weak-sensitive-data-hashing -litellm/proxy/auth/password_policy.py:py/weak-sensitive-data-hashing + -litellm/proxy/_experimental/mcp_server/db.py:py/weak-sensitive-data-hashing input: sarif-results/python.sarif output: sarif-results/python.sarif From 3f2214a095d3cc091d3a225fe14e7950cad2f02f Mon Sep 17 00:00:00 2001 From: yucheng Date: Thu, 24 Sep 2026 10:49:09 +0000 Subject: [PATCH 5/6] chore(codeql): plain punctuation in the mcp lock id comment Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 86320e05861..d8ac3ae1efe 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -74,7 +74,7 @@ jobs: # It also fires on the MCP advisory-lock derivations in # litellm/proxy/_experimental/mcp_server/db.py, which hash identifier # strings (user_id, server_id, server names) into Postgres advisory lock - # keys — neither password nor secret hashing. + # keys, so neither a password nor a secret is hashed. - name: Filter SARIF (OCI sha256, HIBP sha1, MCP lock ids) if: matrix.language == 'python' uses: advanced-security/filter-sarif@2da736ff05ef065cb2894ac6892e47b5eac2c3c0 # v1.1 From de73489dd1f5bdfe7114d06724c0205b13bef068 Mon Sep 17 00:00:00 2001 From: yucheng Date: Thu, 24 Sep 2026 13:49:28 +0000 Subject: [PATCH 6/6] test(mcp): stop comprehension variable shadowing the body helper The inlined comprehension bound a loop variable named body, which made body a function local on CPython 3.12.2 and raised UnboundLocalError at the later call to the module-level body() helper; rename it to bad Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tests/integration/mcp/test_mcp_user_env_vars.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/mcp/test_mcp_user_env_vars.py b/tests/integration/mcp/test_mcp_user_env_vars.py index 26140d318b6..e8e219ad728 100644 --- a/tests/integration/mcp/test_mcp_user_env_vars.py +++ b/tests/integration/mcp/test_mcp_user_env_vars.py @@ -197,7 +197,7 @@ def test_malformed_bodies_missing_users_and_foreign_servers_are_rejected(gateway path: Final = f"/v1/mcp/server/{identity}/user-env-vars" payload: Final[dict[str, JsonValue]] = {"values": {TOKEN: "x"}} malformed: Final[tuple[dict[str, JsonValue], ...]] = ({"values": {TOKEN: 7}}, {"values": ["a"]}, {}) - assert [gateway.request("POST", path, body, key=key).status_code for body in malformed] == [422, 422, 422] + assert [gateway.request("POST", path, bad, key=key).status_code for bad in malformed] == [422, 422, 422] assert set_names(env_status(gateway, key, identity)) == {TOKEN: False} assert [gateway.client.request(method, path, json=payload).status_code for method in METHODS] == [401, 401, 401] no_user: Final = tuple(gateway.request(method, path, payload, key=userless) for method in METHODS)