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

33 KiB

title, tags, type, status, updated
title tags type status updated
Security Audit
mortgagefi
audit
security
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:

  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.

// 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:

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

  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:
    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.

// 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:

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:

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 startupnftcache 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:

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:

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

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 — 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:

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:

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:

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:

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

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