From 8246007d92e2ac1081b1cb3b0f3d69530ec6e099 Mon Sep 17 00:00:00 2001 From: Thilo Schwarz Date: Mon, 18 Aug 2025 20:12:40 +0200 Subject: [PATCH] Add `CfClientPenTest` for penetration testing and enhance `CfClientTest` with environment checks --- README.md | 6 - .../codes/thischwa/cf/CfClientPenTest.java | 115 ++++++++++++++++++ .../java/codes/thischwa/cf/CfClientTest.java | 12 +- 3 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 src/test/java/codes/thischwa/cf/CfClientPenTest.java diff --git a/README.md b/README.md index f1d7460..8841bb2 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,6 @@ This guide comes without any warranty. Use at your own risk. The author is not r --- -## State of the Project - -BETA - ---- - ## Get It The project has its own maven repository. It can be added to the `pom.xml`: diff --git a/src/test/java/codes/thischwa/cf/CfClientPenTest.java b/src/test/java/codes/thischwa/cf/CfClientPenTest.java new file mode 100644 index 0000000..8df0b34 --- /dev/null +++ b/src/test/java/codes/thischwa/cf/CfClientPenTest.java @@ -0,0 +1,115 @@ +package codes.thischwa.cf; + +import codes.thischwa.cf.model.RecordType; +import codes.thischwa.cf.model.ZoneEntity; +import java.util.List; +import java.util.UUID; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Penetration-style tests with malicious and invalid inputs to ensure CfDnsClient behaves safely + * (throws appropriate exceptions, does not crash, and does not create unintended resources). + *

+ * These tests will be skipped if API_EMAIL or API_KEY are not provided via environment variables. + */ +public class CfClientPenTest { + + private static final String ZONE_STR = "mein-d-ns.de"; // existing baseline zone + + private static final String API_EMAIL = System.getenv("API_EMAIL"); + private static final String API_KEY = System.getenv("API_KEY"); + + @BeforeAll + static void checkEnv() { + assumeTrue(API_EMAIL != null && !API_EMAIL.isBlank(), "API_EMAIL not set; skipping pen tests"); + assumeTrue(API_KEY != null && !API_KEY.isBlank(), "API_KEY not set; skipping pen tests"); + } + + private CfDnsClient newClient() { + return new CfDnsClient(API_EMAIL, API_KEY); + } + + @Test + @DisplayName("Invalid credentials should not authenticate and must throw CloudflareApiException") + void testInvalidCredentialsShouldFail() { + // Use syntactically valid but wrong credentials + CfDnsClient badClient = new CfDnsClient("invalid@example.com", UUID.randomUUID().toString()); + assertThrows(CloudflareApiException.class, badClient::zoneListAll); + } + + @Test + @DisplayName("Malicious SLD inputs must not crash and should throw proper exception type") + void testMaliciousSldPatternsDoNotSucceed() throws Exception { + CfDnsClient client = newClient(); + ZoneEntity zone = client.zoneInfo(ZONE_STR); + + List syntacticallyInvalidSlds = + List.of("; rm -rf /", "| cat /etc/passwd", "`shutdown -h now`", + "", "\"quoted\""); + + List syntacticallyValidOrNotAllowedFromCloudflare = + List.of(".", "..", "../..", "..%2F..%2F", "a".repeat(300), "$(reboot)", "emoji-🥷-忍者", + "doesnotexist", "abcdef12345", "unwahrscheinlich-" + System.currentTimeMillis()); + + for (String sld : syntacticallyInvalidSlds) { + assertThrows(IllegalArgumentException.class, () -> client.sldListAll(zone, sld), + "Should throw IllegalArgumentException for invalid SLD '" + sld + "'"); + } + for (String sld : syntacticallyValidOrNotAllowedFromCloudflare) { + assertThrows(CloudflareNotFoundException.class, () -> client.sldListAll(zone, sld), + "Should throw CloudflareNotFoundException for valid but non-existing SLD '" + sld + "'"); + } + } + + @Test + @DisplayName("Invalid record content and TTL boundaries must be rejected by API") + void testInvalidRecordCreateInputsRejected() throws Exception { + CfDnsClient client = newClient(); + ZoneEntity zone = client.zoneInfo(ZONE_STR); + + String sld = "pentest-" + System.currentTimeMillis(); + String fqdn = sld + "." + ZONE_STR; + + // Ensure clean state and guarantee cleanup later + try { + client.recordDeleteTypeIfExists(zone, sld, RecordType.A, RecordType.AAAA, RecordType.CNAME); + } catch (Exception ignored) { + } + + try { + // A record with invalid IPv4 + assertThrows(CloudflareApiException.class, + () -> client.recordCreate(zone, fqdn, 60, RecordType.A, "999.999.999.999")); + + // AAAA record with non-IP content + assertThrows(CloudflareApiException.class, + () -> client.recordCreate(zone, fqdn, 60, RecordType.AAAA, "not-an-ipv6")); + + // TTL boundary checks + assertThrows(CloudflareApiException.class, + () -> client.recordCreate(zone, fqdn, -1, RecordType.A, "130.0.0.3")); + } finally { + // Best-effort cleanup in case anything slipped through + assertDoesNotThrow( + () -> client.recordDeleteTypeIfExists(zone, sld, RecordType.A, RecordType.AAAA, + RecordType.CNAME)); + } + } + + @Test + @DisplayName("recordDeleteTypeIfExists must be safe on non-existing SLD and types") + void testDeleteTypeIfExistsOnNonExistingIsSafe() throws Exception { + CfDnsClient client = newClient(); + ZoneEntity zone = client.zoneInfo(ZONE_STR); + String randomSld = "nonexist-" + System.currentTimeMillis(); + // Should not throw even if nothing exists + assertDoesNotThrow( + () -> client.recordDeleteTypeIfExists(zone, randomSld, RecordType.A, RecordType.AAAA)); + } +} diff --git a/src/test/java/codes/thischwa/cf/CfClientTest.java b/src/test/java/codes/thischwa/cf/CfClientTest.java index aea921e..4909755 100644 --- a/src/test/java/codes/thischwa/cf/CfClientTest.java +++ b/src/test/java/codes/thischwa/cf/CfClientTest.java @@ -10,7 +10,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @Slf4j @@ -25,6 +27,12 @@ public class CfClientTest { private final CfDnsClient client = new CfDnsClient(API_EMAIL, API_KEY); + @BeforeAll + static void checkEnv() { + assumeTrue(API_EMAIL != null && !API_EMAIL.isBlank(), "API_EMAIL not set; skipping pen tests"); + assumeTrue(API_KEY != null && !API_KEY.isBlank(), "API_KEY not set; skipping pen tests"); + } + @Test void testZoneListAnlFailedSldList() throws Exception { List zList = client.zoneListAll(); @@ -62,8 +70,8 @@ public class CfClientTest { String domain = randomSld + "." + ZONE_STR; RecordEntity r; - RecordEntity createdRe1 = null; - RecordEntity createdRe2 = null; + RecordEntity createdRe1; + RecordEntity createdRe2; try { // ensure clean state