From d35e5a4bece1bfadc36cf8c0ff1d99c162fc2559 Mon Sep 17 00:00:00 2001 From: HugoPBrito Date: Mon, 2 Mar 2026 10:14:13 +0100 Subject: [PATCH] fix: enforce cloudflare token/key format validation - Reject API key-shaped values in Cloudflare api_token - Validate Cloudflare api_key as 32-character hexadecimal format - Add UI form validation to prevent token/key cross-input - Update API and UI changelog entries for the current PR --- api/CHANGELOG.md | 2 +- api/src/backend/api/tests/test_serializers.py | 25 ++++++++++++++++++- api/src/backend/api/v1/serializers.py | 8 ++++++ ui/CHANGELOG.md | 1 + ui/types/formSchemas.ts | 20 +++++++++++++++ 5 files changed, 54 insertions(+), 2 deletions(-) diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 85473157d3..c6aafd1de3 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -38,7 +38,7 @@ All notable changes to the **Prowler API** are documented in this file. - Attack Paths: scan no longer raises `DatabaseError` when provider is deleted mid-scan [(#10116)](https://github.com/prowler-cloud/prowler/pull/10116) - Tenant compliance summaries recalculated after provider deletion [(#10172)](https://github.com/prowler-cloud/prowler/pull/10172) - Security Hub export retries transient replica conflicts without failing integrations [(#10144)](https://github.com/prowler-cloud/prowler/pull/10144) -- Cloudflare provider secrets now reject API key format in `api_token` credentials [(#10195)](https://github.com/prowler-cloud/prowler/pull/10195) +- Cloudflare provider secrets now reject API key format in `api_token` and non-key values in `api_key` credentials [(#10195)](https://github.com/prowler-cloud/prowler/pull/10195) ### 🔐 Security diff --git a/api/src/backend/api/tests/test_serializers.py b/api/src/backend/api/tests/test_serializers.py index 613114a92d..a141d1fd78 100644 --- a/api/src/backend/api/tests/test_serializers.py +++ b/api/src/backend/api/tests/test_serializers.py @@ -2,7 +2,11 @@ import pytest from rest_framework.exceptions import ValidationError from api.v1.serializer_utils.integrations import S3ConfigSerializer -from api.v1.serializers import CloudflareTokenProviderSecret, ImageProviderSecret +from api.v1.serializers import ( + CloudflareApiKeyProviderSecret, + CloudflareTokenProviderSecret, + ImageProviderSecret, +) class TestS3ConfigSerializer: @@ -150,3 +154,22 @@ class TestCloudflareProviderSecret: ) assert not serializer.is_valid() assert "api_token" in serializer.errors + + def test_valid_api_key_and_email(self): + serializer = CloudflareApiKeyProviderSecret( + data={ + "api_key": "144c9defac04969c7bfad8efaa8ea194", + "api_email": "user@example.com", + } + ) + assert serializer.is_valid(), serializer.errors + + def test_invalid_api_key_with_token_format(self): + serializer = CloudflareApiKeyProviderSecret( + data={ + "api_key": "Sn3lZJTBX6kkg7OdcBUAxOO963GEIyGQqnFTOFYY", + "api_email": "user@example.com", + } + ) + assert not serializer.is_valid() + assert "api_key" in serializer.errors diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index 7ffdb97f15..d0afd4ad3a 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -1722,6 +1722,14 @@ class CloudflareApiKeyProviderSecret(serializers.Serializer): api_key = serializers.CharField() api_email = serializers.EmailField() + def validate_api_key(self, value: str) -> str: + if not re.fullmatch(r"[a-fA-F0-9]{32}", (value or "").strip()): + raise serializers.ValidationError( + "Invalid Cloudflare API Key format. " + "Use a 32-character hexadecimal Global API Key." + ) + return value + class Meta: resource_name = "provider-secrets" diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 7afc6f1ae7..f0a7d8b885 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -25,6 +25,7 @@ All notable changes to the **Prowler UI** are documented in this file. ### 🐞 Fixed - Findings Severity Over Time chart on Overview not responding to provider and account filters, and chart clipping at Y-axis maximum values [(#10103)](https://github.com/prowler-cloud/prowler/pull/10103) +- Cloudflare credentials form now blocks API key values in `api_token` and token-like values in `api_key` [(#10195)](https://github.com/prowler-cloud/prowler/pull/10195) ### 🔐 Security diff --git a/ui/types/formSchemas.ts b/ui/types/formSchemas.ts index 9c4cc58f41..cea3913610 100644 --- a/ui/types/formSchemas.ts +++ b/ui/types/formSchemas.ts @@ -5,6 +5,8 @@ import { validateMutelistYaml, validateYaml } from "@/lib/yaml"; import { PROVIDER_TYPES, ProviderType } from "./providers"; +const CLOUDFLARE_GLOBAL_API_KEY_REGEX = /^[a-fA-F0-9]{32}$/; + export const addRoleFormSchema = z.object({ name: z.string().min(1, "Name is required"), manage_users: z.boolean().default(false), @@ -379,6 +381,15 @@ export const addCredentialsFormSchema = ( message: "API Token is required", path: [ProviderCredentialFields.CLOUDFLARE_API_TOKEN], }); + } else if ( + CLOUDFLARE_GLOBAL_API_KEY_REGEX.test(apiToken.trim()) + ) { + ctx.addIssue({ + code: "custom", + message: + "This looks like an API Key. Use API Token credentials instead.", + path: [ProviderCredentialFields.CLOUDFLARE_API_TOKEN], + }); } } else if (via === "api_key") { const apiKey = data[ProviderCredentialFields.CLOUDFLARE_API_KEY]; @@ -389,6 +400,15 @@ export const addCredentialsFormSchema = ( message: "API Key is required", path: [ProviderCredentialFields.CLOUDFLARE_API_KEY], }); + } else if ( + !CLOUDFLARE_GLOBAL_API_KEY_REGEX.test(apiKey.trim()) + ) { + ctx.addIssue({ + code: "custom", + message: + "API Key must be a 32-character hexadecimal Cloudflare Global API Key.", + path: [ProviderCredentialFields.CLOUDFLARE_API_KEY], + }); } if (!apiEmail || apiEmail.trim() === "") { ctx.addIssue({