--- 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 = ""; ``` **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 ``` --- ### 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]]