diff --git a/README.md b/README.md index 5d720e9..f16962b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ ## ZeroBounce Java SDK + +## Security + +- Keep API keys on a trusted server. Do not embed them in mobile apps or browser JavaScript that untrusted users can inspect. +- Custom API base URLs (when supported) must use `https://`. Do not pass end-user-controlled hosts into those settings. +- Request URLs include `api_key` as a query parameter (ZeroBounce API contract). Do not log full request URLs or enable payload debug logging in production. + [![Maven Central](https://img.shields.io/maven-central/v/com.zerobounce.java/zerobouncesdk?label=Latest%20release)](https://mvnrepository.com/artifact/com.zerobounce.java/zerobouncesdk) This SDK contains methods for interacting easily with ZeroBounce API. diff --git a/zero-bounce-sdk/src/main/java/com/zerobounce/ZeroBounceSDK.java b/zero-bounce-sdk/src/main/java/com/zerobounce/ZeroBounceSDK.java index af84265..0e8f004 100644 --- a/zero-bounce-sdk/src/main/java/com/zerobounce/ZeroBounceSDK.java +++ b/zero-bounce-sdk/src/main/java/com/zerobounce/ZeroBounceSDK.java @@ -77,6 +77,8 @@ public static void setLogPayloads(boolean enabled) { logPayloads = enabled; } + private static final int DEFAULT_TIMEOUT_MILLIS = 120_000; + String apiBaseUrl = "https://api.zerobounce.net/v2"; private final String bulkApiBaseUrl = "https://bulkapi.zerobounce.net/v2"; private final String bulkApiScoringBaseUrl = "https://bulkapi.zerobounce.net/v2/scoring"; @@ -97,13 +99,13 @@ private ZeroBounceSDK() { /** * Initializes the SDK. *

- * [timeoutInMillis] is set to 0 (no timeout by default) + * [timeoutInMillis] is set to 120000 (120 seconds) by default * * @param apiKey the API key */ public void initialize(String apiKey) { this.apiKey = apiKey; - timeoutInMillis = 0; + timeoutInMillis = DEFAULT_TIMEOUT_MILLIS; } /** @@ -125,11 +127,19 @@ public void initialize(String apiKey, int timeoutInMillis) { */ public void initialize(String apiKey, @Nullable String apiBaseUrl) { this.apiKey = apiKey; + timeoutInMillis = DEFAULT_TIMEOUT_MILLIS; if (apiBaseUrl != null) { + requireHttpsUrl(apiBaseUrl); this.apiBaseUrl = apiBaseUrl; } } + static void requireHttpsUrl(String url) { + if (url == null || !url.regionMatches(true, 0, "https://", 0, 8)) { + throw new IllegalArgumentException("apiBaseUrl must be an https:// URL"); + } + } + /** * Validates the given [email]. * @@ -1181,6 +1191,7 @@ private void sendRequest( logDebug("ZeroBounceSDK::sendRequest executing " + httpMethod + " " + urlPath); con.setConnectTimeout(timeoutInMillis); + con.setReadTimeout(timeoutInMillis); int status = con.getResponseCode(); logDebug("ZeroBounceSDK::sendRequest status: " + status); diff --git a/zero-bounce-sdk/src/test/java/com/zerobounce/ZeroBounceSDKTest.java b/zero-bounce-sdk/src/test/java/com/zerobounce/ZeroBounceSDKTest.java index 5448978..2fe0727 100644 --- a/zero-bounce-sdk/src/test/java/com/zerobounce/ZeroBounceSDKTest.java +++ b/zero-bounce-sdk/src/test/java/com/zerobounce/ZeroBounceSDKTest.java @@ -61,6 +61,18 @@ public void setUp() { zeroBounceSDK.initialize(API_KEY); } + @Test + public void requireHttpsUrl_rejectsHttp() { + assertThrows(IllegalArgumentException.class, () -> ZeroBounceSDK.requireHttpsUrl("http://example.com")); + assertThrows(IllegalArgumentException.class, () -> ZeroBounceSDK.requireHttpsUrl("file:///etc/passwd")); + ZeroBounceSDK.requireHttpsUrl("https://api.zerobounce.net/v2"); + } + + @Test + public void initialize_rejectsNonHttpsBaseUrl() { + assertThrows(IllegalArgumentException.class, () -> zeroBounceSDK.initialize(API_KEY, "http://evil.example")); + } + @Test public void validateEmail_ReturnsSuccess() throws Exception { // Prepare mock response and add it to the server