CVE-2026-62361: Authenticated SQL Injection and Table Access Control Bypass in Listmonk Subscriber Export
Vulnerability ID: CVE-2026-62361
CVSS Score: 5.5
Published: 2026-11-06
CVE-2026-62361 (GHSA-xgjr-7j9q-2h4r) is an authenticated SQL injection and authorization bypass vulnerability in listmonk, an open-source, self-hosted newsletter and mailing list manager written in Go. In versions prior to 6.2.0, the subscriber export endpoint (GET /api/subscribers/export) directly interpolates user-supplied SQL filter expressions into a PostgreSQL query without invoking the table allowlist validation function (validateQueryTables). Consequently, authenticated users possessing the subscribers:sql_query and subscribers:get_all permissions can bypass relational access controls, execute arbitrary subqueries against restricted tables such as users and settings, and modify database state via writable Common Table Expressions (CTEs).
TL;DR
An omission of query plan table validation in listmonk's ExportSubscribers handler allows authenticated users with SQL query permissions to query restricted database tables (such as users and settings) via error-based or blind SQL injection, and modify records due to the lack of a read-only transaction wrapper.
⚠️ Exploit Status: POC
Technical Details
- CVE ID: CVE-2026-62361 (GHSA-xgjr-7j9q-2h4r)
- Weakness Class: CWE-89 (SQL Injection) / Table Allowlist Bypass
- CVSS v3.1 Score: 5.5 Medium (CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:L/A:N)
- Affected Component: ExportSubscribers in internal/core/subscribers.go (GET /api/subscribers/export)
- Prerequisites: Authenticated user with subscribers:sql_query and subscribers:get_all permissions
- Primary Impact: Exfiltration of admin password hashes (users table) and SMTP/API credentials (settings table)
- Exploit Status: Public Proof-of-Concept (PoC) available; EPSS 0.30%
- Patched Version: listmonk v6.2.0
Affected Systems
- knadh/listmonk
-
listmonk: < 6.2.0 (Fixed in:
6.2.0)
Code Analysis
Commit: c0a6525
Fix missing SQL query validation on subscriber export for superadmisn with 'sql_query' permission.
File: internal/core/subscribers.go
@@ -18,15 +18,15 @@ import (
var (
allowedSubQueryTables = map[string]struct{}{
- "subscribers": {},
- "lists": {},
- "subscribers_lists": {},
- "campaigns": {},
- "campaign_lists": {},
- "campaign_views": {},
- "links": {},
- "link_clicks": {},
- "bounces": {},
+ "subscribers": {},
+ "lists": {},
+ "subscriber_lists": {},
+ "campaigns": {},
+ "campaign_lists": {},
+ "campaign_views": {},
+ "links": {},
+ "link_clicks": {},
+ "bounces": {},
}
)
@@ -128,7 +128,7 @@ func (c *Core) QuerySubscribers(searchStr, queryExp string, listIDs []int, subSt
stmt = strings.ReplaceAll(stmt, "%order%", orderBy+" "+order)
// Validate the tables used in the query.
- if err := validateQueryTables(c.db, stmt, allowedSubQueryTables); err != nil {
+ if err := validateQueryTables(c.db, stmt, allowedSubQueryTables, pq.Array(listIDs), subStatus, searchStr, offset, limit); err != nil {
c.log.Printf("error validating query tables: %v", err)
return nil, 0, echo.NewHTTPError(http.StatusBadRequest,
c.i18n.Ts("subscribers.errorPreparingQuery", "error", err.Error()))
@@ -248,6 +248,14 @@ func (c *Core) ExportSubscribers(searchStr, query string, subIDs, listIDs []int,
stmt := strings.ReplaceAll(c.q.QuerySubscribersForExport, "%query%", cond)
+ // Validate the tables used in the query.
+ if err := validateQueryTables(c.db, stmt, allowedSubQueryTables,
+ pq.Array(listIDs), 0, pq.Array(subIDs), subStatus, searchStr, batchSize); err != nil {
+ c.log.Printf("error validating query tables: %v", err)
+ return nil, echo.NewHTTPError(http.StatusBadRequest,
+ c.i18n.Ts("subscribers.errorPreparingQuery", "error", err.Error()))
+ }
+
// Create a readonly transaction that just does COUNT() to obtain the count of results
// and to ensure that the arbitrary query is indeed readonly.
if _, err := c.getSubscriberCount(searchStr, cond, subStatus, listIDs); err != nil {
@@ -593,7 +601,7 @@ func (c *Core) getSubscriberCount(searchStr, queryExp, subStatus string, listIDs
}
// validateQueryTables checks if the query accesses only allowed tables.
-func validateQueryTables(db *sqlx.DB, query string, allowedTables map[string]struct{}) error {
+func validateQueryTables(db *sqlx.DB, query string, allowedTables map[string]struct{}, args ...any) error {
// Get the EXPLAIN (FORMAT JSON) output.
tx, err := db.BeginTxx(context.Background(), &sql.TxOptions{ReadOnly: true})
if err != nil {
@@ -602,7 +610,7 @@ func validateQueryTables(db *sqlx.DB, query string, allowedTables map[string]str
defer tx.Rollback()
var plan string
- if err = tx.QueryRow("EXPLAIN (FORMAT JSON) "+query, nil, models.SubscriberStatusEnabled, "", 0, 10).Scan(&plan); err != nil {
+ if err = tx.QueryRow("EXPLAIN (FORMAT JSON) "+query, args...).Scan(&plan); err != nil {
return err
}
Exploit Details
- GitHub Security Advisory: Official advisory containing proof-of-concept HTTP requests demonstrating error-based exfiltration of bcrypt password hashes and SMTP credentials, as well as time-based blind execution.
Mitigation Strategies
- Upgrade listmonk to version 6.2.0 or later to enforce table allowlist validation on subscriber export queries.
- Restrict the 'subscribers:sql_query' permission strictly to fully trusted Super Admin accounts.
- Implement reverse proxy or WAF filtering on the '/api/subscribers/export' endpoint to block subqueries and type-casting functions in the 'query' parameter.
- Ensure the PostgreSQL database user configured for listmonk does not have PostgreSQL superuser privileges.
Remediation Steps:
- Identify all running instances of listmonk and verify their version numbers (versions < 6.2.0 are vulnerable).
- Pull the latest listmonk v6.2.0+ binary or Docker container image from https://github.com/knadh/listmonk/releases/tag/v6.2.0.
- Restart the listmonk service using the updated binary or container.
- Review user roles in the listmonk administrative dashboard and remove 'subscribers:sql_query' from non-administrative roles.
- If exploitation is suspected, rotate all user passwords, API tokens, and external SMTP/S3 credentials stored in listmonk settings.
References
- GitHub Security Advisory GHSA-xgjr-7j9q-2h4r
- Fix Commit c0a6525009a65265230185f16e8674dcc83aa024
- Listmonk v6.2.0 Release Notes
- NVD - CVE-2026-62361
- CVE.org Record - CVE-2026-62361
- CVEList V5 JSON Record - CVE-2026-62361
Read the full report for CVE-2026-62361 on our website for more details including interactive diagrams and full exploit analysis.
Top comments (0)