diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index aeae2c5..b3c2494 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,63 +1,74 @@
-# To get started with Dependabot version updates, you'll need to specify which
-# package ecosystems to update and where the package manifests are located.
-# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#versioning-strategy
+# Dependabot: security updates only (no version-update PRs).
+# Grouped security PRs open weekly on Tuesday morning (Europe/Bucharest).
+# Auto-merge: .github/workflows/dependabot-auto-merge.yml (on PR + Tuesday cron).
+# https://docs.github.com/en/code-security/dependabot/dependabot-security-updates
version: 2
updates:
- # Keep the NuGet dependencies up to date
- package-ecosystem: "nuget"
directory: "/ZeroBounceSDK/"
schedule:
- interval: "weekly"
- day: "monday"
- target-branch: "master"
+ interval: weekly
+ day: tuesday
+ time: "06:00"
+ timezone: Europe/Bucharest
+ open-pull-requests-limit: 0
labels:
+ - "dependabot-security"
- "nuget dependencies"
groups:
- nuget:
- applies-to: version-updates
+ nuget-sdk-security:
+ applies-to: security-updates
patterns:
- "*"
- package-ecosystem: "nuget"
directory: "/ZeroBounceTests/"
schedule:
- interval: "weekly"
- day: "monday"
- target-branch: "master"
+ interval: weekly
+ day: tuesday
+ time: "06:00"
+ timezone: Europe/Bucharest
+ open-pull-requests-limit: 0
labels:
+ - "dependabot-security"
- "nuget dependencies"
groups:
- nuget:
- applies-to: version-updates
+ nuget-tests-security:
+ applies-to: security-updates
patterns:
- "*"
- package-ecosystem: "nuget"
directory: "/ZeroBounceSample/"
schedule:
- interval: "weekly"
- day: "monday"
- target-branch: "master"
+ interval: weekly
+ day: tuesday
+ time: "06:00"
+ timezone: Europe/Bucharest
+ open-pull-requests-limit: 0
labels:
+ - "dependabot-security"
- "nuget dependencies"
groups:
- nuget:
- applies-to: version-updates
+ nuget-sample-security:
+ applies-to: security-updates
patterns:
- "*"
- # Keep the Github Actions dependencies up to date
- package-ecosystem: "github-actions"
directory: "/"
schedule:
- interval: "weekly"
- day: "monday"
- target-branch: "master"
+ interval: weekly
+ day: tuesday
+ time: "06:00"
+ timezone: Europe/Bucharest
+ open-pull-requests-limit: 0
labels:
+ - "dependabot-security"
- "github dependencies"
groups:
- github:
- applies-to: version-updates
+ github-actions-security:
+ applies-to: security-updates
patterns:
- "*"
diff --git a/.github/workflows/auto_assign_ci.yaml b/.github/workflows/auto_assign_ci.yaml
index 0e6c515..59aa47c 100644
--- a/.github/workflows/auto_assign_ci.yaml
+++ b/.github/workflows/auto_assign_ci.yaml
@@ -1,4 +1,5 @@
name: Auto Assign
+# Audit 2026-10-14: pull_request_target — do not checkout the PR head here; configuration-path only.
on:
pull_request_target:
@@ -16,6 +17,6 @@ jobs:
name: Auto assign (me only)
runs-on: ubuntu-latest
steps:
- - uses: kentaro-m/auto-assign-action@v2.0.2
+ - uses: kentaro-m/auto-assign-action@0a2c53d3721e4c5cfcce6afd4014aecc337979f6 # v2.0.2
with:
configuration-path: ".github/auto_assign.yml"
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 1e8dbe7..99b6493 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -61,7 +61,7 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
- uses: actions/checkout@v7
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
@@ -71,7 +71,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v4.37.8
+ uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -100,6 +100,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v4.37.8
+ uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
with:
category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml
new file mode 100644
index 0000000..138a59d
--- /dev/null
+++ b/.github/workflows/dependabot-auto-merge.yml
@@ -0,0 +1,50 @@
+name: Dependabot security auto-merge
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened]
+ schedule:
+ - cron: "0 6 * * 2"
+ timezone: Europe/Bucharest
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ approve-and-queue-merge:
+ runs-on: ubuntu-latest
+ if: >-
+ github.event_name == 'pull_request'
+ && github.event.pull_request.user.login == 'dependabot[bot]'
+ steps:
+ - name: Approve Dependabot pull request
+ run: gh pr review --approve "$PR_URL"
+ env:
+ PR_URL: ${{ github.event.pull_request.html_url }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Enable auto-merge when checks pass
+ run: gh pr merge --auto --merge "$PR_URL"
+ env:
+ PR_URL: ${{ github.event.pull_request.html_url }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ weekly-merge-ready:
+ runs-on: ubuntu-latest
+ if: github.event_name == 'schedule'
+ steps:
+ - name: Approve and queue merge for open Dependabot PRs
+ run: |
+ set -euo pipefail
+ mapfile -t urls < <(gh pr list --author app/dependabot --state open --json url --jq '.[].url')
+ if [ "${#urls[@]}" -eq 0 ]; then
+ echo "No open Dependabot pull requests."
+ exit 0
+ fi
+ for url in "${urls[@]}"; do
+ gh pr review --approve "$url" || true
+ gh pr merge --auto --merge "$url" || true
+ done
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index b971564..fea3865 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -13,14 +13,27 @@ permissions:
jobs:
validate-tag:
+ outputs:
+ tag_sha: ${{ steps.tag_sha.outputs.sha }}
name: Validate tag
runs-on: ubuntu-latest
steps:
+ - name: Validate tag input
+ env:
+ TAG: ${{ github.event.inputs.tag }}
+ run: |
+ re='^v?[0-9]+\.[0-9]+\.[0-9]+([.+-][0-9A-Za-z.-]+)?$'
+ if [[ ! "$TAG" =~ $re ]]; then
+ echo "Error: the tag input must be a release tag such as v1.2.3."
+ exit 1
+ fi
+
- name: Checkout
- uses: actions/checkout@v7
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
-
+ ref: refs/tags/${{ github.event.inputs.tag }}
+ persist-credentials: false
- name: Verify tag exists
run: |
TAG="${{ github.event.inputs.tag }}"
@@ -28,11 +41,18 @@ jobs:
echo "Error: Tag is required."
exit 1
fi
- if ! git rev-parse "$TAG" >/dev/null 2>&1; then
+ if ! git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
echo "Error: Tag '$TAG' does not exist."
exit 1
fi
- echo "Publishing tag: $TAG"
+
+ - name: Record tag commit SHA
+ id: tag_sha
+ env:
+ TAG: ${{ github.event.inputs.tag }}
+ run: |
+ SHA=$(git rev-parse "refs/tags/$TAG^{commit}")
+ echo "sha=$SHA" >> "$GITHUB_OUTPUT"
publish:
name: Test, pack and publish to NuGet
@@ -45,12 +65,12 @@ jobs:
contents: read
steps:
- name: Checkout tag
- uses: actions/checkout@v7
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
- ref: ${{ github.event.inputs.tag }}
-
+ ref: ${{ needs.validate-tag.outputs.tag_sha }}
+ persist-credentials: false
- name: Set up .NET
- uses: actions/setup-dotnet@v6
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
with:
dotnet-version: "8.0.x"
@@ -69,7 +89,7 @@ jobs:
dotnet pack ZeroBounceSDK/ZeroBounceSDK.csproj -c Release --no-build
- name: NuGet login (trusted publishing)
- uses: NuGet/login@v1
+ uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1
id: login
with:
user: ${{ secrets.NUGET_USER || 'ZeroBounceIntegrations' }}
@@ -97,28 +117,29 @@ jobs:
release:
name: Create GitHub Release
- needs: [publish]
+ needs: [validate-tag, publish]
if: ${{ !cancelled() && !failure() }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
- uses: actions/checkout@v7
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
- ref: ${{ github.event.inputs.tag }}
+ ref: ${{ needs.validate-tag.outputs.tag_sha }}
fetch-depth: 0
+ persist-credentials: false
- name: Create release if missing
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${{ github.event.inputs.tag }}"
- if gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then
+ if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "Release $TAG already exists, skipping."
else
gh release create "$TAG" \
- --repo "${{ github.repository }}" \
+ --repo "$GITHUB_REPOSITORY" \
--title "$TAG" \
--generate-notes
fi
diff --git a/.github/workflows/sdk_ci.yml b/.github/workflows/sdk_ci.yml
index 57f6f56..44dd41b 100644
--- a/.github/workflows/sdk_ci.yml
+++ b/.github/workflows/sdk_ci.yml
@@ -15,10 +15,12 @@ jobs:
name: Test
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v7
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
- name: Set up .NET
- uses: actions/setup-dotnet@v6
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
with:
dotnet-version: "8.0.x"
diff --git a/Dockerfile b/Dockerfile
index 2adf5cc..4e584e9 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,4 +2,7 @@
# Mount the repo at /app (see sdk-docs/docker-compose.yml). Do not rely on COPY for local workflows.
FROM mcr.microsoft.com/dotnet/sdk:8.0
WORKDIR /app
+
+USER 65532:65532
+
CMD ["dotnet", "test", "ZeroBounceTests/ZeroBounceTests.csproj", "-v", "normal"]
diff --git a/ZeroBounceSDK/ZeroBounce.cs b/ZeroBounceSDK/ZeroBounce.cs
index bc0cea0..7fc1496 100644
--- a/ZeroBounceSDK/ZeroBounce.cs
+++ b/ZeroBounceSDK/ZeroBounce.cs
@@ -42,6 +42,23 @@ private static void RequireHttpsUrl(string url)
}
}
+ private static string Enc(string value) => Uri.EscapeDataString(value ?? "");
+
+ private static string RedactApiKey(string message, string apiKey)
+ {
+ if (string.IsNullOrEmpty(apiKey) || string.IsNullOrEmpty(message))
+ return message;
+ return message.Replace(apiKey, "***");
+ }
+
+ private static string SafeMultipartFileName(string fileName)
+ {
+ var baseName = Path.GetFileName(fileName ?? "upload.csv");
+ if (string.IsNullOrWhiteSpace(baseName))
+ baseName = "upload.csv";
+ return baseName.Replace("\"", "").Replace("\r", "").Replace("\n", "");
+ }
+
///
/// Initialize the SDK with your API key. Uses default API base URL.
///
@@ -89,7 +106,7 @@ public void Validate(string email, string ipAddress, Action
if (InvalidApiKey(failureCallback)) return;
_sendRequest(
- _apiBaseUrl + "/validate?api_key=" + _apiKey + "&email=" + email + "&ip_address=" + (ipAddress ?? ""),
+ _apiBaseUrl + "/validate?api_key=" + Enc(_apiKey) + "&email=" + Enc(email) + "&ip_address=" + Enc(ipAddress ?? ""),
successCallback,
failureCallback).Wait();
}
@@ -132,7 +149,7 @@ public void GetCredits(Action successCallback, Action
{
if (InvalidApiKey(failureCallback)) return;
- _sendRequest(_apiBaseUrl + "/getcredits?api_key=" + _apiKey,
+ _sendRequest(_apiBaseUrl + "/getcredits?api_key=" + Enc(_apiKey),
successCallback, failureCallback).Wait();
}
@@ -146,9 +163,9 @@ public void GetApiUsage(
if (InvalidApiKey(failureCallback)) return;
_sendRequest(
- _apiBaseUrl + "/getapiusage?api_key=" + _apiKey
- + "&start_date=" + startDate.ToString("yyyy-MM-dd")
- + "&end_date=" + endDate.ToString("yyyy-MM-dd"),
+ _apiBaseUrl + "/getapiusage?api_key=" + Enc(_apiKey)
+ + "&start_date=" + Enc(startDate.ToString("yyyy-MM-dd"))
+ + "&end_date=" + Enc(endDate.ToString("yyyy-MM-dd")),
successCallback,
failureCallback).Wait();
}
@@ -160,8 +177,8 @@ public void GetActivity(string email, Action successCallback
if (InvalidApiKey(failureCallback)) return;
_sendRequest(
- _apiBaseUrl + "/activity?api_key=" + _apiKey
- + "&email=" + email,
+ _apiBaseUrl + "/activity?api_key=" + Enc(_apiKey)
+ + "&email=" + Enc(email),
successCallback,
failureCallback).Wait();
}
@@ -249,21 +266,21 @@ private void _FindEmail(
return;
}
- string url = _apiBaseUrl + "/guessformat?api_key=" + _apiKey;
+ string url = _apiBaseUrl + "/guessformat?api_key=" + Enc(_apiKey);
if (domain != null) {
- url += "&domain=" + (domain ?? "");
+ url += "&domain=" + Enc(domain ?? "");
}
if (companyName != null) {
- url += "&company_name=" + (companyName ?? "");
+ url += "&company_name=" + Enc(companyName ?? "");
}
if (firstName != null) {
- url += "&first_name=" + (firstName ?? "");
+ url += "&first_name=" + Enc(firstName ?? "");
}
if (middleName != null) {
- url += "&middle_name=" + (middleName ?? "");
+ url += "&middle_name=" + Enc(middleName ?? "");
}
if (lastName != null) {
- url += "&last_name=" + (lastName ?? "");
+ url += "&last_name=" + Enc(lastName ?? "");
}
_sendRequest(
@@ -308,12 +325,12 @@ private void _FindDomain(
return;
}
- string url = _apiBaseUrl + "/guessformat?api_key=" + _apiKey;
+ string url = _apiBaseUrl + "/guessformat?api_key=" + Enc(_apiKey);
if (domain != null) {
- url += "&domain=" + (domain ?? "");
+ url += "&domain=" + Enc(domain ?? "");
}
if (companyName != null) {
- url += "&company_name=" + (companyName ?? "");
+ url += "&company_name=" + Enc(companyName ?? "");
}
_sendRequest(
@@ -336,11 +353,11 @@ public void EmailFinder(
if (InvalidApiKey(failureCallback)) return;
_sendRequest(
- _apiBaseUrl + "/guessformat?api_key=" + _apiKey
- + "&domain=" + (domain ?? "")
- + "&first_name=" + (firstName ?? "")
- + "&middle_name=" + (middleName ?? "")
- + "&last_name=" + (lastName ?? ""),
+ _apiBaseUrl + "/guessformat?api_key=" + Enc(_apiKey)
+ + "&domain=" + Enc(domain ?? "")
+ + "&first_name=" + Enc(firstName ?? "")
+ + "&middle_name=" + Enc(middleName ?? "")
+ + "&last_name=" + Enc(lastName ?? ""),
successCallback,
failureCallback).Wait();
}
@@ -374,7 +391,7 @@ private void _FileStatus(bool scoring, string fileId,
}
_sendRequest(
- BulkApiBaseUrl + (scoring ? "/scoring" : "") + "/filestatus?api_key=" + _apiKey + "&file_id=" + fileId,
+ BulkApiBaseUrl + (scoring ? "/scoring" : "") + "/filestatus?api_key=" + Enc(_apiKey) + "&file_id=" + Enc(fileId),
successCallback, failureCallback).Wait();
}
@@ -423,7 +440,7 @@ public void ScoringSendFile(string filePath, SendFileOptions options,
}
catch (Exception e)
{
- failureCallback(e.Message);
+ failureCallback(RedactApiKey(e.Message, _apiKey));
}
}
@@ -456,7 +473,7 @@ public void SendFile(string filePath, SendFileOptions options,
}
catch (Exception e)
{
- failureCallback(e.Message);
+ failureCallback(RedactApiKey(e.Message, _apiKey));
}
}
@@ -479,7 +496,7 @@ private async Task _SendFile(bool scoring, Stream file, string fileName, SendFil
try
{
var content = new MultipartFormDataContent();
- content.Add(new StreamContent(file), "file", fileName);
+ content.Add(new StreamContent(file), "file", SafeMultipartFileName(fileName));
content.Add(new StringContent(_apiKey), "api_key");
@@ -512,7 +529,7 @@ private async Task _SendFile(bool scoring, Stream file, string fileName, SendFil
}
catch (Exception e)
{
- failureCallback(e.Message);
+ failureCallback(RedactApiKey(e.Message, _apiKey));
}
}
@@ -569,7 +586,7 @@ private static string DownloadTypeToQueryValue(ZBDownloadType t)
private static string BuildGetFileUrl(string bulkBase, bool scoring, string apiKey, string fileId, GetFileOptions options)
{
- var url = bulkBase + (scoring ? "/scoring" : "") + "/getfile?api_key=" + apiKey + "&file_id=" + fileId;
+ var url = bulkBase + (scoring ? "/scoring" : "") + "/getfile?api_key=" + Enc(apiKey) + "&file_id=" + Enc(fileId);
if (options == null) return url;
if (options.DownloadType.HasValue)
url += "&download_type=" + DownloadTypeToQueryValue(options.DownloadType.Value);
@@ -640,7 +657,7 @@ private async Task _GetFile(bool scoring, string fileId, string localDownloadPat
}
catch (Exception e)
{
- failureCallback(e.Message);
+ failureCallback(RedactApiKey(e.Message, _apiKey));
}
}
@@ -669,7 +686,7 @@ private void _DeleteFile(bool scoring, string fileId, Action(string url, Action successCallback, Action
}
catch (Exception e)
{
- failureCallback(e.Message);
+ failureCallback(RedactApiKey(e.Message, _apiKey));
}
}
@@ -711,7 +728,7 @@ private async Task _sendJsonRequest(string url, string json, Action succes
}
catch (Exception e)
{
- failureCallback(e.Message);
+ failureCallback(RedactApiKey(e.Message, _apiKey));
}
}
}