diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e8af47e..57f2542 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -15,8 +15,8 @@ jobs: analyze: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: github/codeql-action/init@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 with: languages: javascript-typescript - - uses: github/codeql-action/analyze@v4 + - uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 91d2d46..46cb662 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -10,5 +10,5 @@ jobs: review: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/dependency-review-action@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cd35b83..ac32afa 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,8 +14,8 @@ jobs: runs-on: ubuntu-latest environment: npm steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 registry-url: https://registry.npmjs.org diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aec8c83..7e7cfc7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,8 +16,8 @@ jobs: node-version: [20, 22, 24] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: ${{ matrix.node-version }} package-manager-cache: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 2011f8f..ae57ce8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes will be documented here. The project follows Semantic Versioning after `1.0.0`; pre-1.0 releases may refine APIs while preserving documented security behavior where possible. +## 0.1.1 - 2026-08-09 + +- Removed all legacy MD5 and MD5-hash paths from the Vonage adapter. +- Pinned GitHub Actions to immutable commits and updated their major releases. +- Added Cloudflare Pages security headers, including a strict Content Security Policy. +- Updated release status, website metadata, and security regression coverage. + ## 0.1.0 - 2026-08-09 - Added Twilio, Telnyx, Vonage, Plivo V3, and Generic HMAC validators. diff --git a/README.md b/README.md index 3b8de2f..17cf2d9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Security-first webhook validation for applications that receive SMS events from Twilio, Telnyx, Vonage, Plivo, or a custom HMAC provider. The package combines signature verification, timestamp checks, replay protection, payload validation, PII redaction, Express middleware, and a defensive localhost tester. -> Status: `0.1.0` release candidate. Validate the exact provider configuration and test in staging before production use. +> Current release: `0.1.1`. The API is pre-1.0; validate the exact provider configuration and test in staging before production use. ## Why @@ -96,7 +96,7 @@ Use `timestamp` and `timestampSeparator` when your signing contract includes a t | Plivo V3 | HMAC-SHA256 over URL, sorted parameters, and nonce | Unique nonce plus message UUID | | Generic HMAC | SHA-256 by default; configurable encoding/prefix | Configurable timestamp and event ID | -Vonage's legacy MD5 modes are rejected by default. If migration compatibility is unavoidable, pass `allowLegacyMd5: true` explicitly and document the risk. +Vonage's legacy MD5 modes are not supported. Configure HMAC-SHA256 or stronger in the provider dashboard. ## Payload validation and logging @@ -147,7 +147,7 @@ See [CLI safety and interpretation](docs/cli.md). - Constant-time comparison for shared-secret signatures. - Raw-body and exact-URL validation where required. - Atomic replay-store interface. -- CodeQL, dependency review, Dependabot, tests, and release provenance workflow. +- CodeQL, dependency review, Dependabot, tests, and trusted npm publishing with provenance. - Responsible disclosure process in [SECURITY.md](SECURITY.md). This project reduces common implementation mistakes; it is not a security certification. Review provider documentation, proxy URL handling, key rotation, storage availability, and application side effects for your environment. diff --git a/docs/providers.md b/docs/providers.md index 374b057..015aa7b 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -23,7 +23,7 @@ Official reference: diff --git a/package-lock.json b/package-lock.json index d0a6c57..f7ba12d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "secure-sms-webhook", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "secure-sms-webhook", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "bin": { "secure-sms-webhook": "cli/tester.js" diff --git a/package.json b/package.json index 0d953d2..0894326 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "secure-sms-webhook", - "version": "0.1.0", + "version": "0.1.1", "description": "Security-first SMS webhook signature validation, replay protection, payload checks, PII redaction, and a defensive local tester.", "type": "module", "main": "./src/index.js", @@ -56,7 +56,7 @@ "type": "git", "url": "git+https://github.com/receive-sms-live/secure-sms-webhook.git" }, - "homepage": "https://github.com/receive-sms-live/secure-sms-webhook#readme", + "homepage": "https://secure-sms-webhook.pages.dev/", "bugs": { "url": "https://github.com/receive-sms-live/secure-sms-webhook/issues" }, diff --git a/site/_headers b/site/_headers new file mode 100644 index 0000000..bd6589e --- /dev/null +++ b/site/_headers @@ -0,0 +1,10 @@ +/* + Content-Security-Policy: default-src 'self'; base-uri 'none'; connect-src 'none'; font-src 'self'; form-action 'none'; frame-ancestors 'none'; img-src 'self' data:; object-src 'none'; script-src 'self' 'sha256-Kv6W6QJhArV5y6iipxf2v0T8NzCHSw8ojkc8GayXxFs='; style-src 'self'; upgrade-insecure-requests + Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Resource-Policy: same-origin + Permissions-Policy: camera=(), geolocation=(), microphone=(), payment=(), usb=() + Referrer-Policy: strict-origin-when-cross-origin + Strict-Transport-Security: max-age=31536000; includeSubDomains + X-Content-Type-Options: nosniff + X-Frame-Options: DENY + X-XSS-Protection: 0 diff --git a/src/index.d.ts b/src/index.d.ts index 209c00d..56a311b 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -61,8 +61,7 @@ export interface VonageOptions { params: Record; signature?: string; secret: string; - algorithm?: 'md5hash' | 'md5' | 'sha1' | 'sha256' | 'sha512'; - allowLegacyMd5?: boolean; + algorithm?: 'sha1' | 'sha256' | 'sha512'; maxAgeSeconds?: number | false; maxFutureSkewSeconds?: number; now?: number; diff --git a/src/providers/vonage.js b/src/providers/vonage.js index 949d343..92418df 100644 --- a/src/providers/vonage.js +++ b/src/providers/vonage.js @@ -1,4 +1,3 @@ -import { createHash } from 'node:crypto'; import { canonicalPairs, configurationFailure, @@ -12,8 +11,6 @@ import { import { validateTimestamp } from '../policies/timestamp.js'; const VONAGE_ALGORITHMS = new Map([ - ['md5hash', 'md5hash'], - ['md5', 'md5'], ['sha1', 'sha1'], ['sha256', 'sha256'], ['sha512', 'sha512'], @@ -36,9 +33,6 @@ export function createVonageSignature({ params, secret, algorithm = 'sha256' }) throw new TypeError(`Unsupported Vonage algorithm: ${algorithm}`); } const data = canonicalVonageParams(params); - if (normalized === 'md5hash') { - return createHash('md5').update(`${data}${secret}`).digest('hex'); - } return hmacBytes(normalized, secret, data).toString('hex'); } @@ -50,20 +44,12 @@ export function verifyVonageWebhook(options) { signature = params?.sig, secret, algorithm = 'sha256', - allowLegacyMd5 = false, maxAgeSeconds = 300, maxFutureSkewSeconds = 30, now, } = options || {}; if (!signature) return invalid(provider, 'MISSING_SIGNATURE', 'Vonage sig parameter is missing.'); const normalized = String(algorithm).toLowerCase().replace(/[^a-z0-9]/g, ''); - if (!allowLegacyMd5 && (normalized === 'md5' || normalized === 'md5hash')) { - return invalid( - provider, - 'INSECURE_ALGORITHM', - 'Legacy MD5 validation is disabled. Configure Vonage HMAC-SHA256 or opt in explicitly.', - ); - } if (params?.timestamp === undefined) { return invalid(provider, 'MISSING_TIMESTAMP', 'Vonage timestamp parameter is missing.'); } diff --git a/tests/integration.test.js b/tests/integration.test.js index c479e6b..ba23c0d 100644 --- a/tests/integration.test.js +++ b/tests/integration.test.js @@ -1,4 +1,5 @@ import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; import { readFileSync } from 'node:fs'; import { spawnSync } from 'node:child_process'; import test from 'node:test'; @@ -68,11 +69,19 @@ test('CLI help exits successfully', () => { test('documentation site is static, disclosed, and contains no secret form fields', () => { const html = readFileSync('site/index.html', 'utf8'); + const headers = readFileSync('site/_headers', 'utf8'); + const structuredData = html.match(/