Files
mortgagefi-helper/docs/Audits/Security Audit.md
Siavash Sameni 6ae581ab2e feat(ui): Ghibli/Miyazaki reskin + Obsidian docs vault + project audit
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>
2026-06-14 08:13:53 +04:00

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]]