← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
CVE-2026-28785 · CWE-89 · Disclosed 2026-03-06

Ghostfolio is an open source wealth management software

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
01 · The Real Story

This is a sharp knife left in a side drawer, not a grenade in the lobby

CVE-2026-28785 is a SQL injection in Ghostfolio's manual asset import flow. In versions before 2.244.0, an attacker can bypass symbol validation and hit the getHistorical() path to run arbitrary SQL against the app database, which can expose or alter portfolio and financial records across users.

The vendor's CRITICAL 9.8 score is technically understandable for raw impact, but it overstates typical enterprise risk. The advisory's own workaround says to disable public user signup so only trusted users can reach the manual import feature; that is a big clue that real exploitation often depends on account creation or existing access, and Ghostfolio itself is a niche self-hosted app with a much smaller exposed population than mainstream edge software.

"Serious if exposed, but this is not a mass-enterprise internet fire like the 9.8 suggests."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Find an exposed Ghostfolio instance

An attacker first needs a reachable Ghostfolio deployment, typically a self-hosted web app on its default port or behind a reverse proxy. Common recon tooling would be Shodan/Censys or plain HTTP fingerprinting with curl/httpx against internet-facing app inventories.
Conditions required:
  • Ghostfolio is deployed and network-reachable
  • The instance is externally exposed or reachable from the attacker's network position
Where this breaks in practice:
  • Many Ghostfolio deployments are hobbyist or internal-only, not enterprise edge infrastructure
  • Reverse proxies, VPN gates, IP allowlists, or private hosting remove the unauthenticated internet path entirely
Detection/coverage: External attack-surface management can usually flag the service; generic vuln scanners may identify Ghostfolio but often will not safely validate this specific issue without credentials or crafted app flow coverage.
STEP 02

Get access to the manual import feature

The practical next step is reaching the manual asset import workflow that exercises getHistorical(). The GitHub advisory's workaround explicitly recommends disabling public signup, which strongly implies attackers often need either open registration or a preexisting account to reach the vulnerable function; tooling here is just the browser, Burp Suite, or scripted HTTP clients.
Conditions required:
  • Public signup is enabled, or the attacker already has a valid account
  • The import feature is exposed to that user role
Where this breaks in practice:
  • If signup is disabled and accounts are tightly controlled, the bug becomes post-auth or insider-only
  • SSO, invite-only onboarding, and tenant admin review sharply reduce reachable population
Detection/coverage: Registration spikes, odd login creation patterns, and access to import-related endpoints should be visible in app or reverse-proxy logs if you retain them.
STEP 03

Exploit blind SQLi through getHistorical()

With access to the vulnerable flow, the attacker can use Burp Repeater, a custom curl request, or eventually sqlmap-style automation to inject crafted symbol data and trigger time-based blind SQL queries. The advisory describes a symbol-validation bypass, so the exploit path is likely straightforward once the correct request shape is known.
Conditions required:
  • The target runs a version earlier than 2.244.0
  • The attacker can send crafted input into the import/history lookup path
Where this breaks in practice:
  • Time-based blind SQLi is slower and noisier than direct data-return SQLi
  • WAFs, aggressive request-rate controls, and database query timeouts can make extraction painful
Detection/coverage: WAF signatures may catch commodity payloads, but app-aware manual probes can slip by. Database slow-query logs and repeated unusual request timing against import/history endpoints are better detection points.
STEP 04

Read or alter Ghostfolio data

Successful exploitation lets the attacker read, modify, or delete the Ghostfolio database content available to the application's DB account. Weaponized follow-on tooling is standard SQL client behavior; the real impact is compromise of financial records in that application, not domain-wide takeover by itself.
Conditions required:
  • The Ghostfolio database user has meaningful read/write privileges
  • The attacker can complete enough blind extraction or modify targeted records
Where this breaks in practice:
  • Blast radius is usually bounded to the Ghostfolio app and its database permissions
  • If DB credentials are least-privileged and backups are solid, destructive impact is contained
