diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 00000000..3dd0603a
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,12 @@
+# Code owners for libphonenumber-csharp.
+# These owners are requested for review automatically when someone opens a PR
+# touching matching paths. See:
+# https://docs.github.com/articles/about-code-owners
+
+# Default owner for everything in the repo.
+* @twcclegg
+
+# Security-sensitive automation and supply-chain config.
+/.github/ @twcclegg
+/.github/workflows/ @twcclegg
+/appveyor.yml @twcclegg
diff --git a/.github/workflows/build_and_run_demo_tests.yml b/.github/workflows/build_and_run_demo_tests.yml
index 5cafdfc7..6f808262 100644
--- a/.github/workflows/build_and_run_demo_tests.yml
+++ b/.github/workflows/build_and_run_demo_tests.yml
@@ -25,6 +25,8 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ with:
+ persist-credentials: false
- name: Setup .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5
with:
diff --git a/.github/workflows/build_and_run_unit_tests_linux.yml b/.github/workflows/build_and_run_unit_tests_linux.yml
index ef392ac3..9ca5a4a5 100644
--- a/.github/workflows/build_and_run_unit_tests_linux.yml
+++ b/.github/workflows/build_and_run_unit_tests_linux.yml
@@ -18,6 +18,8 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ with:
+ persist-credentials: false
- name: Setup .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5
with:
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index b6d061e8..da9ac7aa 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -43,6 +43,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ with:
+ persist-credentials: false
- name: Setup .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5
with:
@@ -52,12 +54,11 @@ jobs:
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- # If you wish to specify custom queries, you can do so here or in a config file.
- # By default, queries listed here will override any specified in a config file.
- # Prefix the list here with "+" to use these queries and those in the config file.
-
+ # Run the broader hardened query suites in addition to the default set.
+ # security-extended adds higher-coverage (lower-precision) security queries;
+ # security-and-quality layers maintainability/reliability checks on top.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
- # queries: security-extended,security-and-quality
+ queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml
index a759af09..0ff0221d 100644
--- a/.github/workflows/deploy-demo.yml
+++ b/.github/workflows/deploy-demo.yml
@@ -31,6 +31,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ with:
+ persist-credentials: false
- uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
with:
diff --git a/.github/workflows/run_all_tests_and_upload_code_coverage.yml b/.github/workflows/run_all_tests_and_upload_code_coverage.yml
index b8865c44..80c41afd 100644
--- a/.github/workflows/run_all_tests_and_upload_code_coverage.yml
+++ b/.github/workflows/run_all_tests_and_upload_code_coverage.yml
@@ -19,6 +19,8 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ with:
+ persist-credentials: false
- name: Setup .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5
with:
diff --git a/.github/workflows/run_performance_tests_windows.yml b/.github/workflows/run_performance_tests_windows.yml
index 31485ffe..ae997814 100644
--- a/.github/workflows/run_performance_tests_windows.yml
+++ b/.github/workflows/run_performance_tests_windows.yml
@@ -28,6 +28,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ with:
+ persist-credentials: false
- name: Setup .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
new file mode 100644
index 00000000..b38e6291
--- /dev/null
+++ b/.github/workflows/scorecard.yml
@@ -0,0 +1,61 @@
+# OpenSSF Scorecard — supply-chain security posture analysis.
+# Results are uploaded to the code-scanning dashboard and (via publish_results)
+# to the public OpenSSF API that backs the Scorecard badge.
+# https://github.com/ossf/scorecard-action
+name: Scorecard supply-chain security
+
+on:
+ # Re-run when branch protection settings change so the score stays current.
+ branch_protection_rule:
+ schedule:
+ - cron: '30 1 * * 6'
+ push:
+ branches: [ "main" ]
+ workflow_dispatch:
+
+# Least privilege by default; the job below elevates only what it needs.
+permissions: read-all
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: false
+
+jobs:
+ analysis:
+ name: Scorecard analysis
+ runs-on: ubuntu-latest
+ permissions:
+ # Needed to upload the results to the code-scanning dashboard.
+ security-events: write
+ # Needed to publish results and get a badge (see publish_results below).
+ id-token: write
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ with:
+ persist-credentials: false
+
+ - name: Run analysis
+ uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
+ with:
+ results_file: results.sarif
+ results_format: sarif
+ # Publish results to the OpenSSF API so the README badge stays current.
+ # Only runs on the default-branch push / schedule events above.
+ publish_results: true
+
+ # Upload the results as artifacts (retained short-term) so they can be
+ # inspected even before code-scanning ingests them.
+ - name: Upload artifact
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: SARIF file
+ path: results.sarif
+ retention-days: 5
+
+ # Upload to the code-scanning dashboard.
+ - name: Upload to code-scanning
+ uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
+ with:
+ sarif_file: results.sarif
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000..ed19d3cf
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,43 @@
+# Code of Conduct
+
+## Our Pledge
+
+This project is committed to providing a welcoming, harassment-free, and
+respectful experience for everyone who participates — contributors, maintainers,
+and users alike — regardless of background or experience level.
+
+## Our Standard
+
+We adopt the [Contributor Covenant](https://www.contributor-covenant.org), version
+2.1, as the code of conduct for this project. The full text is available at:
+
+https://www.contributor-covenant.org/version/2/1/code_of_conduct/
+
+In short: be respectful and constructive, assume good intent, give and accept
+feedback gracefully, and focus on what is best for the community and the project.
+Behavior that is disrespectful, harassing, or otherwise unwelcoming is not
+acceptable in any project space (issues, pull requests, discussions, and reviews).
+
+## Scope
+
+This Code of Conduct applies within all project spaces and also applies when an
+individual is officially representing the project in public spaces.
+
+## Reporting
+
+If you experience or witness unacceptable behavior, please report it privately to
+the project maintainers. You can do this by contacting the repository owner
+([@twcclegg](https://github.com/twcclegg)) through GitHub, or by opening a
+[private security advisory](https://github.com/twcclegg/libphonenumber-csharp/security/advisories/new)
+if you prefer a confidential channel. All reports will be reviewed and handled
+with discretion.
+
+## Enforcement
+
+Maintainers are responsible for clarifying and enforcing this Code of Conduct and
+may take any action they deem appropriate, up to and including removing comments,
+commits, code, and contributions, or banning a participant from the project, in
+response to behavior they consider inappropriate.
+
+This Code of Conduct is adapted from the Contributor Covenant; see the link above
+for the enforcement guidelines and full reference.
diff --git a/README.md b/README.md
index 6c5c2840..b5e9e68b 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
[](https://ci.appveyor.com/project/twcclegg/libphonenumber-csharp/branch/main)
[](https://codecov.io/gh/twcclegg/libphonenumber-csharp)
[](https://www.nuget.org/packages/libphonenumber-csharp/)
+[](https://scorecard.dev/viewer/?uri=github.com/twcclegg/libphonenumber-csharp)
C# port of Google's [libphonenumber library](https://github.com/google/libphonenumber).
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..a28d1ee2
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,58 @@
+# Security Policy
+
+## Supported Versions
+
+`libphonenumber-csharp` tracks upstream Google libphonenumber metadata releases and
+ships frequent updates. Security fixes are applied to the latest released version on
+NuGet only. Please make sure you are on the most recent release before reporting an
+issue.
+
+| Version | Supported |
+| ------------------ | ------------------ |
+| Latest release | :white_check_mark: |
+| Older releases | :x: |
+
+## Reporting a Vulnerability
+
+**Please do not report security vulnerabilities through public GitHub issues,
+discussions, or pull requests.**
+
+Instead, report them privately using GitHub's
+[private vulnerability reporting](https://github.com/twcclegg/libphonenumber-csharp/security/advisories/new).
+This creates a private advisory that only the maintainers can see.
+
+When reporting, please include as much of the following as you can:
+
+- A description of the issue and the affected component (e.g. parsing, matching,
+ short-number handling, metadata loading).
+- The version of the package you are using and the target framework.
+- A minimal proof-of-concept or input that reproduces the problem.
+- The potential impact (e.g. denial of service, incorrect validation result,
+ information disclosure).
+
+We will acknowledge your report as quickly as we can and keep you updated on the
+progress toward a fix and release.
+
+## Scope and Threat Model
+
+This is a library for parsing, formatting, and validating phone numbers. In typical
+usage the **phone-number strings passed to the public API are untrusted**
+(end-user input), while the **metadata shipped inside the assembly is trusted**.
+
+Security-relevant reports we are particularly interested in include:
+
+- Denial of service from untrusted input (e.g. excessive CPU/memory, catastrophic
+ regular-expression backtracking, unbounded allocation).
+- Unhandled exceptions escaping the public API for inputs that should instead be
+ rejected with `NumberParseException`.
+- XML external entity (XXE) or other parsing issues in the `Stream`-based metadata
+ loading constructor, when a consumer loads custom metadata.
+
+Reports that require an attacker to supply malicious **metadata XML** are lower
+severity, since metadata is normally trusted; we still want to know about them.
+
+## Disclosure
+
+We follow coordinated disclosure. Once a fix is available and released, we will
+publish a GitHub Security Advisory crediting the reporter (unless you prefer to
+remain anonymous).
diff --git a/csharp/PhoneNumbers.Extensions/PhoneNumbers.Extensions.csproj b/csharp/PhoneNumbers.Extensions/PhoneNumbers.Extensions.csproj
index b587e1d2..611349b3 100644
--- a/csharp/PhoneNumbers.Extensions/PhoneNumbers.Extensions.csproj
+++ b/csharp/PhoneNumbers.Extensions/PhoneNumbers.Extensions.csproj
@@ -22,6 +22,12 @@
true
$(NoWarn);1591;CA1014;CA1031;CA1062;CA1707
true
+
+ true
+ true
true
AllEnabledByDefault
latest
diff --git a/csharp/PhoneNumbers/BuildMetadataFromXml.cs b/csharp/PhoneNumbers/BuildMetadataFromXml.cs
index f767982e..1bcdf6b2 100644
--- a/csharp/PhoneNumbers/BuildMetadataFromXml.cs
+++ b/csharp/PhoneNumbers/BuildMetadataFromXml.cs
@@ -23,6 +23,7 @@
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
+using System.Xml;
using System.Xml.Linq;
namespace PhoneNumbers
@@ -102,7 +103,23 @@ internal static List BuildPhoneMetadataFromStream(Stream metadata
bool liteBuild = false, bool specialBuild = false, bool isShortNumberMetadata = false,
bool isAlternateFormatsMetadata = false)
{
- var document = XDocument.Load(metadataStream);
+ // Load with a hardened reader. The public PhoneNumberUtil.CreateInstance(Stream)
+ // constructor accepts caller-supplied XML, so we guard that path against XML external
+ // entity (XXE) attacks. The metadata files carry a benign internal DTD subset (only
+ // declarations, no entities), so we must still allow DtdProcessing.Parse;
+ // the security comes from XmlResolver = null (no external DTD/entity is ever fetched)
+ // plus a cap on characters produced by entity expansion (mitigates entity-expansion
+ // denial-of-service such as "billion laughs"). Modern .NET already defaults XmlResolver
+ // to null, but we set these explicitly as defense-in-depth (and to satisfy static
+ // analyzers such as CA3075).
+ var readerSettings = new XmlReaderSettings
+ {
+ DtdProcessing = DtdProcessing.Parse,
+ XmlResolver = null,
+ MaxCharactersFromEntities = 1024 * 1024,
+ };
+ using var reader = XmlReader.Create(metadataStream, readerSettings);
+ var document = XDocument.Load(reader);
var metadataCollection = new List();
var metadataFilter = GetMetadataFilter(liteBuild, specialBuild);
diff --git a/csharp/PhoneNumbers/PhoneNumbers.csproj b/csharp/PhoneNumbers/PhoneNumbers.csproj
index 0ef2f90c..6edf7398 100644
--- a/csharp/PhoneNumbers/PhoneNumbers.csproj
+++ b/csharp/PhoneNumbers/PhoneNumbers.csproj
@@ -23,6 +23,16 @@
true
$(NoWarn);1591;CA1062;CA1707
true
+
+ true
+ true
diff --git a/lib/github-actions-metadata-update.sh b/lib/github-actions-metadata-update.sh
index 402999f9..098d78b2 100644
--- a/lib/github-actions-metadata-update.sh
+++ b/lib/github-actions-metadata-update.sh
@@ -1,6 +1,10 @@
#! /bin/bash
-# Exit bash script on any command that returns a non zero error code
-set -e
+# Exit on any error, treat unset variables as errors, and fail a pipeline if any
+# stage fails. The pipefail matters here: every network read below is `curl | jq`
+# (or `curl | grep | sed`), and without it a failed curl would feed empty input to
+# the parser and the script would happily proceed with an empty version string —
+# potentially cutting a bogus release. Fail closed instead.
+set -euo pipefail
if [ $# -ne 1 ]
then
@@ -8,26 +12,26 @@ then
exit 123
fi
-if [ ! command -v jq &> /dev/null ]
+if ! command -v jq &> /dev/null
then
echo "jq required"
exit 123
fi
getLatestGitHubRelease() {
- curl "https://api.github.com/repos/$1/releases/latest" | jq -r .tag_name
+ curl --fail --silent --show-error --location "https://api.github.com/repos/$1/releases/latest" | jq -r .tag_name
}
getLatestNugetRelease() {
- curl "https://www.nuget.org/packages/$1/" | grep 'og:title' | sed "s/.*$1 \([^\"]*\).*/\1/"
+ curl --fail --silent --show-error --location "https://www.nuget.org/packages/$1/" | grep 'og:title' | sed "s/.*$1 \([^\"]*\).*/\1/"
}
getReleaseDelta() {
- curl https://api.github.com/repos/$1/compare/$2...$3 | jq .files[].filename
+ curl --fail --silent --show-error --location "https://api.github.com/repos/$1/compare/$2...$3" | jq .files[].filename
}
createRelease() {
- curl -f -H "Authorization: Bearer $GITHUB_TOKEN" -d "{\"tag_name\":\"$2\",\",name\":\"$2\"}" "https://api.github.com/repos/$1/releases"
+ curl --fail --silent --show-error -H "Authorization: Bearer $GITHUB_TOKEN" -d "{\"tag_name\":\"$2\",\"name\":\"$2\"}" --location "https://api.github.com/repos/$1/releases"
}
GITHUB_TOKEN=$1