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