From a6fe33738adc8c6a7297d45f26fc514bb46ead9b Mon Sep 17 00:00:00 2001 From: Jacob Umuewu Date: Fri, 28 Aug 2026 13:35:46 +0100 Subject: [PATCH] feat: add audit logging, input sanitization, error reporting, and distributed tracing --- apps/extension/README.md | 18 ++ apps/extension/entrypoints/background.ts | 21 +++ apps/extension/entrypoints/popup/App.tsx | 10 +- apps/extension/lib/error-reporter.test.ts | 58 +++++++ apps/extension/lib/error-reporter.ts | 92 ++++++++++ apps/extension/lib/router.ts | 9 +- apps/extension/lib/sanitization.test.ts | 86 ++++++++++ apps/extension/lib/sanitization.ts | 79 +++++++++ docs/observability.md | 12 ++ packages/service-kit/src/index.ts | 11 ++ packages/service-kit/src/tracing.test.ts | 82 +++++++++ packages/service-kit/src/tracing.ts | 139 +++++++++++++++ services/api-gateway/src/server.ts | 14 +- services/policy-service/src/server.ts | 19 ++- .../wallet-service/src/db/pg-repository.ts | 16 +- services/wallet-service/src/repository.ts | 35 +++- services/wallet-service/src/server.test.ts | 84 +++++++++ services/wallet-service/src/server.ts | 161 +++++++++++++++++- services/worker-service/src/loop.ts | 18 +- 19 files changed, 934 insertions(+), 30 deletions(-) create mode 100644 apps/extension/lib/error-reporter.test.ts create mode 100644 apps/extension/lib/error-reporter.ts create mode 100644 apps/extension/lib/sanitization.test.ts create mode 100644 apps/extension/lib/sanitization.ts create mode 100644 packages/service-kit/src/tracing.test.ts create mode 100644 packages/service-kit/src/tracing.ts diff --git a/apps/extension/README.md b/apps/extension/README.md index 7b7c3aa..3b9b005 100644 --- a/apps/extension/README.md +++ b/apps/extension/README.md @@ -14,3 +14,21 @@ Build-time public env (WXT inlines `WXT_PUBLIC_*` into the client bundle): - A **dev build** (`wxt dev`) with this unset falls back to `http://localhost:3000` and `http://localhost:5173`. - `WXT_PUBLIC_ALLOW_ANY_PAIR_ORIGIN` — set to `1` to explicitly disable the pair-origin restriction (any origin may pair). Named escape hatch only; logs a warning on every startup. **Never set this in a production build.** - `WXT_PUBLIC_MAINNET_RPC_URL` — trusted Soroban RPC used to anchor the device signer's signature-expiration ledger on **mainnet** (L4). The extension never trusts the paired wallet's `rpcUrl` for this. Testnet uses SDF's pinned public endpoint automatically; mainnet has no universal public RPC, so this must be set — **mainnet signing fails closed if it is unset** rather than trusting the caller-supplied endpoint. + +## Input Sanitization (#312) + +dApp-provided connection payloads (such as dApp names, origins, descriptions, and icon URLs) are sanitized before being processed or rendered in the popup UI: +- **HTML & Script Escaping**: HTML special characters (`<`, `>`, `&`, `"`, `'`, `/`) are escaped using `escapeHtml()`. +- **Tag Stripping**: All HTML tags (`")).toBe( + "<script>alert('xss')</script>", + ); + expect(escapeHtml('Hello "World" & ')).toBe( + "Hello "World" & <Friends>", + ); + }); + }); + + describe("sanitizeUrl", () => { + it("blocks javascript: and data: URIs", () => { + expect(sanitizeUrl("javascript:alert(1)")).toBe(""); + expect(sanitizeUrl("JAVASCRIPT:alert(1)")).toBe(""); + expect(sanitizeUrl("data:text/html,")).toBe(""); + expect(sanitizeUrl("vbscript:msgbox(1)")).toBe(""); + }); + + it("allows valid http and https URLs", () => { + expect(sanitizeUrl("https://example.com/icon.png")).toBe( + "https://example.com/icon.png", + ); + }); + + it("handles undefined or null inputs", () => { + expect(sanitizeUrl(undefined)).toBe(""); + expect(sanitizeUrl("")).toBe(""); + }); + }); + + describe("sanitizeString", () => { + it("strips HTML tags and removes control characters", () => { + expect(sanitizeString("Malicious")).toBe( + "Malicious", + ); + expect(sanitizeString("Clean\x00Name")).toBe("CleanName"); + }); + + it("truncates string to specified max length", () => { + const longInput = "a".repeat(200); + expect(sanitizeString(longInput, 50).length).toBe(50); + }); + + it("handles non-string or malformed inputs safely", () => { + expect(sanitizeString(12345)).toBe("12345"); + expect(sanitizeString({ invalid: "object" })).toBe("[object Object]"); + expect(sanitizeString(null)).toBe(""); + expect(sanitizeString(undefined)).toBe(""); + }); + }); + + describe("sanitizeDAppMetadata", () => { + it("sanitizes full dApp metadata payload containing script injection attempts", () => { + const payload = { + name: "DApp Exchange", + description: "Best DEX for tokens", + iconUrl: "javascript:void(0)", + origin: "https://dapp.example.com", + }; + + const result = sanitizeDAppMetadata(payload); + + expect(result.name).not.toContain("