{"record":{"id":"db360695c449b5d9","repo":"knadh/listmonk","slug":"table-s-is-not-allowed","errorCode":null,"errorMessage":"table '%s' is not allowed","messagePattern":"table '(.+?)' is not allowed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/core/subscribers.go","lineNumber":626,"sourceCode":"\t\treturn err\n\t}\n\tdefer tx.Rollback()\n\n\tvar plan string\n\tif err = tx.QueryRow(\"EXPLAIN (FORMAT JSON) \"+query, args...).Scan(&plan); err != nil {\n\t\treturn err\n\t}\n\n\t// Extract all relation names from the JSON plan.\n\ttables, err := getTablesFromQueryPlan(plan)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting tables from query: %v\", err)\n\t}\n\n\t// Validate against allowed tables.\n\tfor _, table := range tables {\n\t\tif _, ok := allowedTables[table]; !ok {\n\t\t\treturn fmt.Errorf(\"table '%s' is not allowed\", table)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// getTablesFromQueryPlan parses the EXPLAIN JSON to find all \"Relation Name\" entries.\nfunc getTablesFromQueryPlan(explainJSON string) ([]string, error) {\n\tvar plans []map[string]any\n\tif err := json.Unmarshal([]byte(explainJSON), &plans); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Collect table names in `tables` recursively.\n\ttables := make(map[string]struct{})\n\tfor _, plan := range plans {\n\t\ttraverseQueryPlan(plan, tables)\n\t}","sourceCodeStart":608,"sourceCodeEnd":644,"githubUrl":"https://github.com/knadh/listmonk/blob/670c01717d48647093335cc23a6be6f4b79c3b6b/internal/core/subscribers.go#L608-L644","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Restrict the query to whitelisted tables: subscribers, lists, subscriber_lists (and their allowed columns)","Fix table-name typos or use the exact schema-qualified name if the whitelist contains qualified names","If additional tables are legitimately needed, extend the allowedTables whitelist in the code and redeploy (mind SQL-injection implications)","Use the library's public list/query APIs instead of raw SQL against unrelated tables"],"exampleFix":"// before\nSELECT * FROM users WHERE email LIKE '%@corp.com'\n// after\nSELECT s.* FROM subscribers s WHERE s.email LIKE '%@corp.com'","handlingStrategy":"validation","validationCode":"allowed := map[string]bool{\"subscribers\": true, \"lists\": true, \"subscriber_lists\": true}\nfor _, t := range extractTableNames(query) {\n    if !allowed[t] {\n        return fmt.Errorf(\"table %q not permitted; use subscribers/lists/subscriber_lists\", t)\n    }\n}","typeGuard":null,"tryCatchPattern":"if err := validateQueryTables(query); err != nil {\n    var denied bool\n    if strings.Contains(err.Error(), \"is not allowed\") { denied = true }\n    return fmt.Errorf(\"invalid segment query (denied=%v): %w\", denied, err)\n}","preventionTips":["Only reference whitelisted tables (subscribers, lists, subscriber_lists) in custom queries","Double-check table-name spelling against the whitelist before saving a query","Don't join to application/admin tables in subscriber segments — use the public APIs instead","If new tables are genuinely needed, update allowedTables in code with a security review"],"tags":["sql","security","whitelist","query-validation"],"backgroundTag":"table-not-allowed","analyzedSha":"670c01717d48647093335cc23a6be6f4b79c3b6b","analyzedAt":"2026-09-01T03:39:35.452Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}