knadh/listmonk · error

table '%s' is not allowed

Error message

table '%s' is not allowed

What it means

After extracting table names from a custom subscriber query's plan, each table is checked against a whitelist of allowed tables. If any relation in the query is not in allowedTables, this error is returned, preventing arbitrary table access through user-supplied SQL used by QuerySubscribers/ExportSubscribers.

Source

Thrown at internal/core/subscribers.go:626

		return err
	}
	defer tx.Rollback()

	var plan string
	if err = tx.QueryRow("EXPLAIN (FORMAT JSON) "+query, args...).Scan(&plan); err != nil {
		return err
	}

	// Extract all relation names from the JSON plan.
	tables, err := getTablesFromQueryPlan(plan)
	if err != nil {
		return fmt.Errorf("error getting tables from query: %v", err)
	}

	// Validate against allowed tables.
	for _, table := range tables {
		if _, ok := allowedTables[table]; !ok {
			return fmt.Errorf("table '%s' is not allowed", table)
		}
	}

	return nil
}

// getTablesFromQueryPlan parses the EXPLAIN JSON to find all "Relation Name" entries.
func getTablesFromQueryPlan(explainJSON string) ([]string, error) {
	var plans []map[string]any
	if err := json.Unmarshal([]byte(explainJSON), &plans); err != nil {
		return nil, err
	}

	// Collect table names in `tables` recursively.
	tables := make(map[string]struct{})
	for _, plan := range plans {
		traverseQueryPlan(plan, tables)
	}

View on GitHub (pinned to 670c01717d)

Solutions

  1. Restrict the query to whitelisted tables: subscribers, lists, subscriber_lists (and their allowed columns)
  2. Fix table-name typos or use the exact schema-qualified name if the whitelist contains qualified names
  3. If additional tables are legitimately needed, extend the allowedTables whitelist in the code and redeploy (mind SQL-injection implications)
  4. Use the library's public list/query APIs instead of raw SQL against unrelated tables

Example fix

// before
SELECT * FROM users WHERE email LIKE '%@corp.com'
// after
SELECT s.* FROM subscribers s WHERE s.email LIKE '%@corp.com'
Defensive patterns

Strategy: validation

Validate before calling

allowed := map[string]bool{"subscribers": true, "lists": true, "subscriber_lists": true}
for _, t := range extractTableNames(query) {
    if !allowed[t] {
        return fmt.Errorf("table %q not permitted; use subscribers/lists/subscriber_lists", t)
    }
}

Try / catch

if err := validateQueryTables(query); err != nil {
    var denied bool
    if strings.Contains(err.Error(), "is not allowed") { denied = true }
    return fmt.Errorf("invalid segment query (denied=%v): %w", denied, err)
}

Prevention

When it happens

Trigger: Passing a query to QuerySubscribers or ExportSubscribers that SELECTs from, JOINs to, or otherwise references any table outside the whitelist — e.g. querying 'users', 'campaigns', or any admin table instead of only subscribers/lists/subscriber_lists.

Common situations: Trying to build segments from tables the library intentionally hides, typos in table names, referencing views or temp tables not on the whitelist, or JOINing to application tables in a custom export.

Related errors


AI-assisted analysis of knadh/listmonk@670c01717d (2026-09-01). Data as JSON: /api/errors/db360695c449b5d9. Report an issue: GitHub.