fix(api): tighten Azure certificate cap and route oversized/whitespace inputs through the typed error

This commit is contained in:
Lydia Vilchez committed 2026-08-31 21:31:31 +02:00
1 parent f67cf7c3e5
commit ef62731c0a
3 files changed
+68 -15

No files matched your search

+8 -8
View File
@@ -21318,7 +21318,7 @@ components:
type: string
description: Base64-encoded PEM or PKCS#12/PFX bundle containing the certificate
and its matching private key for certificate-based authentication.
maxLength: 68266
maxLength: 68268
tenant_id:
type: string
description: The Azure tenant ID, representing the directory
@@ -21371,7 +21371,7 @@ components:
type: string
description: Base64-encoded PEM or PKCS#12/PFX bundle containing the certificate
and its matching private key for certificate-based authentication.
maxLength: 68266
maxLength: 68268
required:
- client_id
- tenant_id
@@ -23469,7 +23469,7 @@ components:
type: string
description: Base64-encoded PEM or PKCS#12/PFX bundle containing the certificate
and its matching private key for certificate-based authentication.
maxLength: 68266
maxLength: 68268
tenant_id:
type: string
description: The Azure tenant ID, representing the directory where
@@ -23522,7 +23522,7 @@ components:
type: string
description: Base64-encoded PEM or PKCS#12/PFX bundle containing the certificate
and its matching private key for certificate-based authentication.
maxLength: 68266
maxLength: 68268
required:
- client_id
- tenant_id
@@ -23936,7 +23936,7 @@ components:
type: string
description: Base64-encoded PEM or PKCS#12/PFX bundle containing the certificate
and its matching private key for certificate-based authentication.
maxLength: 68266
maxLength: 68268
tenant_id:
type: string
description: The Azure tenant ID, representing the directory
@@ -23989,7 +23989,7 @@ components:
type: string
description: Base64-encoded PEM or PKCS#12/PFX bundle containing the certificate
and its matching private key for certificate-based authentication.
maxLength: 68266
maxLength: 68268
required:
- client_id
- tenant_id
@@ -24425,7 +24425,7 @@ components:
type: string
description: Base64-encoded PEM or PKCS#12/PFX bundle containing the certificate
and its matching private key for certificate-based authentication.
maxLength: 68266
maxLength: 68268
tenant_id:
type: string
description: The Azure tenant ID, representing the directory where
@@ -24478,7 +24478,7 @@ components:
type: string
description: Base64-encoded PEM or PKCS#12/PFX bundle containing the certificate
and its matching private key for certificate-based authentication.
maxLength: 68266
maxLength: 68268
required:
- client_id
- tenant_id
+38 -3
View File
@@ -370,9 +370,12 @@ class TestAzureProviderSecret:
)
def test_rejects_oversized_certificate_content(self):
# Payloads larger than the base64 cap must be rejected at the DRF
# field layer before base64 decoding or bundle parsing runs, so a
# multi-MB blob cannot exhaust API-worker memory.
# Payloads larger than the base64 cap must be rejected inside
# `validate_certificate_content` (before base64 decoding or bundle
# parsing runs) so a multi-MB blob cannot exhaust API-worker
# memory. The typed `azure-certificate-content` code lets JSON:API
# clients recognize this as a certificate failure rather than a
# generic length violation.
from api.v1.serializers import _MAX_CERTIFICATE_CONTENT_LENGTH
serializer = AzureProviderSecret(
@@ -384,6 +387,38 @@ class TestAzureProviderSecret:
assert not serializer.is_valid()
assert "certificate_content" in serializer.errors
assert (
serializer.errors["certificate_content"][0].code
== "azure-certificate-content"
)
def test_tolerates_whitespace_in_certificate_content(self):
# A base64 payload with embedded whitespace (CRLF from a Windows
# terminal, wrapped copy-paste) must not be rejected as "invalid
# base64" — whitespace carries no information in the encoding.
import base64
bundle_b64 = self.certificate_bundle()
# Insert CRLF every 64 chars and leading/trailing spaces to mimic
# a copy-paste from a terminal export.
wrapped = (
" "
+ "\r\n".join(bundle_b64[i : i + 64] for i in range(0, len(bundle_b64), 64))
+ " "
)
serializer = AzureProviderSecret(
data={**self.BASE, "certificate_content": wrapped}
)
assert serializer.is_valid(), serializer.errors
# The stored value is the whitespace-stripped payload, so the SDK
# sees exactly the bytes it would from a clean base64 upload.
assert serializer.validated_data["certificate_content"] == bundle_b64
# And it still decodes to the original bundle unchanged.
assert base64.b64decode(
serializer.validated_data["certificate_content"]
) == base64.b64decode(bundle_b64)
def test_mutex_errors_carry_stable_codes(self):
# JSON:API clients key on `code`; without it they cannot tell the
+22 -4
View File
@@ -1793,16 +1793,21 @@ class AwsProviderSecret(serializers.Serializer):
# limit on the decoded bundle. Rejects oversized payloads at the request
# layer so DRF never allocates the doubled memory that base64 decoding plus
# PKCS#12/PEM parsing would need for a multi-MB blob.
_MAX_CERTIFICATE_CONTENT_LENGTH = 68266
# `((51200 + 2) // 3) * 4` = 68268 base64 chars for a bundle exactly at the
# SDK cap. Keep the two constants aligned if either side moves.
_MAX_CERTIFICATE_CONTENT_LENGTH = 68268
class AzureProviderSecret(serializers.Serializer):
client_id = serializers.CharField()
client_secret = serializers.CharField(required=False)
tenant_id = serializers.CharField()
certificate_content = serializers.CharField(
required=False, max_length=_MAX_CERTIFICATE_CONTENT_LENGTH
)
# The size cap is enforced in `validate_certificate_content` with the
# typed `azure-certificate-content` code, not via `max_length=`. DRF's
# built-in max-length check runs before per-field validators and emits
# `code="max_length"`, which JSON:API clients keyed to the typed
# certificate error code cannot recognize as a certificate failure.
certificate_content = serializers.CharField(required=False)
def validate(self, attrs):
if attrs.get("client_secret") and attrs.get("certificate_content"):
@@ -1820,6 +1825,19 @@ class AzureProviderSecret(serializers.Serializer):
def validate_certificate_content(self, certificate_content):
"""Validate the Azure certificate and matching private-key bundle."""
if certificate_content:
# Tolerate whitespace inside the base64 payload: exports from
# Windows terminals (CRLF) or wrapped copy-paste survive without
# tripping `base64.b64decode(validate=True)`, and the reader is
# base64 anyway — internal whitespace carries no information.
certificate_content = "".join(certificate_content.split())
if len(certificate_content) > _MAX_CERTIFICATE_CONTENT_LENGTH:
# Reject oversized payloads with the typed certificate code
# so JSON:API clients recognize this as a certificate-content
# failure rather than a generic length violation.
raise serializers.ValidationError(
"Certificate content exceeds the maximum size.",
code="azure-certificate-content",
)
try:
certificate_data = base64.b64decode(certificate_content, validate=True)
validate_certificate_bundle(certificate_data)