Detection/coverage: Database activity monitoring, audit logs, backup-integrity checks, and unexpected changes to portfolio or transaction records are your best post-exploit signals.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusNo confirmed active exploitation found in authoritative sources reviewed. CISA ADP enrichment marks exploitation as none, and the CVE is not in the KEV catalog.
Proof-of-concept availabilityNo public PoC repo or Metasploit module located during review. That lowers operational risk today, though SQLi is usually reproducible once request structure is understood.
EPSS0.00078 provided in your intel, which is extremely low modeled near-term exploitation probability. Third-party mirrors place it around the low ~20th percentile range; treat that percentile as indicative, not authoritative.
KEV statusNot KEV-listed as of 2026-05-09 review time.
CVSS vector reality checkVendor/NVD score it 9.8 / CRITICAL with AV:N/AC:L/PR:N/UI:N. That reflects *theoretical* remote impact, but the workaround about disabling public signup is a real-world friction point that the base vector does not capture.
Affected versionsAll Ghostfolio versions earlier than 2.244.0 are listed as affected in GHSA/NVD/OSV.
Fixed versionPatch is in 2.244.0. I found no distro backport advisories for packaged Linux distributions, which is unsurprising for a niche self-hosted app.
Exposure populationThis is not Exchange/Confluence/SharePoint scale. Ghostfolio's public metrics show roughly 2.27M Docker pulls and about 8k GitHub stars, which says *popular in self-hosting circles* but still a comparatively narrow enterprise footprint.
Scanning and discovery dataNo authoritative Shodan/Censys count was found during this review. Given Ghostfolio's common self-hosted deployment model and default port documentation, exposed instances likely exist, but the reachable population appears limited.
Disclosure and creditDisclosed 2026-03-06. GitHub advisory credits ratrarity as reporter and dtslvr as remediation developer.
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to HIGH (7.3/10)

The decisive factor is reachability friction: in practice this appears tied to the manual asset import workflow, and the vendor's own workaround points to public signup as a prerequisite amplifier. That means many deployments collapse from 'unauthenticated internet RCE-class urgency' to 'internet-exposed app plus open registration or existing account,' which is still serious but not truly CRITICAL at enterprise scale.

HIGH Affected version range and fix version
MEDIUM Practical exploit preconditions around signup/auth and feature exposure
HIGH No current evidence of KEV listing or active exploitation

Why this verdict

  • Down from 9.8 because exposure is narrower than CVSS implies: the vulnerable path is in *manual asset import*, not a generic pre-auth homepage endpoint every anonymous user naturally hits.
  • Down again because attacker position is often stronger than PR:N suggests: the GHSA workaround says disable *public signup*, which implies many practical exploit chains require open registration or an existing account rather than pure drive-by internet reach.
  • Down again because population is small: Ghostfolio is a niche self-hosted wealth app, not a broadly deployed enterprise edge platform. That sharply limits both victim pool and wormable potential.
  • Held at HIGH because impact inside the app is still ugly: if the feature is reachable, SQLi against the Ghostfolio DB can expose or change sensitive financial data for all users in that instance.
  • Held at HIGH because exploitation mechanics are not exotic: once request structure is understood, SQLi is commodity attacker tradecraft, and time-based blind extraction is still workable.

Why not higher?

I did not keep this at CRITICAL because the real-world chain appears to need more than 'internet packet hits login page, box falls over.' The likely need for public signup or some authenticated path, combined with a niche deployment base and no active exploitation evidence, creates meaningful downward pressure.

Why not lower?

I did not drop this to MEDIUM because the vulnerable feature can still lead to full compromise of the application's core data store. For exposed instances with signup enabled or weak account controls, exploitation should be low-complexity and the confidentiality/integrity hit is real.

05 · Compensating Control

What to do — in priority order.

  1. Disable public signup — Do this within 30 days as the first containment move because the vendor explicitly calls it out as the workaround. If only trusted admins can create accounts, you remove the cleanest remote path into the vulnerable import flow.
  2. Pull Ghostfolio behind identity-aware access — Place the app behind VPN, SSO gateway, reverse-proxy auth, or IP allowlists within 30 days. This changes the attacker position from anonymous internet to authenticated network user and materially cuts reachable population.
  3. Block or monitor import/history endpoints — Add temporary WAF or reverse-proxy rules for the manual asset import and related historical lookup paths within 30 days. Focus on SQLi signatures, repeated delayed-response probes, and abusive request rates to slow blind extraction.
  4. Tighten DB privileges — Review the Ghostfolio application's database account within 30 days and ensure it has only the minimum rights needed. Least privilege does not prevent exploitation, but it does cap how much data an attacker can read or destroy.
  5. Preserve clean backups — Validate recent restorable database backups within 30 days. This matters because the stated impact includes record modification and deletion, and recovery posture decides whether an incident is annoying or business-disruptive.
What doesn't work
  • Relying on EDR/AV on the host does not stop SQLi moving through normal web and DB processes.
  • Assuming 'it has a login page' is enough does not help if public signup is enabled or low-trust users can reach the import feature.
  • Running only a network vuln scan is not enough; many scanners will miss app-flow SQLi hidden behind registration or feature-specific workflows.
06 · Verification

Crowdsourced verification payload.

Run this on the Ghostfolio host or a jump box with access to the local Docker daemon and/or application files. Invoke it as bash verify_ghostfolio_cve_2026_28785.sh ghostfolio for a container named ghostfolio, or bash verify_ghostfolio_cve_2026_28785.sh /opt/ghostfolio for a filesystem path; it needs read access to Docker metadata or the app directory, and no root is required unless your Docker socket permissions demand it.

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/usr/bin/env bash
# verify_ghostfolio_cve_2026_28785.sh
# Check Ghostfolio version against CVE-2026-28785 fixed version 2.244.0
# Outputs one of: VULNERABLE / PATCHED / UNKNOWN
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN

