UI: warm daylight design system (Tailwind v4 @theme palette, gh-* component classes, watercolor grain, Zen Maru Gothic + Klee One fonts), animated SSR-safe GhibliBackground (drifting clouds, meadow hills, soot sprites), and a full reskin of navbar, connect button, dapp page, loan cards, settings modal, and readme. Fixes the bg-white-on-dark loan-card inconsistency. Web3/business logic untouched. Docs: converted docs/ into an Obsidian vault (frontmatter, [[wikilinks]], callouts, Home MOC, folders Architecture/Operations/Audits) and added a full-project audit note (Project Audit 2026-06). Redacted a real leaked Schedy key value from the security audit example (rotate it at Schedy). Also commits the previously-untracked server layer: app/api (cron + tasks routes) and lib (redis, ssrf-guard, task-store). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
741 lines
33 KiB
Markdown
741 lines
33 KiB
Markdown
---
|
|
title: Security Audit
|
|
tags: [mortgagefi, audit, security]
|
|
type: audit
|
|
status: reference
|
|
updated: 2026-06-14
|
|
---
|
|
|
|
# Security Audit — MortgageFi
|
|
|
|
**Audit Date:** 2026-05-01
|
|
**Remediation Date:** 2026-05-01
|
|
**Scope:** Full stack — Next.js frontend, nftcache (Go), embedded scheduler API, ntfy, nginx, Docker Compose, configuration
|
|
**Risk Rating:** 🟡 **MEDIUM** — All critical and high-severity issues have been addressed. Remaining risk is low-to-medium and manageable for production.
|
|
|
|
> [!warning] Overall Risk Rating: MEDIUM
|
|
> All critical and high-severity issues have been addressed. Remaining risk is low-to-medium and manageable for production.
|
|
|
|
---
|
|
|
|
## Remediation Summary
|
|
|
|
All **7 critical** and **11 high** severity issues identified in the initial audit have been resolved. Key changes:
|
|
|
|
1. ✅ **Schedy removed** — Replaced with Vercel-native Next.js API routes (`/api/tasks`, `/api/cron`) + Redis
|
|
2. ✅ **SSRF eliminated** — New scheduler validates URLs against private IP / internal hostname blocklist
|
|
3. ✅ **Secrets removed from client bundle** — No more `NEXT_PUBLIC_` API keys; backend uses server-side env only
|
|
4. ✅ **Sensitive credentials removed from localStorage** — AWS SNS provider removed; Schedy fields removed; security warnings added
|
|
5. ✅ **nftcache hardened** — API key now required at startup; address validation rejects malformed/truncated addresses; maxTokenId capped at 10,000
|
|
6. ✅ **RPC injection prevented** — Frontend validates RPC URLs against an allowlist
|
|
7. ✅ **Network exposure reduced** — Direct container ports removed; only nginx exposed
|
|
8. ✅ **Security headers added** — CSP, HSTS, X-Frame-Options, X-Content-Type-Options in nginx and Next.js
|
|
9. ✅ `.env.example` created and `.gitignore` updated
|
|
|
|
---
|
|
|
|
## Executive Summary (Original)
|
|
|
|
The original codebase contained **7 critical**, **11 high**, and **15 medium/low severity** issues. The most severe were:
|
|
|
|
1. **Server-Side Request Forgery (SSRF)** in Schedy allowing internal network probing
|
|
2. **Secrets committed to version control** including SMTP passwords, API keys, and WalletConnect credentials
|
|
3. **API keys bundled into client-side JavaScript** via `NEXT_PUBLIC_` variables
|
|
4. **Sensitive credentials stored in browser localStorage** without encryption (AWS keys, schedy API key, notification tokens)
|
|
5. **Denial-of-Service via unauthenticated RPC scanning** in nftcache
|
|
6. **Address canonicalization bug** enabling ownership spoofing
|
|
|
|
---
|
|
|
|
## Critical Severity
|
|
|
|
### C1 — SSRF in Schedy Task Executor 🔴 [FIXED]
|
|
**Location:** `mortgagefi-frontend/submodules/schedy/internal/executor/executor.go` (removed)
|
|
**CVSS:** ~9.1 (Critical)
|
|
|
|
> [!danger] Critical (CVSS ~9.1) — SSRF in Schedy Task Executor [FIXED]
|
|
> Schedy executed HTTP webhooks to **arbitrary URLs** with zero URL validation.
|
|
|
|
**Issue:** Schedy executed HTTP webhooks to **arbitrary URLs** with zero URL validation.
|
|
|
|
**Remediation:** Schedy removed entirely. New scheduler in `app/api/cron/route.ts` uses `lib/ssrf-guard.ts` which blocks private IPs, loopback, link-local, multicast, and internal hostnames. Only HTTPS URLs are allowed in production.
|
|
|
|
```go
|
|
// executor.go:37 — task.URL is used directly
|
|
req, err := http.NewRequest(http.MethodPost, task.URL, bytes.NewBuffer(bodyBytes))
|
|
```
|
|
|
|
**Impact:** An attacker with a valid Schedy API key (or if auth is disabled) can schedule tasks targeting:
|
|
- `http://169.254.169.254/latest/meta-data/` — AWS/GCP/Cloud metadata endpoints
|
|
- `http://localhost:8090/nfts/invalidate` — nftcache internal APIs
|
|
- `http://localhost:80/ntfy` — ntfy admin endpoints
|
|
- Internal Docker network services (`http://frontend:3000`, `http://nftcache:8090`)
|
|
|
|
**Proof of Concept:**
|
|
```bash
|
|
curl -X POST http://localhost:8080/tasks \
|
|
-H "X-API-Key: $SCHEDY_API_KEY" \
|
|
-d '{
|
|
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/",
|
|
"execute_at": "2026-05-01T12:00:00Z",
|
|
"payload": "x"
|
|
}'
|
|
```
|
|
|
|
**Remediation:**
|
|
```go
|
|
// Add URL validation in CreateTask handler
|
|
func isAllowedURL(u string) bool {
|
|
parsed, err := url.Parse(u)
|
|
if err != nil { return false }
|
|
if parsed.Scheme != "https" { return false } // enforce HTTPS
|
|
host := parsed.Hostname()
|
|
ip := net.ParseIP(host)
|
|
if ip != nil {
|
|
// Block private/reserved IPs
|
|
if ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsMulticast() {
|
|
return false
|
|
}
|
|
}
|
|
// Block internal hostnames
|
|
blocked := []string{"localhost", "nftcache", "schedy", "ntfy", "frontend", "web"}
|
|
for _, b := range blocked {
|
|
if strings.EqualFold(host, b) { return false }
|
|
}
|
|
return true
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### C2 — Secrets Committed to Version Control 🔴 [FIXED]
|
|
**Location:** `.env`, `.env.local`
|
|
**CVSS:** ~9.0 (Critical)
|
|
|
|
> [!danger] Critical (CVSS ~9.0) — Secrets Committed to Version Control [FIXED]
|
|
> The repository contained **real, active secrets** in committed files.
|
|
|
|
**Issue:** The repository contained **real, active secrets** in committed files.
|
|
|
|
| Secret | Location | Risk |
|
|
|--------|----------|------|
|
|
| `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID` | `.env`, `.env.local` | Project ID exposed |
|
|
| `SCHEDY_API_KEY` | `.env`, `.env.local` | Full scheduler control |
|
|
| `NEXT_PUBLIC_SCHEDY_API_KEY` | `.env`, `.env.local` | Same key, also in browser bundle |
|
|
| `NTFY_SMTP_SENDER_PASS` | `.env`, `.env.local` | SMTP password (Gmail/App Password) |
|
|
| `CRONHOST_API` | `.env`, `.env.local` | Legacy cronhost API key |
|
|
| `manwe-secret` | `.env`, `.env.local` | Unknown but looks sensitive |
|
|
|
|
**Impact:** Anyone with repository access (or if repo becomes public) has full credentials. The SMTP password grants access to the `mortgagefi@amn.gg` mailbox.
|
|
|
|
**Remediation:** ✅ Applied
|
|
1. ✅ Created `.env.example` with dummy values only
|
|
2. ✅ Added `.gitignore` at project root:
|
|
```
|
|
.env
|
|
.env.local
|
|
.env.*.local
|
|
```
|
|
3. **Action required by user:** Rotate all secrets and purge from Git history:
|
|
```bash
|
|
git rm --cached .env .env.local
|
|
git filter-repo --path .env --path .env.local --invert-paths
|
|
# Or use BFG Repo-Cleaner
|
|
```
|
|
|
|
---
|
|
|
|
### C3 — API Keys Bundled in Client-Side JavaScript 🔴 [FIXED]
|
|
**Location:** `.env.local`, `mortgagefi-frontend/utils/scheduler.ts`
|
|
**CVSS:** ~8.5 (Critical)
|
|
|
|
> [!danger] Critical (CVSS ~8.5) — API Keys Bundled in Client-Side JavaScript [FIXED]
|
|
> `NEXT_PUBLIC_SCHEDY_API_KEY` was compiled into the client bundle. Any visitor could extract it.
|
|
|
|
**Issue:** `NEXT_PUBLIC_SCHEDY_API_KEY` was compiled into the client bundle. Any visitor could extract it.
|
|
|
|
**Remediation:** ✅ `NEXT_PUBLIC_SCHEDY_API_KEY` and `NEXT_PUBLIC_SCHEDY_URL` removed entirely. The scheduler is now embedded as same-origin API routes (`/api/tasks`), so no external API key is needed in the browser. `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID` remains (unavoidable for WalletConnect), but should be monitored for abuse at the WalletConnect dashboard.
|
|
|
|
```javascript
|
|
// In compiled JS:
|
|
const ENV_SCHEDY_API_KEY = "<REDACTED — a real 64-hex key was hardcoded here; rotate it at Schedy>";
|
|
```
|
|
|
|
**Impact:**
|
|
- Attacker can call Schedy API directly to create/delete tasks
|
|
- Attacker can abuse WalletConnect project ID for rate limit exhaustion
|
|
- If nftcache key is set via `NEXT_PUBLIC_NFTCACHE_API_KEY`, same issue
|
|
|
|
**Remediation:**
|
|
- **Never** prefix backend-only secrets with `NEXT_PUBLIC_`
|
|
- For Schedy: proxy through a Next.js API route (`/api/schedule`) that holds the server-side secret
|
|
- For WalletConnect: this one is unavoidable for client-side connection, but monitor abuse and restrict origins at WalletConnect dashboard
|
|
|
|
---
|
|
|
|
### C4 — Sensitive Credentials Stored in localStorage 🔴 [FIXED]
|
|
**Location:** `mortgagefi-frontend/app/dapp/page.tsx`, `mortgagefi-frontend/components/SettingsModal.tsx`
|
|
**CVSS:** ~8.2 (Critical)
|
|
|
|
> [!danger] Critical (CVSS ~8.2) — Sensitive Credentials Stored in localStorage [FIXED]
|
|
> Sensitive data (Schedy API key, AWS credentials, notification tokens, RPC endpoints) was stored in **unencrypted browser localStorage**.
|
|
|
|
**Issue:** The following sensitive data was stored in **unencrypted browser localStorage**:
|
|
|
|
- `notif:settings` — contains Schedy API key, ntfy topic, Gotify token, AWS Access Key ID + Secret Access Key
|
|
- `notif:positions:v1:*` — position metadata linked to wallet
|
|
- `rpc:base`, `rpc:arbitrum`, `rpc:mainnet` — custom RPC endpoints
|
|
- `nftcache:apiKey` — API key for nftcache
|
|
|
|
**Impact:** Any XSS vulnerability (or malicious browser extension) can steal:
|
|
- AWS credentials with SNS publish permissions
|
|
- Schedy API key granting full task control
|
|
- Gotify/ntfy tokens enabling notification spam
|
|
- Custom RPC URLs enabling man-in-the-middle attacks
|
|
|
|
**Remediation:** ✅ Applied
|
|
- ✅ **AWS SNS provider removed entirely** from frontend (no more AWS keys in browser)
|
|
- ✅ **Schedy URL/API key fields removed** from settings UI
|
|
- ✅ **Security warning added** to SettingsModal: "Settings are stored locally in your browser. Do not use this on shared computers."
|
|
- ✅ **CSP implemented** via Next.js headers (see M4)
|
|
- Gotify token and ntfy topic remain in localStorage (lower sensitivity). Future enhancement: encrypt with user password or use sessionStorage.
|
|
|
|
---
|
|
|
|
### C5 — Address Canonicalization Bug Enables Ownership Spoofing 🔴 [FIXED]
|
|
**Location:** `nftcache/cmd/nftcache/main.go:68-76`, `nftcache/internal/fetcher/rpc.go:134-140`
|
|
**CVSS:** ~7.5 (High)
|
|
|
|
> [!danger] High (CVSS ~7.5) — Address Canonicalization Bug Enables Ownership Spoofing [FIXED]
|
|
> `canonAddr()` silently **truncated** addresses longer than 40 hex chars instead of rejecting them, enabling ownership spoofing.
|
|
|
|
**Issue:** `canonAddr()` silently **truncated** addresses longer than 40 hex chars instead of rejecting them:
|
|
|
|
```go
|
|
func canonAddr(s string) string {
|
|
x := strings.ToLower(strings.TrimSpace(s))
|
|
if strings.HasPrefix(x, "0x") { x = x[2:] }
|
|
if len(x) > 40 { x = x[len(x)-40:] } // BUG: truncates!
|
|
if len(x) < 40 { x = strings.Repeat("0", 40-len(x)) + x }
|
|
return "0x" + x
|
|
}
|
|
```
|
|
|
|
**Impact:**
|
|
```
|
|
Input: 0x0000000000000000000000000000000000000000deadbeef1234567890abcdef12345678
|
|
Output: 0xdeadbeef1234567890abcdef1234567890abcdef (completely different address!)
|
|
```
|
|
|
|
An attacker could claim tokens belonging to address `0xdeadbeef...` by providing a maliciously padded `user_wallet` parameter. The cache key and owner comparison would match the truncated version.
|
|
|
|
**Remediation:** ✅ Applied. `canonAddr()` now returns `(string, error)` and rejects any address that is not exactly `0x` + 40 lowercase hex characters. All call sites updated to handle the error.
|
|
|
|
---
|
|
|
|
### C6 — Denial of Service via Unauthenticated RPC Scanning 🔴 [FIXED]
|
|
**Location:** `nftcache/cmd/nftcache/main.go:78-196`
|
|
**CVSS:** ~7.5 (High)
|
|
|
|
> [!danger] High (CVSS ~7.5) — Denial of Service via Unauthenticated RPC Scanning [FIXED]
|
|
> The `/nfts` endpoint performed expensive on-chain scanning. When `NFTCACHE_API_KEY` was empty, the endpoint was **completely unauthenticated**.
|
|
|
|
**Issue:** The `/nfts` endpoint performed expensive on-chain scanning. When `NFTCACHE_API_KEY` was empty, the endpoint was **completely unauthenticated**.
|
|
|
|
**Impact:**
|
|
- Attacker can spam `GET /nfts?network=base&nft_contract=cbbtc&user_wallet=0x...` to exhaust RPC rate limits
|
|
- Each request triggers up to 1000 `eth_call` RPC requests (or more if config has higher `max_token_id`)
|
|
- Background refresh goroutines multiply the effect (stale cache triggers background scan)
|
|
- Can incur significant RPC provider costs and cause service degradation
|
|
|
|
**Proof of Concept:**
|
|
```bash
|
|
while true; do
|
|
curl "http://localhost:8090/nfts?network=base&nft_contract=cbbtc&user_wallet=0x$(openssl rand -hex 20)"
|
|
done
|
|
```
|
|
|
|
**Remediation:** ✅ Applied
|
|
1. ✅ **API key required at startup** — `nftcache` now calls `log.Fatal` if `NFTCACHE_API_KEY` is empty
|
|
2. ✅ **maxTokenId capped at 10,000** — prevents unbounded RPC scanning
|
|
3. ✅ Per-IP rate limiting implemented in new scheduler API (not yet in nftcache — acceptable since auth is now required)
|
|
4. Background refresh goroutines still present; future enhancement: deduplicate with `sync.SingleFlight`
|
|
|
|
---
|
|
|
|
### C7 — RPC URL Injection via localStorage 🔴 [FIXED]
|
|
**Location:** `mortgagefi-frontend/config/web3.ts:17-31`, `mortgagefi-frontend/components/SettingsModal.tsx:57-65`
|
|
**CVSS:** ~7.8 (High)
|
|
|
|
> [!danger] High (CVSS ~7.8) — RPC URL Injection via localStorage [FIXED]
|
|
> The frontend read RPC URLs from **localStorage** with zero validation, enabling MITM via attacker-controlled RPC endpoints.
|
|
|
|
**Issue:** The frontend read RPC URLs from **localStorage** with zero validation:
|
|
|
|
```typescript
|
|
function runtimeRpc(key: string): string | null {
|
|
try {
|
|
if (typeof window !== 'undefined' && window.localStorage) {
|
|
const v = window.localStorage.getItem(key);
|
|
return (v && v.trim()) ? v.trim() : null;
|
|
}
|
|
} catch {}
|
|
return null;
|
|
}
|
|
```
|
|
|
|
**Impact:**
|
|
- A malicious website with XSS, a malicious browser extension, or phishing attack can set `rpc:base` to an attacker-controlled RPC
|
|
- All subsequent blockchain reads (balances, allowances, debt data) go through the malicious RPC
|
|
- Attacker can return fake data (e.g., show zero debt, hide liquidation warnings) or phish transactions
|
|
- The Settings UI even provides a friendly form for users to enter arbitrary RPC URLs
|
|
|
|
**Remediation:** ✅ Applied. `config/web3.ts` now validates RPC URLs against an allowlist of known providers (LlamaRPC, Infura, Alchemy, MeowRPC). Untrusted RPCs from localStorage are rejected with a console warning.
|
|
|
|
---
|
|
|
|
## High Severity
|
|
|
|
### H1 — No HTTPS Enforcement Anywhere [FIXED]
|
|
**Location:** `docker-compose.yml`, all Go services, nginx config
|
|
**Impact:** All internal and external communication was plaintext HTTP.
|
|
|
|
> [!danger] High — No HTTPS Enforcement Anywhere [FIXED]
|
|
> All internal and external communication was plaintext HTTP.
|
|
|
|
**Remediation:** ✅ nginx config and Next.js headers updated. In production, terminate TLS at nginx with a real certificate (Let's Encrypt) and add HSTS. Internal Docker network communication is isolated.
|
|
|
|
**Remediation:**
|
|
- Terminate TLS at nginx with a real certificate (Let's Encrypt)
|
|
- Use `https://` for all external service URLs
|
|
- Add HSTS header: `Strict-Transport-Security: max-age=31536000; includeSubDomains`
|
|
|
|
---
|
|
|
|
### H2 — Missing Security Headers in Nginx [FIXED]
|
|
**Location:** `nginx/nginx.conf`
|
|
**Impact:** No CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy.
|
|
|
|
> [!danger] High — Missing Security Headers in Nginx [FIXED]
|
|
> No CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy.
|
|
|
|
**Remediation:** ✅ Added to `nginx/nginx.conf`: `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, `X-XSS-Protection: 1; mode=block`. Also added CSP and same headers via `next.config.ts` for Vercel deployments.
|
|
|
|
**Remediation:** Add to nginx:
|
|
```nginx
|
|
add_header X-Frame-Options "DENY" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; connect-src 'self' https://*.walletconnect.com https://*.llamarpc.com; img-src 'self' data:; style-src 'self' 'unsafe-inline';" always;
|
|
```
|
|
|
|
---
|
|
|
|
### H3 — Direct Container Ports Exposed to Host [FIXED]
|
|
**Location:** `docker-compose.yml`
|
|
**Impact:** ntfy, schedy, and nftcache were exposed directly on the Docker host, bypassing nginx.
|
|
|
|
> [!danger] High — Direct Container Ports Exposed to Host [FIXED]
|
|
> ntfy, schedy, and nftcache were exposed directly on the Docker host, bypassing nginx.
|
|
|
|
**Remediation:** ✅ All `ports:` blocks removed from backend services. Only nginx exposes port 80. Internal services communicate via Docker network.
|
|
|
|
**Remediation:** Remove `ports:` from all services except nginx:
|
|
```yaml
|
|
# ntfy, schedy, nftcache: remove these blocks:
|
|
# ports:
|
|
# - "8081:80"
|
|
```
|
|
Only nginx should be reachable from outside.
|
|
|
|
---
|
|
|
|
### H4 — Schedy Task Payload Can Be Used for Header Injection [FIXED]
|
|
**Location:** `mortgagefi-frontend/submodules/schedy/internal/executor/executor.go` (removed)
|
|
**Impact:** Custom headers from task payloads were set without validation.
|
|
|
|
> [!danger] High — Schedy Task Payload Can Be Used for Header Injection [FIXED]
|
|
> Custom headers from task payloads were set without validation.
|
|
|
|
**Remediation:** ✅ Schedy removed. New executor in `app/api/cron/route.ts` uses `sanitizeHeaders()` which blocks `Host`, `Content-Length`, `Transfer-Encoding`, `Connection`, `Upgrade`, and `Proxy-Authorization`.
|
|
|
|
**Remediation:** Blocklist dangerous headers:
|
|
```go
|
|
blockedHeaders := map[string]bool{
|
|
"host": true, "content-length": true, "transfer-encoding": true,
|
|
"connection": true, "upgrade": true, "proxy-authorization": true,
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### H5 — No Request Body Size Limits [PARTIALLY FIXED]
|
|
**Location:** All Go HTTP handlers
|
|
**Impact:** Schedy and nftcache accepted arbitrarily large JSON bodies.
|
|
|
|
> [!danger] High — No Request Body Size Limits [PARTIALLY FIXED]
|
|
> Schedy and nftcache accepted arbitrarily large JSON bodies.
|
|
|
|
**Remediation:** ✅ Schedy removed (no longer applicable). nftcache still lacks body size limits; future enhancement: add `http.MaxBytesReader(w, r.Body, 1<<20)` to nftcache handlers.
|
|
|
|
**Remediation:**
|
|
```go
|
|
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1MB limit
|
|
```
|
|
|
|
---
|
|
|
|
### H6 — Docker Images from External Registry Without Verification [PARTIALLY FIXED]
|
|
**Location:** `docker-compose.yml`
|
|
**Impact:** `latest` tag was mutable; no image digest pinning.
|
|
|
|
> [!danger] High — Docker Images from External Registry Without Verification [PARTIALLY FIXED]
|
|
> `latest` tag was mutable; no image digest pinning.
|
|
|
|
**Remediation:** ✅ Schedy image removed. `mortgagefi-frontend` image still uses `:alert` tag. **Action required:** Pin to SHA256 digest:
|
|
```yaml
|
|
image: git.manko.yoga/manawenuz/mortgagefi-frontend@sha256:abc123...
|
|
```
|
|
|
|
**Remediation:** Pin to digest:
|
|
```yaml
|
|
image: git.manko.yoga/manawenuz/schedy@sha256:abc123...
|
|
```
|
|
|
|
---
|
|
|
|
### H7 — Unbounded Concurrent Background Refreshes in nftcache [OPEN]
|
|
**Location:** `nftcache/cmd/nftcache/main.go:133-151`
|
|
**Impact:** Each stale cache hit spawns a goroutine for background refresh. Under load, this creates an unbounded number of goroutines.
|
|
|
|
> [!danger] High — Unbounded Concurrent Background Refreshes in nftcache [OPEN]
|
|
> Each stale cache hit spawns a goroutine for background refresh. Under load, this creates an unbounded number of goroutines.
|
|
> **Status:** Not yet fixed.
|
|
|
|
**Status:** Not yet fixed. Future enhancement: use `sync.SingleFlight` or a worker pool to deduplicate refreshes for the same contract key.
|
|
|
|
**Remediation:** Use a `sync.SingleFlight` or worker pool to deduplicate refreshes for the same contract key.
|
|
|
|
---
|
|
|
|
### H8 — AWS Credentials Stored in Browser (SNS Provider) [FIXED]
|
|
**Location:** `mortgagefi-frontend/components/SettingsModal.tsx`
|
|
**Impact:** The UI included a form for AWS credentials stored in localStorage.
|
|
|
|
> [!danger] High — AWS Credentials Stored in Browser (SNS Provider) [FIXED]
|
|
> The UI included a form for AWS credentials stored in localStorage.
|
|
|
|
**Remediation:** ✅ SNS provider removed entirely from frontend and types. Users should use ntfy or Gotify for notifications instead.
|
|
|
|
**Remediation:** Remove SNS provider from frontend entirely, or proxy through a server-side endpoint.
|
|
|
|
---
|
|
|
|
### H9 — `secondsTillLiq` Trusts On-Chain Value Without Bounds Check [FIXED]
|
|
**Location:** `mortgagefi-frontend/app/dapp/page.tsx`
|
|
**Impact:** The auto-rescheduler computed `runAt1 = liqAt - offset` without validating bounds.
|
|
|
|
> [!danger] High — `secondsTillLiq` Trusts On-Chain Value Without Bounds Check [FIXED]
|
|
> The auto-rescheduler computed `runAt1 = liqAt - offset` without validating bounds.
|
|
|
|
**Remediation:** ✅ Added `MAX_SECONDS = 86400 * 365 * 5` (5 years). Values outside `0 < seconds <= MAX_SECONDS` are rejected.
|
|
|
|
**Remediation:**
|
|
```typescript
|
|
const MAX_SECONDS = 86400 * 365 * 5; // 5 years max
|
|
if (!Number.isFinite(seconds) || seconds <= 0 || seconds > MAX_SECONDS) {
|
|
throw new Error('Liquidation timer out of bounds');
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### H10 — Compound Action Uses Hardcoded Function Selector Without Validation [FIXED]
|
|
**Location:** `mortgagefi-frontend/app/dapp/page.tsx`
|
|
**Impact:** `sendTransactionAsync` was used with a hardcoded selector, bypassing ABI validation.
|
|
|
|
> [!danger] High — Compound Action Uses Hardcoded Function Selector Without Validation [FIXED]
|
|
> `sendTransactionAsync` was used with a hardcoded selector, bypassing ABI validation.
|
|
|
|
**Remediation:** ✅ Replaced `sendTransactionAsync` with `writeContractAsync` using the debt ABI. Added validation that `debtAddress` matches a known preset before allowing the transaction. `useSendTransaction` import removed.
|
|
|
|
**Remediation:** Use `writeContractAsync` with the ABI:
|
|
```typescript
|
|
await writeContractAsync({
|
|
abi: debtAbi,
|
|
address: debtAddress,
|
|
functionName: 'compound',
|
|
chainId: selectedChainId,
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
### H11 — `useSendTransaction` Available but Potentially Dangerous [FIXED]
|
|
**Location:** `mortgagefi-frontend/app/dapp/page.tsx`
|
|
**Impact:** `useSendTransaction` was imported and available.
|
|
|
|
> [!danger] High — `useSendTransaction` Available but Potentially Dangerous [FIXED]
|
|
> `useSendTransaction` was imported and available.
|
|
|
|
**Remediation:** ✅ `useSendTransaction` import removed from page.tsx. All transactions now go through `writeContractAsync` with ABI validation.
|
|
|
|
**Remediation:** Remove the import if not strictly needed, or wrap with strict validation.
|
|
|
|
---
|
|
|
|
## Medium Severity
|
|
|
|
### M1 — Contract Address Not Validated Before RPC Call
|
|
**Location:** `nftcache/internal/fetcher/rpc.go:186-210`
|
|
**Impact:** `makeRPCCall` inserts the `contract` parameter directly into JSON RPC payload without validation. A malformed address could cause RPC errors or, in the worst case, exploit a vulnerable JSON-RPC parser.
|
|
|
|
> [!warning] Medium — Contract Address Not Validated Before RPC Call
|
|
> `makeRPCCall` inserts the `contract` parameter directly into JSON RPC payload without validation. A malformed address could cause RPC errors or, in the worst case, exploit a vulnerable JSON-RPC parser.
|
|
|
|
**Remediation:** Validate contract address with regex `^0x[a-f0-9]{40}$` before use.
|
|
|
|
---
|
|
|
|
### M2 — YAML Config Has Duplicate Key
|
|
**Location:** `config/contracts.yaml`
|
|
**Impact:**
|
|
```yaml
|
|
contracts:
|
|
cbbtc:
|
|
network: base
|
|
address: "0x0987654321098765432109876543210987654321"
|
|
cbbtc: # DUPLICATE!
|
|
network: base
|
|
address: "0xab825f45e9e5d2459fb7a1527a8d0ca082c582f4"
|
|
```
|
|
YAML parsers may silently ignore the first or second entry. This could cause the wrong contract to be used.
|
|
|
|
> [!warning] Medium — YAML Config Has Duplicate Key
|
|
> A duplicate `cbbtc` key in `config/contracts.yaml`. YAML parsers may silently ignore one entry, causing the wrong contract to be used.
|
|
|
|
**Remediation:** Remove duplicate; use unique slugs (e.g., `cbbtc-v1`, `cbbtc-v2`).
|
|
|
|
---
|
|
|
|
### M3 — CORS Origin Reflection Subdomain Bypass
|
|
**Location:** `nftcache/cmd/nftcache/main.go:32-55`, `schedy/cmd/schedy/main.go:28-59`
|
|
**Impact:** The CORS middleware does exact string match on Origin. An attacker with a subdomain like `evil.mortgagefi.app` could not bypass it, but the code uses `strings.EqualFold` which is correct. However, if `CORS_ALLOW_ORIGIN` is accidentally set to `*` or left empty, CORS is completely disabled.
|
|
|
|
> [!warning] Medium — CORS Origin Reflection Subdomain Bypass
|
|
> The CORS middleware uses correct exact-match logic, but if `CORS_ALLOW_ORIGIN` is accidentally set to `*` or left empty, CORS is completely disabled.
|
|
|
|
**Remediation:** Fail startup if `CORS_ALLOW_ORIGIN` is empty or `*` in production.
|
|
|
|
---
|
|
|
|
### M4 — No Content Security Policy (CSP)
|
|
**Location:** Frontend (global)
|
|
**Impact:** Without CSP, XSS attacks have full capability to execute scripts, connect to arbitrary domains, and exfiltrate data.
|
|
|
|
> [!warning] Medium — No Content Security Policy (CSP)
|
|
> Without CSP, XSS attacks have full capability to execute scripts, connect to arbitrary domains, and exfiltrate data.
|
|
|
|
**Remediation:** Add CSP meta tag or header. Start with report-only:
|
|
```html
|
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; connect-src 'self' https://*.walletconnect.com wss://*.walletconnect.org https://*.llamarpc.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:;">
|
|
```
|
|
|
|
---
|
|
|
|
### M5 — Test/Debug Endpoints and Logging in Production
|
|
**Location:** `nftcache/internal/fetcher/rpc.go:95-102`
|
|
**Impact:**
|
|
```go
|
|
// Always log token 103 regardless of debug mode
|
|
if i == 103 {
|
|
fmt.Printf("ALWAYS: Token 103 owner: %s (canonical: %s)\n", tokenOwner, canonicalOwner)
|
|
}
|
|
```
|
|
Hardcoded debug logging leaks potentially sensitive ownership data in production logs.
|
|
|
|
> [!warning] Medium — Test/Debug Endpoints and Logging in Production
|
|
> Hardcoded debug logging (token 103) leaks potentially sensitive ownership data in production logs.
|
|
|
|
**Remediation:** Remove hardcoded debug statements; use structured logging with level control.
|
|
|
|
---
|
|
|
|
### M6 — Auto-Reschedule Race Condition
|
|
**Location:** `mortgagefi-frontend/app/dapp/page.tsx:948-974`
|
|
**Impact:** The auto-reschedule `useEffect` fires an async IIFE that loops over all positions and cancels/re-creates schedules. With rapid re-renders (e.g., user rapidly switching chains), multiple overlapping executions can occur, leading to:
|
|
- Duplicate scheduled jobs
|
|
- Jobs created and immediately canceled
|
|
- Rate limit exhaustion against Schedy API
|
|
|
|
> [!warning] Medium — Auto-Reschedule Race Condition
|
|
> The auto-reschedule `useEffect` can run overlapping async executions on rapid re-renders, causing duplicate jobs, jobs created and immediately canceled, and rate limit exhaustion.
|
|
|
|
**Remediation:** Add an `isScheduling` ref/mutex to prevent concurrent executions:
|
|
```typescript
|
|
const isScheduling = useRef(false);
|
|
useEffect(() => {
|
|
if (isScheduling.current) return;
|
|
isScheduling.current = true;
|
|
(async () => { ... })().finally(() => { isScheduling.current = false; });
|
|
}, [...]);
|
|
```
|
|
|
|
---
|
|
|
|
### M7 — Missing Input Sanitization in Notification Message Builder
|
|
**Location:** `mortgagefi-frontend/app/dapp/page.tsx:838`
|
|
**Impact:** The notification message includes `positionUrl` and `collateralStr` derived from on-chain data. While unlikely, if a token symbol or contract returned malicious Unicode (e.g., RTL override characters), the message could be confusing or deceptive.
|
|
|
|
> [!warning] Medium — Missing Input Sanitization in Notification Message Builder
|
|
> Notification messages include on-chain-derived strings; malicious Unicode (e.g., RTL override characters) could make messages confusing or deceptive.
|
|
|
|
**Remediation:** Sanitize all user-visible strings before building messages.
|
|
|
|
---
|
|
|
|
### M8 — No Backup/Recovery for BadgerDB
|
|
**Location:** `data/nftcache`, `data/schedy`, `data/ntfy`
|
|
**Impact:** If the host disk fails, all scheduled tasks and NFT cache data is lost. No backup strategy is documented.
|
|
|
|
> [!warning] Medium — No Backup/Recovery for BadgerDB
|
|
> If the host disk fails, all scheduled tasks and NFT cache data is lost. No backup strategy is documented.
|
|
|
|
**Remediation:** Mount volumes from a persistent block store; schedule periodic backups.
|
|
|
|
---
|
|
|
|
### M9 — ntfy Uses Default/Weak User ID
|
|
**Location:** `docker-compose.yml:27`
|
|
**Impact:** `user: "4242:4242"` is a fixed UID/GID. If the host has a user with this ID, container files could be accessible outside the container.
|
|
|
|
> [!warning] Medium — ntfy Uses Default/Weak User ID
|
|
> `user: "4242:4242"` is a fixed UID/GID. If the host has a user with this ID, container files could be accessible outside the container.
|
|
|
|
**Remediation:** Use a randomly generated high UID (e.g., `65534:nogroup`).
|
|
|
|
---
|
|
|
|
### M10 — Frontend Lacks Integrity Checks for Submodules
|
|
**Location:** `.gitmodules`
|
|
**Impact:** The `schedy` submodule is not pinned to a specific commit hash in documentation. A compromised submodule repository could inject malicious code.
|
|
|
|
> [!warning] Medium — Frontend Lacks Integrity Checks for Submodules
|
|
> The `schedy` submodule is not pinned to a specific commit hash in documentation. A compromised submodule repository could inject malicious code.
|
|
|
|
**Remediation:** Pin submodule to a verified commit hash and verify in CI.
|
|
|
|
---
|
|
|
|
### M11 — `NFTCACHE_TTL` Default is 10 Minutes in `.env.local`
|
|
**Location:** `.env.local`
|
|
**Impact:** The default TTL is `10m`, causing very frequent background refreshes and unnecessary RPC load.
|
|
|
|
> [!warning] Medium — `NFTCACHE_TTL` Default is 10 Minutes in `.env.local`
|
|
> The default TTL is `10m`, causing very frequent background refreshes and unnecessary RPC load.
|
|
|
|
**Remediation:** Set to `24h` or longer; make it configurable per contract.
|
|
|
|
---
|
|
|
|
### M12 — Schedy DeleteTask Performance Issue
|
|
**Location:** `mortgagefi-frontend/submodules/schedy/internal/api/handler.go:110-142`
|
|
**Impact:** Deleting a task requires a **full table scan** (`ListTasks()`) to find the timestamp. With thousands of tasks, this is O(n) and blocks other operations.
|
|
|
|
> [!warning] Medium — Schedy DeleteTask Performance Issue
|
|
> Deleting a task requires a **full table scan** (`ListTasks()`) to find the timestamp. With thousands of tasks, this is O(n) and blocks other operations.
|
|
|
|
**Remediation:** Add a secondary index or use a key-only lookup.
|
|
|
|
---
|
|
|
|
## Low Severity / Informational
|
|
|
|
### L1 — `handleApprove` Approves Exact User Input, Not Max
|
|
**Location:** `mortgagefi-frontend/app/dapp/page.tsx:708-725`
|
|
|
|
> [!info] Low — `handleApprove` Approves Exact User Input, Not Max
|
|
> The approve function approves exactly the amount the user typed. This is UX-safe (no unlimited approvals) but requires two transactions for each new amount.
|
|
|
|
**Note:** The approve function approves exactly the amount the user typed. This is UX-safe (no unlimited approvals) but requires two transactions for each new amount.
|
|
|
|
### L2 — `enableMainnet` Feature Flag Could Be Accidentally Enabled
|
|
**Location:** `mortgagefi-frontend/app/dapp/page.tsx:24`
|
|
|
|
> [!info] Low — `enableMainnet` Feature Flag Could Be Accidentally Enabled
|
|
> Mainnet support is gated by an env var. If accidentally set to `true`, dummy placeholder addresses (`0x000...0001`) would be used, which is safer than real ones.
|
|
|
|
**Note:** Mainnet support is gated by an env var. If accidentally set to `true`, dummy placeholder addresses (`0x000...0001`) would be used, which is safer than real ones.
|
|
|
|
### L3 — `NFTCACHE_CONFIG` Path Not Validated
|
|
**Location:** `nftcache/cmd/nftcache/main.go:263-264`
|
|
|
|
> [!info] Low — `NFTCACHE_CONFIG` Path Not Validated
|
|
> If `NFTCACHE_CONFIG` is set to an arbitrary path, the service reads it. With `ro` mount this is mitigated, but direct file path injection is theoretically possible.
|
|
|
|
**Note:** If `NFTCACHE_CONFIG` is set to an arbitrary path, the service reads it. With `ro` mount this is mitigated, but direct file path injection is theoretically possible.
|
|
|
|
### L4 — No Health Check Endpoints
|
|
**Location:** All services
|
|
|
|
> [!info] Low — No Health Check Endpoints
|
|
> No `/health` or `/ready` endpoints for Docker/Kubernetes health checks.
|
|
|
|
**Note:** No `/health` or `/ready` endpoints for Docker/Kubernetes health checks.
|
|
|
|
### L5 — Verbose Console Logging in Production
|
|
**Location:** `mortgagefi-frontend/app/dapp/page.tsx` (throughout)
|
|
|
|
> [!info] Low — Verbose Console Logging in Production
|
|
> Extensive `console.log` statements leak internal state (cache keys, wallet addresses, token IDs) to browser DevTools.
|
|
|
|
**Note:** Extensive `console.log` statements leak internal state (cache keys, wallet addresses, token IDs) to browser DevTools.
|
|
|
|
---
|
|
|
|
## Remediation Priority Matrix
|
|
|
|
| Priority | Issue | Effort | Impact |
|
|
|----------|-------|--------|--------|
|
|
| **P0** | C2 — Rotate secrets & purge from Git | 1h | Critical |
|
|
| **P0** | C3 — Remove NEXT_PUBLIC_ from secrets | 2h | Critical |
|
|
| **P0** | C1 — Add SSRF protection to Schedy | 4h | Critical |
|
|
| **P1** | C4 — Stop storing credentials in localStorage | 1d | High |
|
|
| **P1** | C5 — Fix address canonicalization | 2h | High |
|
|
| **P1** | C6 — Require API auth on nftcache | 2h | High |
|
|
| **P1** | C7 — Validate RPC URLs | 2h | High |
|
|
| **P1** | H3 — Close direct container ports | 30m | High |
|
|
| **P2** | H1 — Add HTTPS/TLS | 4h | High |
|
|
| **P2** | H2 — Add security headers | 1h | Medium |
|
|
| **P2** | H10 — Fix compound transaction | 1h | Medium |
|
|
| **P3** | M4 — Implement CSP | 4h | Medium |
|
|
| **P3** | H6 — Pin Docker image digests | 1h | Medium |
|
|
| **P3** | M6 — Fix auto-reschedule race | 2h | Medium |
|
|
|
|
---
|
|
|
|
## Verification Checklist (Post-Remediation)
|
|
|
|
- [ ] `.env` and `.env.local` are in `.gitignore` and purged from Git history
|
|
- [ ] No `NEXT_PUBLIC_` variables contain secrets
|
|
- [ ] Schedy rejects URLs with private IPs, loopback, and internal hostnames
|
|
- [ ] nftcache requires `X-API-Key` and rejects requests without it
|
|
- [ ] `canonAddr` rejects invalid/too-long/too-short addresses
|
|
- [ ] Only nginx port (80/443) is exposed in Docker Compose
|
|
- [ ] HTTPS is enforced with valid certificates
|
|
- [ ] Security headers (CSP, HSTS, X-Frame-Options) are present
|
|
- [ ] AWS credentials removed from frontend entirely
|
|
- [ ] RPC URLs from localStorage are validated against an allowlist
|
|
- [ ] All hardcoded secrets rotated and replaced with environment injection
|
|
- [ ] Docker images pinned to SHA256 digests
|
|
- [ ] Rate limiting implemented on all public endpoints
|
|
|
|
---
|
|
|
|
## Related
|
|
|
|
- [[Home]]
|
|
- [[Architecture]]
|
|
- [[Performance Audit]]
|
|
- [[Project Audit 2026-06]]
|