From 27438b00f2fd29b569f45113d77f6d117ce03698 Mon Sep 17 00:00:00 2001 From: Anant Date: Wed, 7 Oct 2026 01:41:56 +0530 Subject: [PATCH 1/2] security: patch signing server transitive runtime advisories --- package-lock.json | 28 +++++++++++++++++---------- package.json | 4 ++++ tests/unit/bip39Compatibility.test.ts | 26 +++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 tests/unit/bip39Compatibility.test.ts diff --git a/package-lock.json b/package-lock.json index e4bca47..c630155 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6982,9 +6982,9 @@ "license": "MIT" }, "node_modules/pbkdf2": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.6.tgz", - "integrity": "sha512-BT6eelPB1EyGHo8pC0o9Bl6k6SYVhKO1jEbd3lcTrtr7XHdjP8BW1YpfCV3G9Kwkxgattk+S5q2/RvuttCsS1g==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.7.tgz", + "integrity": "sha512-nS4mvFgVwUrecPTRdpdseM2fwpdfnX0/HibA8DgBRQtihUAzHQYJFU2l+OlEtiXtpqkrMvCPDwyXMUnASDt6Qw==", "license": "MIT", "dependencies": { "create-hash": "^1.2.0", @@ -7131,7 +7131,9 @@ } }, "node_modules/proxy-addr": { - "version": "2.0.7", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -7139,6 +7141,10 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/punycode": { @@ -11127,7 +11133,7 @@ "requires": { "@types/node": "11.11.6", "create-hash": "^1.1.0", - "pbkdf2": "^3.0.9", + "pbkdf2": "3.1.7", "randombytes": "^2.0.1" } }, @@ -11761,7 +11767,7 @@ "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.13", - "proxy-addr": "~2.0.7", + "proxy-addr": "2.0.8", "qs": "~6.16.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", @@ -13406,9 +13412,9 @@ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==" }, "pbkdf2": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.6.tgz", - "integrity": "sha512-BT6eelPB1EyGHo8pC0o9Bl6k6SYVhKO1jEbd3lcTrtr7XHdjP8BW1YpfCV3G9Kwkxgattk+S5q2/RvuttCsS1g==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.7.tgz", + "integrity": "sha512-nS4mvFgVwUrecPTRdpdseM2fwpdfnX0/HibA8DgBRQtihUAzHQYJFU2l+OlEtiXtpqkrMvCPDwyXMUnASDt6Qw==", "requires": { "create-hash": "^1.2.0", "create-hmac": "^1.1.7", @@ -13509,7 +13515,9 @@ } }, "proxy-addr": { - "version": "2.0.7", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", "requires": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" diff --git a/package.json b/package.json index 61303b6..1b166e6 100644 --- a/package.json +++ b/package.json @@ -53,5 +53,9 @@ "tslint": "5.12.1", "typescript": "5.0.4", "@types/express": "4.16.0" + }, + "overrides": { + "pbkdf2": "3.1.7", + "proxy-addr": "2.0.8" } } diff --git a/tests/unit/bip39Compatibility.test.ts b/tests/unit/bip39Compatibility.test.ts new file mode 100644 index 0000000..1172d0e --- /dev/null +++ b/tests/unit/bip39Compatibility.test.ts @@ -0,0 +1,26 @@ +import * as crypto from "crypto"; +import * as bip39 from "bip39"; + +// Published BIP39 English test vector; this mnemonic never held funds. +const mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + +describe("BIP39 seed compatibility after PBKDF2 patch", () => { + it("matches the published passphrase vector", () => { + expect(bip39.mnemonicToSeedSync(mnemonic, "TREZOR").toString("hex")).toBe( + "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e534955" + + "31f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04" + ); + }); + + it("matches Node crypto with a long synthetic passphrase", () => { + const passphrase = "keeper-disposable-test-".repeat(12); + const expected = crypto.pbkdf2Sync( + mnemonic.normalize("NFKD"), + `mnemonic${passphrase}`.normalize("NFKD"), + 2048, + 64, + "sha512" + ); + expect(bip39.mnemonicToSeedSync(mnemonic, passphrase)).toEqual(expected); + }); +}); From 3cc7728c27328e7b0358d1f91a32d8d13feba90a Mon Sep 17 00:00:00 2001 From: Anant Date: Wed, 7 Oct 2026 18:31:47 +0530 Subject: [PATCH 2/2] Patch remaining compatible runtime dependency paths --- CONTRIBUTING.md | 2 + docs/SECURITY_REVIEW.md | 21 +++++ package-lock.json | 93 ++++++------------- package.json | 5 +- .../runtimeDependencyCompatibility.test.ts | 65 +++++++++++++ 5 files changed, 120 insertions(+), 66 deletions(-) create mode 100644 docs/SECURITY_REVIEW.md create mode 100644 tests/unit/runtimeDependencyCompatibility.test.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 954e608..13de400 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,3 +5,5 @@ Use Node.js 22. Start from a clean checkout and run `npm ci --ignore-scripts`, ` Keep tests self-contained and use generated, disposable identities. Do not require production infrastructure, cloud credentials, or signing keys in a pull request. Never commit `.env` files, private keys, wallet seeds, database exports, production logs, or deployment access. Add tests for changed behavior, especially authorization and signing paths. Document any new local configuration in `.env.example` using names and safe placeholders only. Open a focused pull request describing the behavior, risks, and verification. Keep dependency changes in the lockfile. If a security issue or suspected credential exposure is involved, follow [SECURITY.md](SECURITY.md) before opening a public issue. + +Runtime dependency remediation and the preserved mobile error contract are recorded in [docs/SECURITY_REVIEW.md](docs/SECURITY_REVIEW.md). Review those notes alongside audit output; a passing local suite does not verify hosted provider delivery. diff --git a/docs/SECURITY_REVIEW.md b/docs/SECURITY_REVIEW.md new file mode 100644 index 0000000..b855b99 --- /dev/null +++ b/docs/SECURITY_REVIEW.md @@ -0,0 +1,21 @@ +# Source security and dependency review + +Reviewed 7 October 2026 with Node 22 and the committed lockfile. This document records source/contributor checks; it is not approval to deploy a hosted backend. + +## Runtime dependencies + +The lockfile pins patched `proxy-addr` 2.0.8 and `pbkdf2` 3.1.7. BIP39 published vectors and a long-passphrase fixture retain output compatibility. Moment 2.31.0 fixes [GHSA-4p3w-j4w9-5jqw](https://github.com/advisories/GHSA-4p3w-j4w9-5jqw); duration-format fixtures cover the email calls used here. + +An explicit override resolves the four transitive UUID copies to CommonJS-compatible `uuid` 11.1.1, the patched version for [GHSA-w5hq-g745-h8pq](https://github.com/advisories/GHSA-w5hq-g745-h8pq). The callers in AWS SDK, google-gax, node-cron and teeny-request use `v4()` without a caller-provided buffer. Compatibility tests exercise AWS/Google UUID helpers, cron registration/manual execution and the Google HTTP client's multipart boundary against a loopback server. This avoids changing cloud-client or cron lifecycle APIs merely to update the transitive package. Keep the override visible until those upstream chains support a patched version themselves. + +`npm audit --omit=dev --package-lock-only` reports no critical, high or moderate runtime findings after these changes. One low AWS SDK v2 region-validation advisory remains: [GHSA-j965-2qgj-vjmq](https://github.com/advisories/GHSA-j965-2qgj-vjmq). The SES client region here comes from deployment configuration (`SES_REGION`), not request data. Restrict that configuration to the deployment's valid AWS region. Do not follow the audit suggestion to downgrade AWS SDK to v1. Plan the supported AWS SDK v3 migration separately, with a staging SES send/response test and runtime identity review. Unit and local contributor checks do not contact SES or Google Secret Manager. + +## HTTP errors and logging + +The mobile client consumes the existing `{ err: message }` response from Signing Server routes. Changing all failures to generic text or a new code schema would change verification/signing error behavior. Preserve that response contract in the logging-only change. A future public-error contract should explicitly allow stable expected route errors and sanitize provider/database exceptions, with mobile acceptance fixtures per route before any hosted rollout. + +Never put request bodies, seeds, keys, OTPs, access configuration, provider exception contents or real user logs into an issue or PR. Source/history secret scans and synthetic error-canary tests belong in review. Source scans cannot establish what a separate hosted system logged historically; any investigation or credential change must follow the private security process and its production dependencies. + +## Contributor verification + +Use a fresh public clone, Node 22, `npm ci --ignore-scripts`, `npm run compile`, `npm test`, the runtime audit above, and both current-source and history scans documented in CONTRIBUTING. Tests use generated disposable identities and deny live Secret Manager access. Retain the MIT/ISC notices and SECURITY reporting channel when distributing the source. diff --git a/package-lock.json b/package-lock.json index c630155..820e834 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "fs": "0.0.1-security", "http": "0.0.0", "idx": "3.0.1", - "moment": "2.30.1", + "moment": "2.31.0", "mongoose": "6.13.11", "morgan": "1.12.1", "node-cron": "3.0.2", @@ -3206,13 +3206,6 @@ "which-typed-array": "^1.1.2" } }, - "node_modules/aws-sdk/node_modules/uuid": { - "version": "8.0.0", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/axios": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", @@ -4870,20 +4863,6 @@ "node": ">=14" } }, - "node_modules/google-gax/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/gopd": { "version": "1.2.0", "license": "MIT", @@ -6499,7 +6478,9 @@ } }, "node_modules/moment": { - "version": "2.30.1", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.31.0.tgz", + "integrity": "sha512-0acOTfMiWOheYS4eoWb80yYMb/JLvVv9SHbs2PehaDzfUG0Bw855SKyk0IKTnPGa5+U2bmi3W68l1+sGLX/pvw==", "license": "MIT", "engines": { "node": "*" @@ -6672,13 +6653,6 @@ "node": ">=6.0.0" } }, - "node_modules/node-cron/node_modules/uuid": { - "version": "8.3.2", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/node-fetch": { "version": "2.7.0", "license": "MIT", @@ -7912,17 +7886,6 @@ "node": ">= 6" } }, - "node_modules/teeny-request/node_modules/uuid": { - "version": "9.0.1", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/test-exclude": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", @@ -8515,6 +8478,19 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -10922,7 +10898,7 @@ "sax": "1.2.1", "url": "0.10.3", "util": "^0.12.4", - "uuid": "8.0.0", + "uuid": "11.1.1", "xml2js": "0.6.2" }, "dependencies": { @@ -10935,9 +10911,6 @@ "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } - }, - "uuid": { - "version": "8.0.0" } } }, @@ -12019,14 +11992,7 @@ "proto3-json-serializer": "^2.0.2", "protobufjs": "^7.3.2", "retry-request": "^7.0.0", - "uuid": "^9.0.1" - }, - "dependencies": { - "uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" - } + "uuid": "11.1.1" } }, "gopd": { @@ -13106,7 +13072,9 @@ "dev": true }, "moment": { - "version": "2.30.1" + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.31.0.tgz", + "integrity": "sha512-0acOTfMiWOheYS4eoWb80yYMb/JLvVv9SHbs2PehaDzfUG0Bw855SKyk0IKTnPGa5+U2bmi3W68l1+sGLX/pvw==" }, "mongodb": { "version": "4.17.2", @@ -13212,12 +13180,7 @@ "node-cron": { "version": "3.0.2", "requires": { - "uuid": "8.3.2" - }, - "dependencies": { - "uuid": { - "version": "8.3.2" - } + "uuid": "11.1.1" } }, "node-fetch": { @@ -13993,7 +13956,7 @@ "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.9", "stream-events": "^1.0.5", - "uuid": "^9.0.0" + "uuid": "11.1.1" }, "dependencies": { "agent-base": { @@ -14016,9 +13979,6 @@ "agent-base": "6", "debug": "4" } - }, - "uuid": { - "version": "9.0.1" } } }, @@ -14391,6 +14351,11 @@ "utils-merge": { "version": "1.0.1" }, + "uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==" + }, "v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/package.json b/package.json index 1b166e6..5b0848f 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "fs": "0.0.1-security", "http": "0.0.0", "idx": "3.0.1", - "moment": "2.30.1", + "moment": "2.31.0", "mongoose": "6.13.11", "morgan": "1.12.1", "node-cron": "3.0.2", @@ -56,6 +56,7 @@ }, "overrides": { "pbkdf2": "3.1.7", - "proxy-addr": "2.0.8" + "proxy-addr": "2.0.8", + "uuid": "11.1.1" } } diff --git a/tests/unit/runtimeDependencyCompatibility.test.ts b/tests/unit/runtimeDependencyCompatibility.test.ts new file mode 100644 index 0000000..cfb969e --- /dev/null +++ b/tests/unit/runtimeDependencyCompatibility.test.ts @@ -0,0 +1,65 @@ +const { createRequire } = require("module"); +import { createServer } from "http"; +import { AddressInfo } from "net"; +import { formatDuration } from "../../src/services/mailing/mailer"; + +const uuidV4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +describe("patched runtime dependency compatibility", () => { + for (const dependency of ["aws-sdk", "google-gax", "node-cron", "teeny-request"]) { + it(`${dependency} resolves the patched CommonJS UUID without changing v4 output`, () => { + const load = createRequire(require.resolve(dependency)); + expect(load("uuid/package.json").version).toBe("11.1.1"); + expect(load("uuid").v4()).toMatch(uuidV4); + }); + } + + it("preserves the UUID helpers called by AWS and Google clients", () => { + expect(require("aws-sdk").util.uuid.v4()).toMatch(uuidV4); + expect(require("google-gax/build/src/util").makeUUID()).toMatch(uuidV4); + }); + + it("preserves cron registration and manual execution without starting a timer", () => { + const cron = require("node-cron"); + const callback = jest.fn(); + const task = cron.schedule("*/10 * * * *", callback, { scheduled: false }); + try { + expect(task.options.name).toMatch(uuidV4); + task.now(); + expect(callback).toHaveBeenCalledTimes(1); + } finally { + task.stop(); + } + }); + + it("preserves the Google HTTP client's multipart UUID boundary on loopback", async () => { + let contentType = ""; + const server = createServer((req, res) => { + contentType = req.headers["content-type"] || ""; + req.resume(); + req.on("end", () => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end('{"ok":true}'); + }); + }); + await new Promise(resolve => server.listen(0, "127.0.0.1", resolve)); + try { + const { teenyRequest } = require("teeny-request"); + const result = await new Promise((resolve, reject) => teenyRequest({ + uri: `http://127.0.0.1:${(server.address() as AddressInfo).port}/synthetic`, + method: "POST", + headers: {}, + multipart: [{ "Content-Type": "application/json", body: "synthetic-metadata" }, { "Content-Type": "application/octet-stream", body: require("stream").Readable.from(["synthetic-ciphertext"]) }], + }, (error, response, body) => error ? reject(error) : resolve({ response, body }))); + expect(result.response.statusCode).toBe(200); + expect(contentType.replace("multipart/related; boundary=", "")).toMatch(uuidV4); + } finally { + await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())); + } + }); + + it.each([[0, "0:00:00"], [59999, "0:00:59"], [3600000, "1:00:00"], [176400000, "49:00:00"]])( + "keeps email duration formatting for %i milliseconds", + (milliseconds, expected) => expect(formatDuration(milliseconds)).toBe(expected) + ); +});