set -u

FIXED_VERSION="2.244.0"
TARGET="${1:-ghostfolio}"
FOUND_VERSION=""

trim() {
  local s="$1"
  s="${s#\"}"
  s="${s%\"}"
  printf '%s' "$s"
}

version_lt() {
  # returns 0 if $1 < $2
  [ "$1" = "$2" ] && return 1
  local first
  first=$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)
  [ "$first" = "$1" ]
}

extract_version_from_package_json() {
  local file="$1"
  [ -f "$file" ] || return 1
  local v
  v=$(grep -E '"version"\s*:\s*"[^"]+"' "$file" 2>/dev/null | head -n1 | sed -E 's/.*"version"\s*:\s*"([^"]+)".*/\1/')
  [ -n "$v" ] || return 1
  printf '%s' "$v"
  return 0
}

extract_version_from_container() {
  local container="$1"
  command -v docker >/dev/null 2>&1 || return 1
  docker inspect "$container" >/dev/null 2>&1 || return 1

  local v=""

  # Try common package.json locations inside the container
  for p in /usr/src/app/package.json /app/package.json /var/www/package.json; do
    v=$(docker exec "$container" sh -c "[ -f '$p' ] && grep -E '\"version\"\s*:\s*\"[^\"]+\"' '$p' | head -n1" 2>/dev/null | sed -E 's/.*\"version\"\s*:\s*\"([^\"]+)\".*/\1/' )
    if [ -n "$v" ]; then
      printf '%s' "$v"
      return 0
    fi
  done

  # Fall back to image tag if present and specific
  v=$(docker inspect --format '{{.Config.Image}}' "$container" 2>/dev/null | sed -E 's/.*:([^:@]+)$/\1/')
  case "$v" in
    latest|stable|main|master|"") return 1 ;;
    *) printf '%s' "$v"; return 0 ;;
  esac
}

extract_version_from_path() {
  local base="$1"
  [ -d "$base" ] || return 1
  local p
  for p in "$base/package.json" "$base/apps/api/package.json" "$base/app/package.json"; do
    FOUND_VERSION=$(extract_version_from_package_json "$p") && { printf '%s' "$FOUND_VERSION"; return 0; }
  done
  return 1
}

# 1) If target is a directory, inspect files directly
if [ -d "$TARGET" ]; then
  FOUND_VERSION=$(extract_version_from_path "$TARGET") || true
fi

# 2) If not found, try as a Docker container name/ID
if [ -z "$FOUND_VERSION" ]; then
  FOUND_VERSION=$(extract_version_from_container "$TARGET") || true
fi

# 3) If still not found, try auto-discovering a likely Ghostfolio container
if [ -z "$FOUND_VERSION" ] && command -v docker >/dev/null 2>&1; then
  CANDIDATE=$(docker ps --format '{{.Names}} {{.Image}}' 2>/dev/null | awk 'tolower($0) ~ /ghostfolio/ {print $1; exit}')
  if [ -n "${CANDIDATE:-}" ]; then
    FOUND_VERSION=$(extract_version_from_container "$CANDIDATE") || true
  fi
fi

FOUND_VERSION=$(trim "$FOUND_VERSION")

if [ -z "$FOUND_VERSION" ]; then
  echo "UNKNOWN - could not determine Ghostfolio version from path/container: $TARGET"
  exit 2
fi

if version_lt "$FOUND_VERSION" "$FIXED_VERSION"; then
  echo "VULNERABLE - Ghostfolio version $FOUND_VERSION is older than fixed version $FIXED_VERSION"
  exit 1
else
  echo "PATCHED - Ghostfolio version $FOUND_VERSION is at or above fixed version $FIXED_VERSION"
  exit 0
fi
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning: find every Ghostfolio instance, confirm whether it is internet-exposed, and immediately identify whether public signup is enabled. For this HIGH reassessment, the noisgate mitigation SLA is ≤30 days: by then, disable public signup, pull exposed instances behind stronger access controls, and put temporary monitoring or WAF coverage on the vulnerable import/history flow. The noisgate remediation SLA is ≤180 days: upgrade all affected instances to 2.244.0 or later and verify the running version, but do not let the long patch window delay the access-control cleanup that removes the most realistic attack path.

Sources

  1. GitHub Security Advisory GHSA-m5cc-7jw5-34xp
  2. Ghostfolio 2.244.0 release
  3. NVD CVE-2026-28785
  4. OSV entry for CVE-2026-28785
  5. OpenCVE record with CISA ADP enrichment
  6. CISA Known Exploited Vulnerabilities Catalog
  7. Ghostfolio Open Startup metrics
  8. Ghostfolio self-hosting FAQ
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.