From bb7e216e08210babd3b0a783901f347dbf3bfa0f Mon Sep 17 00:00:00 2001 From: Francisc Balint <7178547+franciscbalint@users.noreply.github.com> Date: Thu, 20 Aug 2026 00:41:23 +0400 Subject: [PATCH] Harden HTTPS base URLs and fetch timeouts. Reject non-https custom apiBaseURL values and abort fetch after 120s when AbortSignal.timeout is available. --- README.md | 7 +++++++ src/utils.js | 17 +++++++++++++++++ src/zero-bounce.js | 4 ++-- tests/zero-bounce.test.js | 8 ++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1bbbda9..b3a6f81 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ [ZeroBounce](https://www.zerobounce.net) JavaScript API v2 + +## 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. + ## WE DO NOT RECOMMEND USING THIS SDK ON A FRONT-END ENVIRONMENT AS THE API KEY WILL BE VISIBLE This is a JavaScript wrapper class for the ZeroBounce API v2. diff --git a/src/utils.js b/src/utils.js index 2821f66..bb82b5b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,11 +1,27 @@ // Constants export const API_BULK_BASE_URL = "https://bulkapi.zerobounce.net/v2"; +export const HTTP_TIMEOUT_MS = 120000; export const HEADERS = { Accept: "*/*", "Accept-Encoding": "gzip, deflate, br", Connection: "keep-alive", }; +export function requireHttpsUrl(url) { + if (typeof url !== "string" || !/^https:\/\//i.test(url)) { + parameterIsInvalid("apiBaseURL", "Must be an https:// URL."); + throw new Error("apiBaseURL must be an https:// URL"); + } + return url; +} + +function fetchTimeoutSignal() { + if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") { + return AbortSignal.timeout(HTTP_TIMEOUT_MS); + } + return undefined; +} + function nonEmptyStringField(o, key) { const v = o[key]; if (v == null) return false; @@ -100,6 +116,7 @@ export async function createRequest({ method: requestType, headers: HEADERS, body, + signal: fetchTimeoutSignal(), }); if (returnText) { const finalResult = await response.text(); diff --git a/src/zero-bounce.js b/src/zero-bounce.js index cdb5404..786c6d4 100644 --- a/src/zero-bounce.js +++ b/src/zero-bounce.js @@ -1,4 +1,4 @@ -import { createRequest, notInitialized, parameterIsMissing, parameterIsInvalid } from "./utils.js"; +import { createRequest, notInitialized, parameterIsMissing, parameterIsInvalid, requireHttpsUrl } from "./utils.js"; /** * Validation status values returned by the API (validate, validateBatch). @@ -94,7 +94,7 @@ export class ZeroBounceSDK { parameterIsMissing("Api key", "Please provide a valid API key."); } else { this._api_key = apiKey; - this._api_base_url = apiBaseURL; + this._api_base_url = requireHttpsUrl(apiBaseURL); this._initialized = true; } } diff --git a/tests/zero-bounce.test.js b/tests/zero-bounce.test.js index b555b7b..c041cb4 100644 --- a/tests/zero-bounce.test.js +++ b/tests/zero-bounce.test.js @@ -38,6 +38,14 @@ describe("ZeroBounceSDK", () => { }); }); + describe("init security", () => { + it("rejects a non-https apiBaseURL", () => { + expect(() => zeroBounceSDK.init("valid-api-key", "http://evil.example/v2")).toThrow( + "apiBaseURL must be an https:// URL" + ); + }); + }); + describe("getCredits", () => { it("should throw an error if not initialized", async () => { await zeroBounceSDK.getCredits();