From 116fb7083d0703835a965a0c6274497907cb2859 Mon Sep 17 00:00:00 2001 From: StylusFrost Date: Mon, 1 Jun 2026 21:35:34 +0200 Subject: [PATCH] fix(api): reject non-object external provider secret - Validate the secret is a JSON object before the no-schema path accepts it, so a list/string/null cannot be persisted and fail later at {**secret} - Add parametrized coverage for list/string/null/int payloads --- api/src/backend/api/tests/test_serializers.py | 15 +++++++++++++++ api/src/backend/api/v1/serializers.py | 2 ++ 2 files changed, 17 insertions(+) diff --git a/api/src/backend/api/tests/test_serializers.py b/api/src/backend/api/tests/test_serializers.py index 3a930c37ce..a2feb4e498 100644 --- a/api/src/backend/api/tests/test_serializers.py +++ b/api/src/backend/api/tests/test_serializers.py @@ -52,6 +52,21 @@ class TestExternalProviderSecretValidation: "external-template", {"anything": "goes"} ) + @pytest.mark.parametrize("bad_secret", [["a", "b"], "a-string", None, 42]) + def test_secret_rejected_when_not_a_json_object(self, bad_secret): + """Even with no declared schema, a non-object secret must be rejected so + a list/string/null cannot be persisted and blow up later at + ``{**secret}``. See PR #11402 review (Alan-TheGentleman).""" + provider_class = MagicMock() + provider_class.get_credentials_schema.return_value = [] + with patch( + "api.v1.serializers.SDKProvider.get_class", return_value=provider_class + ): + with pytest.raises(ValidationError): + BaseWriteProviderSecretSerializer._validate_external_provider_secret( + "external-template", bad_secret + ) + class TestProviderEnumSerializerField: """The provider field accepts whatever the SDK exposes (built-in or diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index 5d71d2c2e2..0a8d31cd1e 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -1552,6 +1552,8 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer): Providers that declare no schema have their secret accepted as-is; the credentials are then validated by the provider's ``test_connection``. """ + if not isinstance(secret, dict): + raise serializers.ValidationError({"secret": ["Must be a JSON object."]}) schemas = SDKProvider.get_class(provider_type).get_credentials_schema() if not schemas: return