Add CfClientPenTest for penetration testing and enhance CfClientTest with environment checks

This commit is contained in:
2025-08-18 20:12:40 +02:00
parent 997d0df060
commit 8246007d92
3 changed files with 125 additions and 8 deletions
-6
View File
@@ -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`:
@@ -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).
* <p>
* 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<String> syntacticallyInvalidSlds =
List.of("; rm -rf /", "| cat /etc/passwd", "`shutdown -h now`",
"<script>alert('x')</script>", "\"quoted\"");
List<String> 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));
}
}
@@ -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<ZoneEntity> 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