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 @@ [![Build status](https://ci.appveyor.com/api/projects/status/76abbk0qveot0mbo/branch/main?svg=true)](https://ci.appveyor.com/project/twcclegg/libphonenumber-csharp/branch/main) [![codecov](https://codecov.io/gh/twcclegg/libphonenumber-csharp/branch/main/graph/badge.svg)](https://codecov.io/gh/twcclegg/libphonenumber-csharp) [![NuGet](https://img.shields.io/nuget/dt/libphonenumber-csharp.svg)](https://www.nuget.org/packages/libphonenumber-csharp/) +[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/twcclegg/libphonenumber-csharp/badge)](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