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.
4 steps from start to impact.
Find an exposed Ghostfolio instance
curl/httpx against internet-facing app inventories.- Ghostfolio is deployed and network-reachable
- The instance is externally exposed or reachable from the attacker's network position
- 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
Get access to the manual import feature
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.- Public signup is enabled, or the attacker already has a valid account
- The import feature is exposed to that user role
- 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
Exploit blind SQLi through getHistorical()
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.- The target runs a version earlier than 2.244.0
- The attacker can send crafted input into the import/history lookup path
- 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
Read or alter Ghostfolio data
- The Ghostfolio database user has meaningful read/write privileges
- The attacker can complete enough blind extraction or modify targeted records
- 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
The supporting signals.
| In-the-wild status | No 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 availability | No public PoC repo or Metasploit module located during review. That lowers operational risk today, though SQLi is usually reproducible once request structure is understood. |
| EPSS | 0.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 status | Not KEV-listed as of 2026-05-09 review time. |
| CVSS vector reality check | Vendor/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 versions | All Ghostfolio versions earlier than 2.244.0 are listed as affected in GHSA/NVD/OSV. |
| Fixed version | Patch 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 population | This 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 data | No 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 credit | Disclosed 2026-03-06. GitHub advisory credits ratrarity as reporter and dtslvr as remediation developer. |
noisgate verdict.
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.
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.
What to do — in priority order.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
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.
#!/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
If you remember one thing.
Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.