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>
33 KiB
title, tags, type, status, updated
| title | tags | type | status | updated | |||
|---|---|---|---|---|---|---|---|
| Security Audit |
|
audit | reference | 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:
- ✅ Schedy removed — Replaced with Vercel-native Next.js API routes (
/api/tasks,/api/cron) + Redis - ✅ SSRF eliminated — New scheduler validates URLs against private IP / internal hostname blocklist
- ✅ Secrets removed from client bundle — No more
NEXT_PUBLIC_API keys; backend uses server-side env only - ✅ Sensitive credentials removed from localStorage — AWS SNS provider removed; Schedy fields removed; security warnings added
- ✅ nftcache hardened — API key now required at startup; address validation rejects malformed/truncated addresses; maxTokenId capped at 10,000
- ✅ RPC injection prevented — Frontend validates RPC URLs against an allowlist
- ✅ Network exposure reduced — Direct container ports removed; only nginx exposed
- ✅ Security headers added — CSP, HSTS, X-Frame-Options, X-Content-Type-Options in nginx and Next.js
- ✅
.env.examplecreated and.gitignoreupdated
Executive Summary (Original)
The original codebase contained 7 critical, 11 high, and 15 medium/low severity issues. The most severe were:
- Server-Side Request Forgery (SSRF) in Schedy allowing internal network probing
- Secrets committed to version control including SMTP passwords, API keys, and WalletConnect credentials
- API keys bundled into client-side JavaScript via
NEXT_PUBLIC_variables - Sensitive credentials stored in browser localStorage without encryption (AWS keys, schedy API key, notification tokens)
- Denial-of-Service via unauthenticated RPC scanning in nftcache
- 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.
// 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 endpointshttp://localhost:8090/nfts/invalidate— nftcache internal APIshttp://localhost:80/ntfy— ntfy admin endpoints- Internal Docker network services (
http://frontend:3000,http://nftcache:8090)
Proof of Concept:
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:
// 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
- ✅ Created
.env.examplewith dummy values only - ✅ Added
.gitignoreat project root:.env .env.local .env.*.local - Action required by user: Rotate all secrets and purge from Git history:
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_KEYwas 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.
// 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 Keynotif:positions:v1:*— position metadata linked to walletrpc:base,rpc:arbitrum,rpc:mainnet— custom RPC endpointsnftcache: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:
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
/nftsendpoint performed expensive on-chain scanning. WhenNFTCACHE_API_KEYwas 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_callRPC requests (or more if config has highermax_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:
while true; do
curl "http://localhost:8090/nfts?network=base&nft_contract=cbbtc&user_wallet=0x$(openssl rand -hex 20)"
done
Remediation: ✅ Applied
- ✅ API key required at startup —
nftcachenow callslog.FatalifNFTCACHE_API_KEYis empty - ✅ maxTokenId capped at 10,000 — prevents unbounded RPC scanning
- ✅ Per-IP rate limiting implemented in new scheduler API (not yet in nftcache — acceptable since auth is now required)
- 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:
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:baseto 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:
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:
# 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:
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:
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]
latesttag was mutable; no image digest pinning.
Remediation: ✅ Schedy image removed. mortgagefi-frontend image still uses :alert tag. Action required: Pin to SHA256 digest:
image: git.manko.yoga/manawenuz/mortgagefi-frontend@sha256:abc123...
Remediation: Pin to digest:
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 —
secondsTillLiqTrusts On-Chain Value Without Bounds Check [FIXED] The auto-rescheduler computedrunAt1 = liqAt - offsetwithout validating bounds.
Remediation: ✅ Added MAX_SECONDS = 86400 * 365 * 5 (5 years). Values outside 0 < seconds <= MAX_SECONDS are rejected.
Remediation:
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]
sendTransactionAsyncwas 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:
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 —
useSendTransactionAvailable but Potentially Dangerous [FIXED]useSendTransactionwas 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
makeRPCCallinserts thecontractparameter 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:
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
cbbtckey inconfig/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_ORIGINis 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:
<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:
// 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
useEffectcan 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:
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
schedysubmodule 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_TTLDefault is 10 Minutes in.env.localThe default TTL is10m, 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 —
handleApproveApproves 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 —
enableMainnetFeature Flag Could Be Accidentally Enabled Mainnet support is gated by an env var. If accidentally set totrue, 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_CONFIGPath Not Validated IfNFTCACHE_CONFIGis set to an arbitrary path, the service reads it. Withromount 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
/healthor/readyendpoints 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.logstatements 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)
.envand.env.localare in.gitignoreand purged from Git history- No
NEXT_PUBLIC_variables contain secrets - Schedy rejects URLs with private IPs, loopback, and internal hostnames
- nftcache requires
X-API-Keyand rejects requests without it canonAddrrejects 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