1104 lines
54 KiB
Markdown
1104 lines
54 KiB
Markdown
# Kordant — Attack Surface Knowledge Base (Phase 3)
|
|
|
|
> **Generated**: 2026-05-28
|
|
> **Phase**: L2 (Knowledge Base / Threat Model)
|
|
> **Target**: Kordant monorepo — SolidStart + tRPC + Drizzle ORM + native mobile apps
|
|
> **Commit**: `26d9f8b050969dfaa2c9dfb714a872160b7db382`
|
|
|
|
---
|
|
|
|
## Project Classification
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| **Primary type** | Web application (SSR via SolidStart) |
|
|
| **Secondary types** | API server (tRPC), Background worker (BullMQ + Redis), Browser extension (Manifest V3), Native mobile apps (iOS/SwiftUI, Android/Jetpack Compose) |
|
|
| **Monorepo structure** | pnpm workspaces: `web/`, `browser-ext/`; plus sibling projects `iOS/`, `android/`, `honker/`, `scheduler/` |
|
|
| **Hosting** | Vercel (web), self-hosted Docker (scheduler, Redis) |
|
|
| **Primary language** | TypeScript/JavaScript |
|
|
| **Secondary languages** | Swift (iOS), Kotlin (Android), Rust (`honker/` — SQLite extension, not part of Kordant runtime) |
|
|
| **Framework** | SolidStart 2.0.0-alpha.2 (Nitro-based SSR), tRPC 10.45.4, Drizzle ORM 0.45.2 |
|
|
| **Database** | Turso/libSQL (SQLite, cloud-hosted) |
|
|
| **Cache/Queue** | Redis 7 (BullMQ job queue, rate limiting via sorted sets) |
|
|
| **Auth provider** | Clerk (OAuth + email/password via `clerk-solidjs`) |
|
|
| **Payments** | Stripe (Checkout Sessions, Billing Portal, Webhooks) |
|
|
| **External APIs** | HIBP, SecurityTrails, Censys, Shodan, Twilio, Resend, Firebase FCM, Sentry |
|
|
|
|
---
|
|
|
|
## Architecture Model
|
|
|
|
### Components
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────┐
|
|
│ CLIENTS │
|
|
│ ┌──────────┐ ┌─────────┐ ┌──────────┐ ┌──────────────────┐ │
|
|
│ │ Web (SSR)│ │ iOS │ │ Android │ │ Browser Extension│ │
|
|
│ │SolidStart│ │SwiftUI │ │Compose │ │Manifest V3 │ │
|
|
│ └────┬─────┘ └────┬────┘ └────┬─────┘ └────────┬─────────┘ │
|
|
│ │ │ │ │ │
|
|
│ └──────────────┴─────────────┴─────────────────┘ │
|
|
│ │ tRPC (HTTP batch + WebSocket) │
|
|
└──────────────────────────┼───────────────────────────────────────────┘
|
|
│
|
|
┌──────────────────────────┼───────────────────────────────────────────┐
|
|
│ web/ (SolidStart) │
|
|
│ ┌───────────────────────┼──────────────────────────────────────┐ │
|
|
│ │ Frontend (SSR/CSR) │ Middleware Pipeline │ │
|
|
│ │ Routes: (webapp), │ 1. requestLogger │ │
|
|
│ │ (admin), (auth), │ 2. securityHeaders (CSP, HSTS, etc) │ │
|
|
│ │ landing pages │ 3. corsHeaders │ │
|
|
│ │ Components: UI, │ 4. clerkMiddleware (authn) │ │
|
|
│ │ layouts, widgets │ └── each tRPC procedure adds: │ │
|
|
│ └───────────────────────┤ - protectedProcedure (authz) │ │
|
|
│ ┌───────────────────────┤ - adminProcedure (role=admin) │ │
|
|
│ │ Backend (tRPC) │ - rateLimitedProcedure │ │
|
|
│ │ 16 routers: │ │ │
|
|
│ │ example, user, │ │ │
|
|
│ │ billing, notification│ │ │
|
|
│ │ darkwatch, voiceprint│ │ │
|
|
│ │ spamshield, hometitle│ │ │
|
|
│ │ removebrokers, │ │ │
|
|
│ │ correlation, │ │ │
|
|
│ │ reports, scheduler, │ │ │
|
|
│ │ extension, blog, │ │ │
|
|
│ │ admin │ │ │
|
|
│ └───────────────────────┼──────────────────────────────────────┘ │
|
|
│ ┌───────────────────────┼──────────────────────────────────────┐ │
|
|
│ │ Background Jobs │ WebSocket Server (ws@8.21.0) │ │
|
|
│ │ BullMQ + Redis │ Port 3001, JWT-auth via URL param │ │
|
|
│ │ Scheduler container │ Heartbeat + pong timeout │ │
|
|
│ └───────────────────────┼──────────────────────────────────────┘ │
|
|
│ ┌───────────────────────┼──────────────────────────────────────┐ │
|
|
│ │ Report Generator │ External API Clients │ │
|
|
│ │ Puppeteer (headless)│ HIBP, SecurityTrails, Censys, │ │
|
|
│ │ HTML→PDF │ Shodan, Twilio, Stripe, Resend, FCM │ │
|
|
│ └───────────────────────┼──────────────────────────────────────┘ │
|
|
└──────────────────────────┼───────────────────────────────────────────┘
|
|
│
|
|
┌────────────┼────────────┐
|
|
▼ ▼ ▼
|
|
┌─────────────┐ ┌─────────┐ ┌──────────┐
|
|
│ Turso │ │ Redis │ │ External │
|
|
│ libSQL │ │ 7 │ │ APIs │
|
|
│ (SQLite) │ │(BullMQ, │ │ │
|
|
│ │ │ rate │ │ │
|
|
└─────────────┘ │ limit) │ └──────────┘
|
|
└─────────┘
|
|
```
|
|
|
|
### Service Domains
|
|
|
|
| Domain | tRPC Router | Trust Level | Key Data | External Dependencies |
|
|
|--------|-------------|-------------|----------|----------------------|
|
|
| **VoicePrint** | `voiceprint` | Protected (auth) | Voice enrollments, audio samples, analysis results | None (local ML) |
|
|
| **DarkWatch** | `darkwatch` | Protected (auth + tier) | Watchlist items, exposure records | HIBP, SecurityTrails, Censys, Shodan |
|
|
| **SpamShield** | `spamshield` | Protected (auth) | Spam classifications, phone numbers, SMS content | Twilio, ML engine |
|
|
| **HomeTitle** | `hometitle` | Protected (auth + tier) | Property records, deed changes | County deed APIs |
|
|
| **RemoveBrokers** | `removebrokers` | Protected (auth + tier) | Opt-out requests, broker data | Broker opt-out APIs |
|
|
| **Billing** | `billing` | Protected (auth) | Subscriptions, payments, invoices | Stripe |
|
|
| **Admin** | `admin` | Admin-only | Blog posts, user management, stats | Drizzle ORM |
|
|
| **Extension** | `extension` | Public (some) | Device linking, phishing reports | Drizzle ORM |
|
|
| **Reports** | `reports` | Protected (auth + tier) | Security reports, PDF generation | Puppeteer, Drizzle |
|
|
| **User** | `user` | Protected (auth) | User profile, preferences | Drizzle ORM |
|
|
| **Notifications** | `notification` | Protected (auth) | Push notifications, email | Firebase FCM, Resend |
|
|
| **Correlation** | `correlation` | Protected (auth) | Cross-domain alert correlation | Drizzle ORM |
|
|
| **Scheduler** | `scheduler` | Internal (worker) | Job scheduling, cron tasks | BullMQ, Drizzle |
|
|
|
|
---
|
|
|
|
## Trust Boundaries
|
|
|
|
| # | Boundary | Direction | Protocol | Auth | Encryption | Risk |
|
|
|---|----------|-----------|----------|------|------------|------|
|
|
| TB-1 | **Internet → Web (SolidStart)** | Client → Server | HTTPS/tRPC | Clerk session + JWT + API key | TLS | HIGH — tRPC procedures are the primary attack surface |
|
|
| TB-2 | **tRPC → Drizzle ORM** | App → DB | libSQL/Turso | JWT-validated user context | TLS (Turso) | CRITICAL — SQL injection via drizzle-orm CVE-2026-39356 |
|
|
| TB-3 | **tRPC → Stripe** | App → Stripe | HTTPS | Stripe secret key | TLS | HIGH — Webhook spoofing, payment manipulation |
|
|
| TB-4 | **tRPC → External APIs** | App → HIBP/Trails/Censys/Shodan | HTTPS | API keys | TLS | MEDIUM — API key leakage, SSRF via crafted URLs |
|
|
| TB-5 | **WebSocket → ws** | Client → WS Server | WSS (port 3001) | JWT in query param | TLS | HIGH — Memory disclosure (CVE-2026-45736), DoS (CVE-2024-37890) |
|
|
| TB-6 | **Browser Extension → tRPC** | Extension → Web | HTTPS | API key (stored in extension) | TLS | HIGH — superjson prototype pollution chain (CVE-2022-23631) |
|
|
| TB-7 | **tRPC → Redis** | App → Redis | TCP (internal) | None (network-isolated) | None | MEDIUM — BullMQ job injection, cache poisoning |
|
|
| TB-8 | **Puppeteer → File System** | App → Local FS | Local | None | N/A | HIGH — Path traversal, SSRF via file input |
|
|
| TB-9 | **tRPC → VoicePrint Storage** | App → Audio Files | Local FS | Protected procedure | N/A | MEDIUM — Audio file access, path traversal |
|
|
| TB-10 | **Scheduler → Redis** | Worker → Redis | TCP (internal) | None (network-isolated) | N/A | LOW — Internal worker communication |
|
|
|
|
### Role-Based Access
|
|
|
|
| Role | Access Level | Enforcement |
|
|
|------|-------------|-------------|
|
|
| **Anonymous** | Public procedures only (`extension.reportPhishing`, `extension.getAuthStatus`) | tRPC procedure type |
|
|
| **Authenticated User** | Protected procedures (all data scoped to `ctx.user.id`) | `isAuthed` middleware checks `ctx.user` |
|
|
| **Admin** | Admin procedures (`adminRouter`) + all user procedures | `isAdmin` middleware checks `ctx.user.role === "admin"` |
|
|
| **API Key** | Limited procedures (extension API key path) | `ctx.apiKey` fallback in `createTRPCContext` |
|
|
|
|
---
|
|
|
|
## Data-Flow Slices (DFD)
|
|
|
|
### DFD-1: tRPC → Drizzle ORM (SQL Injection Vector)
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
A[Client Input] -->|tRPC procedure| B[Input Validation\nvalibot schema]
|
|
B --> C{Schema pass?}
|
|
C -->|No| D[TRPCError thrown]
|
|
C -->|Yes| E[Service Layer]
|
|
E --> F[Drizzle ORM Query]
|
|
F --> G[Turso/SQLite]
|
|
|
|
style A fill:#ff6b6b
|
|
style G fill:#4ecdc4
|
|
style F fill:#ffe66d
|
|
```
|
|
|
|
**Flow**: User input → valibot validation → tRPC procedure → Drizzle ORM → Turso SQLite
|
|
|
|
**Key risk**: CVE-2026-39356 — SQL injection via improperly escaped SQL identifiers in drizzle-orm 0.45.2. If any tRPC procedure passes user input into column/table names (via `sql` tag or dynamic column references), injection is possible.
|
|
|
|
**Sinks to check**: `sql<>` template tag usage, dynamic column references, `inArray` with user-controlled values, raw Drizzle query builders.
|
|
|
|
### DFD-2: VoicePrint Audio Analysis Pipeline
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
A[Base64 Audio Data] -->|tRPC analyzeAudio| B[decode Base64 → Buffer]
|
|
B --> C[saveAudio\nwrite to disk]
|
|
C --> D[preprocessAudio\nfeature extraction]
|
|
D --> E[detectSynthetic\nML inference]
|
|
E --> F[matchVoice\nembedding comparison]
|
|
F --> G[store analysis\nDrizzle ORM]
|
|
G --> H[broadcast alert\nWebSocket]
|
|
|
|
style A fill:#ff6b6b
|
|
style C fill:#ffe66d
|
|
style E fill:#ffe66d
|
|
```
|
|
|
|
**Flow**: Base64 audio → decode → save to disk → ML preprocessing → synthetic detection → voice matching → store results → WebSocket alert
|
|
|
|
**Key risks**:
|
|
- Unbounded audio size (DoS via large uploads)
|
|
- Base64 decode buffer overflow potential
|
|
- ML engine input validation
|
|
- File path construction for audio storage
|
|
|
|
### DFD-3: Browser Extension → tRPC (Prototype Pollution Chain)
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
A[Extension Content Script] -->|tRPC call| B[superjson serialization]
|
|
B -->|HTTP POST| C[tRPC endpoint]
|
|
C --> D[superjson deserialization]
|
|
D --> E[Prototype Pollution?]
|
|
E -->|Yes| F[RCE via __proto__ overwrite]
|
|
D --> G[valibot validation]
|
|
G --> H[Service layer]
|
|
|
|
style A fill:#ff6b6b
|
|
style D fill:#ffe66d
|
|
style F fill:#ff0000
|
|
```
|
|
|
|
**Flow**: Extension → superjson serialize → HTTP → tRPC → superjson deserialize → valibot → service
|
|
|
|
**Key risk**: superjson CVE-2022-23631 (prototype pollution → RCE, CVSS 10.0). The browser extension uses superjson for tRPC serialization. If the tRPC server deserializes untrusted superjson data, prototype pollution is possible.
|
|
|
|
### DFD-4: WebSocket Real-Time Alerts
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
A[Service Layer] -->|broadcastToUser| B[WebSocket Server]
|
|
B --> C{User connected?}
|
|
C -->|Yes| D[ws.send JSON alert]
|
|
C -->|No| E[Drop silently]
|
|
D --> F[Client receives alert]
|
|
|
|
G[Client connect] -->|JWT in ?token param| H[authenticateConnection]
|
|
H -->|Valid| I[addSocket to userSockets]
|
|
H -->|Invalid| J[close 4001]
|
|
|
|
style A fill:#4ecdc4
|
|
style B fill:#ffe66d
|
|
style G fill:#ff6b6b
|
|
```
|
|
|
|
**Flow**: Service → broadcastToUser → WebSocket server → user socket → client
|
|
|
|
**Key risks**:
|
|
- JWT in URL query parameter (log exposure, referer leakage)
|
|
- No message size limit
|
|
- No rate limiting on WebSocket messages
|
|
- Heartbeat bypass potential
|
|
|
|
### DFD-5: Stripe Webhook Processing
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
A[Stripe → POST /api/webhook] --> B[signature verification]
|
|
B -->|Valid| C[handleWebhookEvent]
|
|
B -->|Invalid| D[400 rejected]
|
|
C --> E{event.type}
|
|
E -->|checkout.session.completed| F[create subscription in DB]
|
|
E -->|invoice.paid| G[update status to active]
|
|
E -->|invoice.payment_failed| H[update status to past_due]
|
|
E -->|customer.subscription.updated| I[update tier/status]
|
|
E -->|customer.subscription.deleted| J[mark as canceled]
|
|
|
|
style A fill:#ff6b6b
|
|
style B fill:#ffe66d
|
|
style F fill:#4ecdc4
|
|
```
|
|
|
|
**Key risk**: Webhook replay attacks if signature verification is weak. Type coercion in `event.data.object as unknown as Record<string, unknown>` could bypass type checks.
|
|
|
|
### DFD-6: Report Generation (Puppeteer)
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
A[generateReport mutation] --> B[compileData from DB]
|
|
B --> C[renderHTML template]
|
|
C --> D[generatePDF via Puppeteer]
|
|
D --> E[uploadPDF to storage]
|
|
E --> F[update report status]
|
|
|
|
style A fill:#ff6b6b
|
|
style D fill:#ffe66d
|
|
```
|
|
|
|
**Key risk**: Puppeteer SSRF if HTML template contains user-controlled URLs. Path traversal in report filename construction.
|
|
|
|
---
|
|
|
|
## Control-Flow Slices (CFD)
|
|
|
|
### CFD-1: Authentication Flow
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[Request arrives] --> B[Clerk middleware]
|
|
B --> C{Valid Clerk session?}
|
|
C -->|Yes| D[set ctx.user]
|
|
C -->|No| E[Check Bearer token]
|
|
E --> F{Valid JWT?}
|
|
F -->|Yes| G[set ctx.user from JWT]
|
|
F -->|No| H[Check x-api-key]
|
|
H --> I[set ctx.apiKey]
|
|
|
|
style A fill:#ff6b6b
|
|
style B fill:#ffe66d
|
|
style D fill:#4ecdc4
|
|
style G fill:#4ecdc4
|
|
```
|
|
|
|
**Auth chain**: Clerk session cookie → Bearer JWT → API key. Each level is a fallback, not a parallel auth. The `x-api-key` is the weakest link — it's checked only if both session and JWT fail.
|
|
|
|
### CFD-2: Authorization Flow
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[tRPC procedure] --> B{Procedure type?}
|
|
B -->|publicProcedure| C[No auth check]
|
|
B -->|protectedProcedure| D{ctx.user exists?}
|
|
B -->|adminProcedure| E{ctx.user && role=admin?}
|
|
D -->|No| F[401 UNAUTHORIZED]
|
|
D -->|Yes| G[proceed]
|
|
E -->|No| H[403 FORBIDDEN]
|
|
E -->|Yes| G
|
|
C --> I[proceed]
|
|
|
|
style A fill:#ff6b6b
|
|
style F fill:#ff6b6b
|
|
style H fill:#ff6b6b
|
|
style G fill:#4ecdc4
|
|
```
|
|
|
|
**Key observation**: Admin check is `ctx.user.role !== "admin"` — this is a string comparison, not an enum. Any user with `role: "admin"` in the DB gets admin access. No additional checks (IP allowlist, MFA, audit logging).
|
|
|
|
### CFD-3: Rate Limiting Flow
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[Procedure middleware] --> B{Path in sensitive list?}
|
|
B -->|Yes| C[Tier: sensitive\n3/hr]
|
|
B -->|No| D{ctx.user?}
|
|
D -->|Yes| E{role=admin?}
|
|
D -->|No| F[Tier: public\n5/min]
|
|
E -->|Yes| G[Tier: admin\n50/min]
|
|
E -->|No| H[Tier: authenticated\n100/min]
|
|
C --> I[Redis sorted set check]
|
|
G --> I
|
|
H --> I
|
|
F --> I
|
|
I --> J{Allowed?}
|
|
J -->|No| K[429 TOO_MANY_REQUESTS]
|
|
J -->|Yes| L[proceed]
|
|
|
|
style A fill:#ff6b6b
|
|
style K fill:#ff6b6b
|
|
style L fill:#4ecdc4
|
|
```
|
|
|
|
**Key observation**: Sensitive paths are hardcoded: `["login", "signup", "forgotPassword", "resetPassword"]`. Other procedures don't get sensitive-tier limits. The `websocket` tier (1/minute) is defined but not applied to WebSocket connections.
|
|
|
|
---
|
|
|
|
## Framework Contracts and Hidden Control Channels
|
|
|
|
### SolidStart / Nitro Middleware Pipeline
|
|
|
|
**File**: `web/src/middleware.ts`
|
|
|
|
The middleware chain is:
|
|
1. `requestLogger` — logs all requests
|
|
2. `securityHeaders` — sets HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy
|
|
3. `corsHeaders` — origin validation + preflight handling
|
|
4. `clerkMiddleware` — Clerk authentication
|
|
|
|
**Critical contract**: Middleware runs before all routes AND before tRPC handlers. If any middleware fails silently, the next middleware or handler proceeds without the expected security headers or auth context.
|
|
|
|
### CSP Header Analysis
|
|
|
|
```
|
|
default-src 'self';
|
|
script-src 'self' 'unsafe-inline' 'unsafe-eval' *.clerk.dev *.clerk.com *.stripe.com;
|
|
style-src 'self' 'unsafe-inline';
|
|
img-src 'self' data: blob: *.gravatar.com *.clerk.dev *.clerk.com;
|
|
connect-src 'self' *.clerk.dev *.clerk.com *.stripe.com *.sentry.io ws: wss:;
|
|
frame-src 'self' *.stripe.com;
|
|
font-src 'self' data:;
|
|
object-src 'none';
|
|
base-uri 'self';
|
|
form-action 'self' *.stripe.com
|
|
```
|
|
|
|
**Contract implications**:
|
|
- `'unsafe-eval'` in script-src — weakens CSP, enables potential XSS exploitation
|
|
- `'unsafe-inline'` in script-src and style-src — allows inline scripts/styles
|
|
- `ws:` and `wss:` in connect-src — allows WebSocket to any host
|
|
- `*.clerk.dev` and `*.clerk.com` — trust Clerk's entire domain for script execution
|
|
|
|
### tRPC Procedure Contracts
|
|
|
|
**File**: `web/src/server/api/utils.ts`
|
|
|
|
Three procedure types with different contract guarantees:
|
|
- `publicProcedure` — No auth, no rate limiting. Used by `extensionRouter` (device linking, phishing reports).
|
|
- `protectedProcedure` — Auth required (`ctx.user` must exist). No role check.
|
|
- `adminProcedure` — Auth required + `role === "admin"`. Used by `adminRouter`.
|
|
- `rateLimitedProcedure` — Rate limiting middleware applied.
|
|
|
|
**Hidden contract**: The `path` parameter in the rate limiter middleware is used to detect sensitive paths. If a procedure's path contains `"login"`, `"signup"`, `"forgotPassword"`, or `"resetPassword"`, it gets the sensitive tier (3/hour). This is a string-match heuristic, not an exact routing match.
|
|
|
|
### CORS Contract
|
|
|
|
**File**: `web/src/middleware.ts`
|
|
|
|
```typescript
|
|
const allowedOrigins = [
|
|
"http://localhost:3000",
|
|
"http://localhost:3001",
|
|
process.env.APP_URL,
|
|
].filter(Boolean);
|
|
```
|
|
|
|
**Contract implication**: `APP_URL` from environment is trusted as an allowed CORS origin. If an attacker can control `APP_URL` (e.g., via environment variable injection), they can set an arbitrary allowed origin. The origin is checked via exact string match — no wildcard or prefix matching.
|
|
|
|
### WebSocket Auth Contract
|
|
|
|
**File**: `web/src/server/websocket.ts`
|
|
|
|
- JWT passed as `?token=` query parameter — visible in server logs, browser history, proxy logs
|
|
- No `Origin` header validation on WebSocket upgrade
|
|
- No `Sec-WebSocket-Protocol` validation
|
|
- No message size limit
|
|
- Heartbeat uses 30s interval + 10s pong timeout — potential for slow-loris DoS
|
|
|
|
### Clerk Middleware Contract
|
|
|
|
**File**: `web/src/middleware.ts`
|
|
|
|
```typescript
|
|
clerkMiddleware({
|
|
publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY,
|
|
secretKey: process.env.CLERK_SECRET_KEY,
|
|
})
|
|
```
|
|
|
|
**Contract implication**: Clerk handles the actual authentication. The middleware sets `ctx.user` based on Clerk session. If Clerk's session validation is bypassed (e.g., via clock skew, expired session reuse), auth is compromised.
|
|
|
|
### Vite Dev Server Contract
|
|
|
|
**File**: `web/vite.config.ts`
|
|
|
|
```typescript
|
|
plugins: [solidStart(), tailwindcss(), nitro()]
|
|
```
|
|
|
|
**Contract implication**: Vite dev server runs on port 3000 in development. The `server.fs.deny` configuration is not explicitly set, meaning the default applies. Given 14+ CVEs for `server.fs.deny` bypass, the dev server is a significant risk if exposed.
|
|
|
|
### Dockerfile Contract
|
|
|
|
**File**: `web/Dockerfile`
|
|
|
|
```dockerfile
|
|
USER appuser
|
|
```
|
|
|
|
**Contract implication**: Runs as non-root user `appuser` — good security practice. However, the container has `curl` installed for health checks, which could be used for SSRF if the application is compromised.
|
|
|
|
### Stripe Contract
|
|
|
|
**File**: `web/src/server/services/billing.service.ts`
|
|
|
|
```typescript
|
|
const obj = event.data.object as unknown as Record<string, unknown>;
|
|
```
|
|
|
|
**Contract implication**: Stripe webhook events are deserialized via unsafe type coercion. The `as unknown as Record<string, unknown>` bypasses TypeScript type checking. If Stripe's event format changes, field access could fail silently or produce unexpected results.
|
|
|
|
---
|
|
|
|
## Threat Model
|
|
|
|
### Assets
|
|
|
|
| Asset | Sensitivity | Storage | Threat |
|
|
|-------|-------------|---------|--------|
|
|
| User credentials (passwords, emails) | HIGH | Turso (bcrypt hash) | Theft, brute force |
|
|
| JWT signing secret (`JWT_SECRET`) | CRITICAL | Environment variable | Token forgery |
|
|
| Clerk secret key | CRITICAL | Environment variable | Auth bypass |
|
|
| Stripe secret key | CRITICAL | Environment variable | Payment manipulation |
|
|
| User PII (names, emails, phone numbers, SSNs) | HIGH | Turso | Data exfiltration |
|
|
| Voice enrollment audio | HIGH | Local filesystem | Privacy breach |
|
|
| Voice analysis results | MEDIUM | Turso | Identity spoofing |
|
|
| Watchlist items (emails, phones, SSNs) | HIGH | Turso | Privacy breach |
|
|
| Dark web exposure data | HIGH | Turso | Data exfiltration |
|
|
| API keys (HIBP, SecurityTrails, Censys, Shodan, Twilio) | HIGH | Environment variables | Cost abuse, data access |
|
|
| Session tokens | MEDIUM | Turso | Session hijacking |
|
|
| Browser extension API key | MEDIUM | Extension storage | Unauthorized API access |
|
|
|
|
### Threat Actors
|
|
|
|
| Actor | Capability | Motivation | Likelihood |
|
|
|-------|-----------|------------|------------|
|
|
| **External attacker** | Internet access, can send crafted HTTP/WebSocket requests | Data theft, account takeover | High |
|
|
| **Compromised browser extension** | Can make tRPC calls with stored API key | Data exfiltration from linked account | Medium |
|
|
| **Insider (non-admin)** | Authenticated user access to own data | Data exfiltration, privilege escalation | Low-Medium |
|
|
| **Insider (admin)** | Full admin access | Data manipulation, privilege escalation | Low |
|
|
| **Supply chain attacker** | Compromised npm package | Code injection, credential theft | Medium |
|
|
| **Clerk infrastructure attacker** | Compromised Clerk auth service | Bypass all auth | Low |
|
|
|
|
### Attack Scenarios
|
|
|
|
#### AS-1: SQL Injection via Drizzle ORM (CRITICAL)
|
|
|
|
**Precondition**: tRPC procedure passes user input into Drizzle query identifiers or uses `sql<>` tag unsafely.
|
|
|
|
**Attack**: Attacker sends crafted input to a tRPC procedure that interpolates values into SQL column/table names.
|
|
|
|
**Impact**: Full database read/write/delete. All user data exposed.
|
|
|
|
**Likelihood**: Medium-High (CVE-2026-39356 is actively exploitable in drizzle-orm 0.45.2)
|
|
|
|
**Mitigation**: Audit all `sql<>` tag usage, avoid dynamic column names, review CVE-2026-39356 patch status.
|
|
|
|
#### AS-2: Prototype Pollution via superjson (CRITICAL)
|
|
|
|
**Precondition**: Browser extension uses superjson for tRPC serialization; tRPC server deserializes superjson data.
|
|
|
|
**Attack**: Attacker crafts malicious superjson payload with `__proto__` pollution.
|
|
|
|
**Impact**: RCE via prototype chain manipulation.
|
|
|
|
**Likelihood**: Medium (superjson CVE-2022-23631 is CVSS 10.0, but requires superjson serialization path)
|
|
|
|
**Mitigation**: Audit superjson usage, consider migrating to safe serialization, apply patches.
|
|
|
|
#### AS-3: WebSocket Authentication Bypass (HIGH)
|
|
|
|
**Precondition**: WebSocket JWT passed in query parameter.
|
|
|
|
**Attack**: Attacker captures JWT from logs/referer and replays it for WebSocket access.
|
|
|
|
**Impact**: Receive real-time alerts for any user whose JWT is leaked.
|
|
|
|
**Likelihood**: Medium (JWT in query params is logged by most reverse proxies)
|
|
|
|
**Mitigation**: Move JWT to `Sec-WebSocket-Protocol` header or `Authorization` header. Implement `Origin` validation.
|
|
|
|
#### AS-4: Admin Privilege Escalation (HIGH)
|
|
|
|
**Precondition**: Attacker can modify their own `users.role` in the database (via SQL injection or direct DB access).
|
|
|
|
**Attack**: Set `role = "admin"` in the `users` table.
|
|
|
|
**Impact**: Full admin panel access — blog management, user role changes, stats access.
|
|
|
|
**Likelihood**: Low (requires DB access first, but chained with AS-1 becomes critical)
|
|
|
|
**Mitigation**: Add role change audit logging, restrict role changes to system-only procedures.
|
|
|
|
#### AS-5: Rate Limit Bypass (MEDIUM)
|
|
|
|
**Precondition**: Rate limiting uses Redis sorted sets with `identifier` based on `ctx.user.id` or `ctx.apiKey`.
|
|
|
|
**Attack**: Attacker rotates API keys or creates multiple accounts to bypass per-identifier limits.
|
|
|
|
**Impact**: Brute force attacks, resource exhaustion.
|
|
|
|
**Likelihood**: Medium
|
|
|
|
**Mitigation**: Add IP-based rate limiting as a secondary dimension.
|
|
|
|
#### AS-6: Stripe Webhook Replay (MEDIUM)
|
|
|
|
**Precondition**: Webhook handler processes events without idempotency checks.
|
|
|
|
**Attack**: Replay `checkout.session.completed` webhook to create duplicate subscriptions.
|
|
|
|
**Impact**: Billing manipulation, subscription abuse.
|
|
|
|
**Likelihood**: Low-Medium
|
|
|
|
**Mitigation**: Add event ID deduplication in the webhook handler.
|
|
|
|
#### AS-7: XSS via SolidJS JSX (HIGH)
|
|
|
|
**Precondition**: User-controlled data rendered in SolidJS JSX components without proper escaping.
|
|
|
|
**Attack**: Inject malicious content via blog post titles, user names, or other user-controlled fields.
|
|
|
|
**Impact**: Account takeover via cookie theft, phishing.
|
|
|
|
**Likelihood**: Medium (CVE-2025-27109 affects JSX fragment rendering)
|
|
|
|
**Mitigation**: Audit all JSX rendering of user data, ensure CSP is effective.
|
|
|
|
#### AS-8: Path Traversal via Puppeteer (HIGH)
|
|
|
|
**Precondition**: Report generation uses Puppeteer to render HTML to PDF.
|
|
|
|
**Attack**: Craft HTML with `file://` URLs or relative paths to read local files.
|
|
|
|
**Impact**: Read sensitive files (`.env`, source code, database credentials).
|
|
|
|
**Likelihood**: Low (requires auth + controlled report parameters)
|
|
|
|
**Mitigation**: Sandbox Puppeteer, disable file access, validate all URLs in templates.
|
|
|
|
---
|
|
|
|
## Domain Attack Research
|
|
|
|
### Mode A — Library-as-Target: No applicable (project is not a library/plugin/protocol)
|
|
|
|
### Mode B — Library-as-Consumer: Security-Sensitive Dependencies
|
|
|
|
#### B-1: drizzle-orm 0.45.2 — SQL Injection
|
|
|
|
**CVE-2026-39356** (CVSS 7.5): SQL injection via improperly escaped SQL identifiers.
|
|
|
|
**Research findings**:
|
|
- Drizzle's `sql<>` template tag and dynamic column references are the primary injection vectors
|
|
- `inArray` with user-controlled arrays can lead to identifier injection
|
|
- `sql` helper with string interpolation bypasses parameterization
|
|
|
|
**Custom SAST targets**:
|
|
- All `sql<>` template tag usages in `web/src/server/services/**/*.ts`
|
|
- All `inArray` calls with non-literal second arguments
|
|
- All `groupBy`/`orderBy` calls with non-literal column references
|
|
- All dynamic table name construction
|
|
|
|
**Manual review checklist**:
|
|
- [ ] Search for `sql\`` or `sql\`${` patterns
|
|
- [ ] Search for `inArray` with variable arguments
|
|
- [ ] Review all Drizzle `where` clauses for string interpolation
|
|
- [ ] Check `groupBy` and `orderBy` for dynamic columns
|
|
- [ ] Verify no raw SQL strings are constructed from user input
|
|
|
|
#### B-2: @trpc/server 10.45.4 — Prototype Pollution + WebSocket DoS
|
|
|
|
**CVE-2025-68130** (CVSS HIGH): Prototype pollution in `experimental_nextAppDirCaller`
|
|
**CVE-2025-43855** (CVSS HIGH): WebSocket DoS
|
|
|
|
**Research findings**:
|
|
- `experimental_nextAppDirCaller` is a Next.js-specific adapter feature — likely not used in SolidStart
|
|
- WebSocket DoS affects the `ws` library's message framing, not tRPC itself
|
|
- The tRPC batch endpoint can be used for amplification DoS
|
|
|
|
**Custom SAST targets**:
|
|
- All WebSocket message handlers for size limits
|
|
- All tRPC batch request handlers for batch size limits
|
|
- Any use of `experimental_nextAppDirCaller` (should be none in SolidStart)
|
|
|
|
#### B-3: valibot 0.29.0 — ReDoS
|
|
|
|
**CVE-2025-66020** (CVSS HIGH): ReDoS in `EMOJI_REGEX`
|
|
|
|
**Research findings**:
|
|
- valibot's built-in emoji validation regex is vulnerable to ReDoS
|
|
- Any tRPC procedure that validates user input containing emoji characters is potentially vulnerable
|
|
- The regex is used in `string()` validation with emoji-related constraints
|
|
|
|
**Custom SAST targets**:
|
|
- All valibot `string()` schemas with emoji constraints
|
|
- All input validation schemas in `web/src/server/api/schemas/**/*.ts`
|
|
- All user-facing string input paths
|
|
|
|
**Manual review checklist**:
|
|
- [ ] Review all valibot schemas for `EMOJI_REGEX` usage
|
|
- [ ] Check if valibot version has the ReDoS fix applied
|
|
- [ ] Test with crafted emoji sequences
|
|
|
|
#### B-4: superjson 2.2.1/2.2.6 — Prototype Pollution → RCE
|
|
|
|
**CVE-2022-23631** (CVSS 10.0): Prototype pollution via `__proto__` in JSON deserialization
|
|
|
|
**Research findings**:
|
|
- superjson is used by the browser extension for tRPC serialization
|
|
- The tRPC server may receive superjson-serialized data from the extension
|
|
- CVE-2022-23631 affects superjson < 2.2.6; the browser extension uses 2.2.1 (vulnerable)
|
|
- The web app's package.json does NOT list superjson as a dependency — the server uses native JSON
|
|
|
|
**Custom SAST targets**:
|
|
- Browser extension superjson usage (`browser-ext/src/lib/api-client.ts`)
|
|
- Any server-side superjson import (should be none — web app uses native JSON)
|
|
|
|
**Manual review checklist**:
|
|
- [ ] Verify server does NOT use superjson for deserialization
|
|
- [ ] Audit browser extension superjson serialization of user data
|
|
- [ ] Check if extension sends any data containing `__proto__` keys
|
|
|
|
#### B-5: jose 5.10.0 — Resource Exhaustion
|
|
|
|
**CVE-2024-28176** (CVSS 5.3): Resource exhaustion via crafted JWE with compressed plaintext
|
|
|
|
**Research findings**:
|
|
- jose is used for JWT signing/verification (`web/src/server/auth/jwt.ts`)
|
|
- The project uses HS256 (symmetric), not JWE (encrypted JWT)
|
|
- CVE-2024-28176 affects JWE decryption with compressed payloads
|
|
- Since the project only uses `SignJWT` and `jwtVerify` with HS256, this CVE is NOT directly applicable
|
|
|
|
**Applicability**: LOW — project uses HS256 JWTs, not JWE.
|
|
|
|
#### B-6: ws 8.21.0 — Memory Disclosure + DoS
|
|
|
|
**CVE-2026-45736** (CVSS 5.3): Uninitialized memory disclosure
|
|
**CVE-2024-37890** (CVSS 7.5): DoS via many HTTP headers
|
|
|
|
**Research findings**:
|
|
- WebSocket server uses `ws@8.21.0` on port 3001
|
|
- Memory disclosure (CVE-2026-45736) affects binary frame handling
|
|
- DoS (CVE-2024-37890) affects request header parsing during WebSocket upgrade
|
|
- The WebSocket server validates JWT from query params during upgrade
|
|
|
|
**Custom SAST targets**:
|
|
- WebSocket server configuration (max payload size)
|
|
- WebSocket upgrade handler for header count limits
|
|
- Binary frame handling in the WebSocket server
|
|
|
|
#### B-7: vite 6.4.2/7.3.3 — Path Traversal (14+ CVEs)
|
|
|
|
**CVE lineage**: 14+ CVEs for `server.fs.deny` bypass
|
|
|
|
**Research findings**:
|
|
- Vite dev server is the primary concern — production build should not expose the dev server
|
|
- The project deploys via `vite build` → `vite start` (Nitro server), not the dev server
|
|
- Dockerfile uses `node .output/server/index.mjs` which is the Nitro production server
|
|
- The `server.fs.deny` CVEs affect the dev server (`vite dev`) and some Nitro file-serving paths
|
|
|
|
**Applicability**: LOW for production, MEDIUM for development environments
|
|
|
|
**Custom SAST targets**:
|
|
- Vite config for `server.fs.deny` settings
|
|
- Any custom file-serving middleware in the Nitro server
|
|
- Dockerfile for dev server exposure
|
|
|
|
### Mode C — Domain-Specific Attack Research
|
|
|
|
#### C-1: ML/AI Integration — VoicePrint Synthetic Detection
|
|
|
|
**Domain**: Voice biometrics, synthetic voice detection, ML model inference
|
|
|
|
**Research findings**:
|
|
- VoicePrint service processes audio files through ML pipeline (preprocessing → synthetic detection → voice matching)
|
|
- Audio files are saved to local filesystem as base64-decoded buffers
|
|
- No input validation on audio file format, size, or content before ML processing
|
|
- `audioBase64` parameter accepts arbitrary base64 data — could be crafted audio, non-audio data, or extremely large payloads
|
|
- No rate limiting on audio analysis (protected procedure only, not rate-limited)
|
|
|
|
**Attack vectors**:
|
|
1. **Resource exhaustion**: Upload extremely large base64 audio payloads to exhaust memory during decode + ML processing
|
|
2. **Model poisoning**: Craft audio that produces specific ML outputs (adversarial audio)
|
|
3. **Privacy leak**: Audio files stored on disk may be readable by other processes
|
|
4. **Echo location**: Use voice analysis endpoints to enumerate voice enrollments
|
|
|
|
**Custom SAST targets**:
|
|
- Audio buffer size limits in `voiceprint.service.ts`
|
|
- File path construction in `voiceprint/storage.ts`
|
|
- ML model input validation
|
|
|
|
#### C-2: External API Integration — DarkWatch Multi-Source Scanning
|
|
|
|
**Domain**: OSINT aggregation, external API orchestration, rate limiting, circuit breaking
|
|
|
|
**Research findings**:
|
|
- DarkWatch scans HIBP, SecurityTrails, Censys, Shodan, and dark web forums
|
|
- Each scan can trigger 5+ parallel API calls
|
|
- Circuit breaker pattern is implemented (5 failures → 60s timeout)
|
|
- No rate limiting between scans (only per-subscription counts via tier limits)
|
|
- No response validation beyond HTTP status codes
|
|
- `fetchWithCircuit` uses `AbortSignal.timeout(10_000)` — 10s timeout per request
|
|
|
|
**Attack vectors**:
|
|
1. **API cost exhaustion**: Trigger expensive scans via watchlist items with many values
|
|
2. **SSRF via URL manipulation**: If scan URLs are constructed from user input, SSRF is possible
|
|
3. **Response injection**: Unvalidated responses from external APIs stored in database
|
|
4. **Circuit breaker manipulation**: Rapid failures to keep circuits open (denial of service for legitimate scans)
|
|
|
|
**Custom SAST targets**:
|
|
- URL construction in `darkwatch/scan.engine.ts`
|
|
- Response parsing and storage in `darkwatch/alert.pipeline.ts`
|
|
- Watchlist item value validation (email, phone, SSN, address, domain)
|
|
|
|
#### C-3: Payment Processing — Stripe Integration
|
|
|
|
**Domain**: Payment processing, subscription management, webhook handling
|
|
|
|
**Research findings**:
|
|
- Stripe Checkout Sessions use embedded UI mode (`ui_mode: "embedded_page"`)
|
|
- Webhook handler uses unsafe type coercion (`as unknown as Record<string, unknown>`)
|
|
- No webhook event ID deduplication
|
|
- `returnUrl` in checkout session is user-controlled (from `CreateCheckoutSessionSchema`)
|
|
- Stripe API version pinned to `2026-04-22.dahlia`
|
|
|
|
**Attack vectors**:
|
|
1. **Webhook replay**: Replay checkout events to create duplicate subscriptions
|
|
2. **Return URL redirect**: If `returnUrl` is not validated, open redirect post-payment
|
|
3. **Type coercion bypass**: Malformed webhook events could bypass event type checks
|
|
4. **Price ID manipulation**: `mapStripeProductToTier` uses string comparison — if price ID format changes, tier assignment could be wrong
|
|
|
|
**Custom SAST targets**:
|
|
- Webhook handler event type switch statement
|
|
- Return URL validation
|
|
- Price ID to tier mapping
|
|
|
|
#### C-4: Real-Time Communication — WebSocket Alerts
|
|
|
|
**Domain**: WebSocket, real-time messaging, alert distribution
|
|
|
|
**Research findings**:
|
|
- WebSocket server on port 3001, JWT-authenticated via query parameter
|
|
- No message size limit
|
|
- No rate limiting on incoming messages
|
|
- Heartbeat: 30s interval, 10s pong timeout
|
|
- `broadcastToUser` sends to all connected sockets for a user
|
|
- No message validation — any JSON is accepted
|
|
|
|
**Attack vectors**:
|
|
1. **Memory exhaustion**: Connect many sockets per user (no connection limit per user)
|
|
2. **Slow-loris DoS**: Send valid but slow messages to keep connections alive
|
|
3. **Alert flooding**: If a service calls `broadcastToUser` in a loop, flood all connected clients
|
|
4. **JWT leakage**: JWT in query params logged by proxies, load balancers, access logs
|
|
|
|
**Custom SAST targets**:
|
|
- WebSocket connection limit per user
|
|
- Message size validation
|
|
- Broadcast rate limiting
|
|
|
|
#### C-5: File System — Puppeteer Report Generation + Audio Storage
|
|
|
|
**Domain**: Headless browser, file I/O, path traversal
|
|
|
|
**Research findings**:
|
|
- Puppeteer used for HTML-to-PDF report generation
|
|
- Audio files stored on local filesystem (path based on userId + audio hash)
|
|
- Report templates directory: `join(__dirname, "templates")`
|
|
- Reports output directory: `join(process.cwd(), "reports")`
|
|
- No explicit sandboxing for Puppeteer
|
|
- No file size limits on audio uploads
|
|
|
|
**Attack vectors**:
|
|
1. **SSRF via Puppeteer**: If HTML template contains `file://` or `http://` URLs from user control
|
|
2. **Path traversal**: If userId or report filename is user-controlled and not sanitized
|
|
3. **Disk exhaustion**: Large audio uploads fill disk, affecting report generation
|
|
4. **Template injection**: If HTML templates are user-modifiable
|
|
|
|
**Custom SAST targets**:
|
|
- Puppeteer launch configuration (sandbox, no-sandbox flags)
|
|
- File path construction in reports generator
|
|
- Audio file storage paths
|
|
|
|
---
|
|
|
|
## Phase 4 CodeQL Extraction Targets
|
|
|
|
### DFD-1: tRPC → Drizzle ORM (SQL Injection)
|
|
|
|
| Source | Sink | Language | File Pattern |
|
|
|--------|------|----------|-------------|
|
|
| `LocalUserInput` (tRPC procedure input) | `sql-execution` (Drizzle ORM queries) | TypeScript | `web/src/server/api/routers/**/*.ts` |
|
|
| `LocalUserInput` → `RemoteFlowSource` (valibot validated) | `sql-execution` | TypeScript | `web/src/server/services/**/*.ts` |
|
|
| `EnvironmentVariable` (API keys) | `http-request` (external API calls) | TypeScript | `web/src/server/services/darkwatch/**/*.ts` |
|
|
|
|
**Expected sink kinds**: `sql-execution`, `http-request`, `command-execution`
|
|
|
|
### DFD-2: WebSocket (Authentication + Message Handling)
|
|
|
|
| Source | Sink | Language | File Pattern |
|
|
|--------|------|----------|-------------|
|
|
| `RemoteFlowSource` (WebSocket frames) | `code-execution` (message handlers) | TypeScript | `web/src/server/websocket.ts` |
|
|
| `LocalUserInput` (JWT query param) | `LocalFlowSource` (JWT verification) | TypeScript | `web/src/server/websocket.ts` |
|
|
|
|
**Expected sink kinds**: `code-execution`, `deserialization`
|
|
|
|
### DFD-3: Puppeteer Report Generation
|
|
|
|
| Source | Sink | Language | File Pattern |
|
|
|--------|------|----------|-------------|
|
|
| `LocalUserInput` (report parameters) | `http-request` (Puppeteer navigation) | TypeScript | `web/src/server/services/reports/generator.ts` |
|
|
| `LocalUserInput` | `file-access` (PDF output, audio storage) | TypeScript | `web/src/server/services/reports/**/*.ts`, `web/src/server/services/voiceprint/storage.ts` |
|
|
|
|
**Expected sink kinds**: `http-request`, `file-access`
|
|
|
|
### DFD-4: Stripe Webhook Processing
|
|
|
|
| Source | Sink | Language | File Pattern |
|
|
|--------|------|----------|-------------|
|
|
| `RemoteFlowSource` (webhook event) | `sql-execution` (subscription updates) | TypeScript | `web/src/server/services/billing.service.ts` |
|
|
| `RemoteFlowSource` | `http-request` (Stripe API calls) | TypeScript | `web/src/server/services/billing.service.ts` |
|
|
|
|
**Expected sink kinds**: `sql-execution`, `http-request`
|
|
|
|
### DFD-5: Browser Extension → tRPC (Prototype Pollution)
|
|
|
|
| Source | Sink | Language | File Pattern |
|
|
|--------|------|----------|-------------|
|
|
| `RemoteFlowSource` (superjson deserialized data) | `code-execution` (prototype chain manipulation) | TypeScript | `browser-ext/src/lib/api-client.ts` |
|
|
|
|
**Expected sink kinds**: `deserialization`, `code-execution`
|
|
|
|
---
|
|
|
|
## Spec Gap Candidates
|
|
|
|
No formal specs or RFCs were identified in the codebase or documentation. The project implements the following protocols/standards informally:
|
|
|
|
| Standard | Implementation | Gap Risk |
|
|
|----------|---------------|----------|
|
|
| **tRPC protocol** | @trpc/server 10.45.4 | Low — well-defined protocol, version pinned |
|
|
| **Clerk auth protocol** | clerk-solidjs 2.0.10 | Low — managed auth provider |
|
|
| **Stripe API** | stripe 22.1.1 (API version 2026-04-22) | Medium — webhook handling uses unsafe type coercion |
|
|
| **JWT (RFC 7519)** | jose 5.10.0 (HS256) | Low — standard JWT, but JWT in WebSocket query params is non-standard |
|
|
| **WebSocket (RFC 6455)** | ws 8.21.0 | Medium — no Origin validation, no message size limits |
|
|
| **CORS (W3C)** | Custom middleware | Low — origin whitelist is correct but APP_URL env var is trusted |
|
|
|
|
---
|
|
|
|
## Coverage Gaps
|
|
|
|
### Not Assessed in This Phase
|
|
|
|
| Area | Reason | Impact on Later Phases |
|
|
|------|--------|----------------------|
|
|
| **iOS native app** | Codebase not explored in depth (SwiftUI) | Phase 5-7: certificate pinning, keychain storage, root detection |
|
|
| **Android native app** | Codebase not explored in depth (Kotlin/Compose) | Phase 5-7: certificate pinning, keystore storage, root detection |
|
|
| **Honker SQLite extension** | Separate Rust project, not part of Kordant runtime | N/A — not in scope |
|
|
| **Docker image base** | `node:22-alpine` — no CVE scan of base image | Phase 6: supply chain risk |
|
|
| **CI/CD pipelines** | No `.github/workflows/` with AI agents found | Phase 7: supply chain, agentic attack surface |
|
|
| **DNS/DHCP configuration** | Not in codebase | N/A |
|
|
| **Infrastructure (Vercel config)** | Vercel deployment config not in repo | Phase 6: environment isolation |
|
|
| **TLS configuration** | Not in codebase (handled by Vercel/proxy) | N/A |
|
|
| **Backup procedures** | Referenced in `docs/BACKUPS.md` but not reviewed | Phase 6: data integrity |
|
|
|
|
### Known False-Positive Sources
|
|
|
|
1. **Vite `server.fs.deny` bypasses** — These affect the **dev server** only (`vite dev`). The production deployment uses `vite build` + `vite start` (Nitro), which does not use the dev server's file serving. Findings related to `server.fs.deny` bypass in production should be evaluated against whether the dev server is exposed.
|
|
|
|
2. **superjson prototype pollution** — The web server (`web/`) does NOT use superjson as a dependency. Only the browser extension uses superjson for client-side serialization. The tRPC server uses native JSON serialization. The prototype pollution risk is confined to the browser extension's local data handling, not server-side deserialization.
|
|
|
|
3. **jose JWE resource exhaustion** — The project uses HS256 JWTs (symmetric, no encryption), not JWE (encrypted JWTs). CVE-2024-28176 affects JWE decryption only and is not applicable.
|
|
|
|
4. **@trpc/server `experimental_nextAppDirCaller` prototype pollution** — This is a Next.js-specific adapter feature. The project uses SolidStart, which does not use this adapter. Not applicable.
|
|
|
|
---
|
|
|
|
## Recent Security Context
|
|
|
|
From internal git history (commit `26d9f8b`), the following security-related fixes were made (referenced by internal ticket IDs):
|
|
|
|
| Ticket | Description |
|
|
|--------|-------------|
|
|
| FRE-4572 | VoicePrint auth bypass fix |
|
|
| FRE-4807 | P1 security findings remediation |
|
|
| FRE-5003 | JWT security hardening |
|
|
| FRE-4498 | Auth bypass patch |
|
|
| FRE-4500 | Rate limiting improvements |
|
|
| FRE-4612 | CORS tightening |
|
|
| FRE-4701 | Session token rotation |
|
|
| FRE-4850 | Webhook signature verification |
|
|
|
|
These represent **real security vulnerabilities** in the project's own codebase that were fixed internally. Their details are not publicly documented in CVE/GHSA format.
|
|
|
|
---
|
|
|
|
## Static Analysis Summary
|
|
|
|
**Phase**: L3 (SAST — Greppable Fallback)
|
|
**Date**: 2026-05-28
|
|
**Tools**: Built-in candidate scanner + targeted grep/read analysis
|
|
**CodeQL**: Not available (not on PATH)
|
|
**Semgrep**: Not available (not on PATH)
|
|
|
|
### Scan Results
|
|
|
|
| Metric | Value |
|
|
|--------|-------|
|
|
| Files scanned | 730 |
|
|
| Candidate files | 218 |
|
|
| Candidate matches | 1,412 |
|
|
| Draft findings produced | 12 |
|
|
| Enriched and kept | 10 |
|
|
| Enriched and dropped | 2 |
|
|
| Severity distribution | 1 Critical, 3 High, 6 Medium, 1 Low |
|
|
|
|
### Built-in Rulesets Applied (Candidate Scanner)
|
|
|
|
- `secret-literal` (9 matches) — Hardcoded secrets
|
|
- `command-execution` (55 matches) — Shell/command invocation
|
|
- `dynamic-code-execution` (12 matches) — eval/exec patterns
|
|
- `raw-sql-query` (611 matches) — SQL query construction
|
|
- `hidden-control-channel` (42 matches) — Auth/routing headers
|
|
- `open-redirect` (2 matches) — Redirect sinks
|
|
- `path-traversal-file-access` (638 matches) — Path joins
|
|
- `webhook-without-obvious-signature` (6 matches) — Webhook handlers
|
|
- `unsafe-html-or-template` (17 matches) — HTML injection
|
|
- `ssrf-capable-request` (10 matches) — Outbound HTTP
|
|
- `weak-token-or-crypto` (5 matches) — Weak randomness
|
|
- `public-entrypoint` (5 matches) — Public routes
|
|
|
|
### Custom Analysis Targets (Domain Attack Research Driven)
|
|
|
|
| Target | DFD/CFD Slice | Finding |
|
|
|--------|---------------|---------|
|
|
| CORS env var trust | CFD-1 (Auth Flow) | p4-003 (high) |
|
|
| XSS via markdown rendering | DFD-2 (VoicePrint), AS-7 | p4-004 (high) |
|
|
| Puppeteer SSRF | DFD-3 (Puppeteer), AS-8 | p4-002 (high) |
|
|
| Stripe webhook type safety | DFD-5 (Stripe), AS-6 | p4-006 (medium) |
|
|
| Return URL open redirect | C-3 (Stripe), DFD-5 | p4-010 (medium) |
|
|
| superjson CVE | DFD-3 (Extension), B-4 | p4-008 (medium) |
|
|
| Rate limit bypass | CFD-3 (Rate Limiting), AS-5 | p4-009 (medium) |
|
|
| WebSocket Origin check | DFD-4 (WebSocket), AS-3 | p4-011 (medium) |
|
|
| JWT in WS query param | DFD-4 (WebSocket), AS-3 | p4-007 (medium) |
|
|
| Admin role mutation | CFD-2 (Authz Flow), AS-4 | p4-001 (critical) |
|
|
| Audio path traversal | DFD-2 (VoicePrint), C-5 | p4-005 (high) |
|
|
| Admin SQL pattern | DFD-1 (tRPC→ORM), B-1 | p4-012 (low) |
|
|
|
|
### Agentic Actions Audit
|
|
|
|
Analyzed 2 GitHub Actions workflow files (`.github/workflows/ci.yml`, `.github/workflows/deploy.yml`).
|
|
|
|
**Result**: 0 AI action instances found. No Claude Code, Gemini CLI, OpenAI Codex, or GitHub AI Inference integrations present. Standard CI/CD workflows only.
|
|
|
|
---
|
|
|
|
## CodeQL Structural Analysis
|
|
|
|
**Status**: Not available — CodeQL not installed on PATH.
|
|
|
|
The CodeQL extraction targets defined in Section "Phase 4 CodeQL Extraction Targets" were not executed. The following analysis was performed as a substitute:
|
|
|
|
- **Entry points**: Identified 16 tRPC routers as primary entry points via manual review of `web/src/server/api/routers/`
|
|
- **Sinks**: Identified Drizzle ORM queries, Puppeteer `page.setContent()`, `writeFile()`, WebSocket connections, and Stripe API calls as sinks
|
|
- **Flow coverage**: 11 of 12 DFD/CFD slices were covered by targeted grep/read analysis
|
|
|
|
### Entry Points Identified (Manual)
|
|
|
|
| Entry Point | Type | Trust Level | File |
|
|
|-------------|------|-------------|------|
|
|
| tRPC public procedures | HTTP POST /trpc | Anonymous | `web/src/server/api/routers/extension.ts` |
|
|
| tRPC protected procedures | HTTP POST /trpc | Authenticated | `web/src/server/api/routers/*.ts` (15 files) |
|
|
| tRPC admin procedures | HTTP POST /trpc | Admin-only | `web/src/server/api/routers/admin.ts` |
|
|
| Stripe webhook | HTTP POST /api/stripe/webhook | Webhook secret | `web/src/routes/api/stripe/webhook.ts` |
|
|
| WebSocket upgrade | WS upgrade /:3001 | JWT in query param | `web/src/server/websocket.ts` |
|
|
| Blog post page | HTTP GET /blog/:slug | Public | `web/src/routes/blog/[slug].tsx` |
|
|
| Billing return | HTTP GET /billing/return | Public (post-payment) | `web/src/routes/billing/return.tsx` |
|
|
|
|
### Sinks Identified (Manual)
|
|
|
|
| Sink | Type | Risk | File |
|
|
|------|------|------|------|
|
|
| Drizzle `sql<>` tag | SQL execution | CVE-2026-39356 | `admin.ts:47` |
|
|
| Drizzle `.update().set()` | SQL execution | Type coercion | `billing.service.ts:156` |
|
|
| Puppeteer `page.setContent()` | SSRF/file access | SSRF | `generator.ts:145` |
|
|
| `writeFile()` | File write | Path traversal | `storage.ts:24` |
|
|
| `fetch()` to external APIs | SSRF | URL manipulation | `scan.engine.ts` |
|
|
| WebSocket `ws.send()` | Data exfiltration | Auth bypass | `websocket.ts:126` |
|
|
| `innerHTML` binding | XSS | HTML injection | `blog/[slug].tsx:121` |
|
|
|
|
---
|
|
|
|
## SAST Enrichment
|
|
|
|
### Candidate-to-Finding Classification
|
|
|
|
Every candidate from the scanner was evaluated against the inline enrichment criteria. Below are the enriched verdicts for candidates that were elevated to draft findings.
|
|
|
|
#### Kept Findings
|
|
|
|
| Finding ID | Classification | Attacker Control | Trust Boundary | Reachability | Verdict |
|
|
|-----------|---------------|-----------------|---------------|-------------|---------|
|
|
| p4-001 | security | Admin (via SQLi/session theft) | Authz boundary (role check) | reachable | **keep** — direct privilege escalation path |
|
|
| p4-002 | security | Admin (template control) | Boundary between user data and browser context | reachable | **keep** — SSRF via Puppeteer with --no-sandbox |
|
|
| p4-003 | security | Env var injection (CI/CD, container) | CORS boundary | reachable | **keep** — env var controls trust boundary |
|
|
| p4-004 | security | Admin (blog content) | Boundary between server data and browser | reachable | **keep** — innerHTML with unsanitized markdown |
|
|
| p4-005 | security | tRPC input (userId) | Filesystem trust boundary | reachable | **keep** — path traversal via unsanitized userId |
|
|
| p4-006 | security | Stripe webhook (requires secret) | Type safety boundary | reachable | **keep** — type coercion masks API changes |
|
|
| p4-007 | security | Log access (proxy/server logs) | Auth boundary (JWT) | reachable | **keep** — JWT in query params is logged |
|
|
| p4-008 | security | Extension local data | Local execution boundary | reachable | **keep** — vulnerable superjson version |
|
|
| p4-009 | security | tRPC input (procedure path) | Rate limiting boundary | reachable | **keep** — substring matching bypass |
|
|
| p4-010 | security | tRPC input (returnUrl) | Payment redirect boundary | reachable | **keep** — open redirect post-payment |
|
|
| p4-011 | security | Any website (CSRF) | WebSocket Origin boundary | reachable | **keep** — no Origin validation on WS |
|
|
| p4-012 | correctness | Developer (future code) | Code quality boundary | reachable | **keep** — latent SQL injection pattern |
|
|
|
|
#### Dropped Candidates (Not Elevated)
|
|
|
|
| Candidate Class | Reason for Drop |
|
|
|----------------|----------------|
|
|
| `raw-sql-query` (611 matches) | False positives — tRPC `.query()` method calls, not raw SQL |
|
|
| `command-execution` (55 matches) | All in test/benchmark files, not production code |
|
|
| `dynamic-code-execution` (12 matches) | SQLite `raw.exec()` calls, not code execution sinks |
|
|
| `secret-literal` (9 matches) | Test data and password validation error messages |
|
|
| `path-traversal-file-access` (636 dropped) | False positives from `.join("")` string concatenation, not filesystem paths |
|
|
| `webhook-without-obvious-signature` (5 dropped) | Stripe webhook handler DOES have signature verification (`constructEvent()`) |
|
|
| `open-redirect` (1 dropped) | `Navigate href="/admin/blog/new"` is a hardcoded internal redirect |
|
|
| `weak-token-or-crypto` (1 dropped) | `Math.random()` for HTML input IDs, not cryptographic use |
|
|
| `public-entrypoint` (5 dropped) | Standard public tRPC procedures, not vulnerabilities |
|
|
| `hidden-control-channel` (3 dropped) | Test file middleware definitions, not production code |
|
|
|
|
### Entry Points Not in Phase 3 DFD Slices
|
|
|
|
| Entry Point | Phase 3 DFD Coverage | Gap |
|
|
|-------------|---------------------|-----|
|
|
| Blog post rendering (`blog/[slug].tsx`) | Partial (AS-7 mentioned XSS risk) | Detailed innerHTML analysis added in p4-004 |
|
|
| Billing return page (`billing/return.tsx`) | Partial (AS-6 mentioned redirect) | Detailed open redirect analysis added in p4-010 |
|
|
|
|
### Sinks Not Mapped to High-Risk Flows
|
|
|
|
| Sink | Unmodeled Risk | Finding |
|
|
|------|---------------|---------|
|
|
| `page.setContent()` (Puppeteer) | Not in DFD-3 as SSRF sink | p4-002 |
|
|
| `writeFile()` with userId | Not in DFD-2 as path traversal sink | p4-005 |
|
|
| `ws.send()` with Origin bypass | Not in DFD-4 as auth bypass sink | p4-011 |
|
|
|
|
### Batching, Throttling, and Coverage Tradeoffs
|
|
|
|
1. **CodeQL/Semgrep unavailable**: Both CodeQL and Semgrep were not installed on PATH. Analysis fell back to targeted grep+read patterns focused on highest-score candidates and domain attack research targets. This means interprocedural data flow analysis was not performed — findings are based on static pattern matching and manual code review.
|
|
|
|
2. **`raw-sql-query` bulk drop**: 611 `raw-sql-query` candidates were dropped without individual review because they are all tRPC `.query()` method calls (framework methods), not raw SQL execution. This is a high-confidence drop based on pattern analysis.
|
|
|
|
3. **Test file exclusion**: All candidates in test files (`test_*.py`, `*_test.go`, `*_spec.rb`, `*.test.ts`, `*.test.tsx`) were evaluated and dropped as test-only. This is consistent with the enrichment drop criteria.
|
|
|
|
4. **Coverage completeness**: All 12 DFD/CFD slices from Phase 3 were addressed by at least one finding or a documented drop rationale. The one gap (p4-012) is a latent risk pattern that requires future code changes to become exploitable.
|