{"record":{"id":"d77b082d516abb58","repo":"Tencent/WeKnora","slug":"select-into-is-not-allowed","errorCode":null,"errorMessage":"SELECT INTO is not allowed","messagePattern":"SELECT INTO is not allowed","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/utils/inject.go","lineNumber":1358,"sourceCode":"}\n\n// validateSelectStmt validates a SELECT statement with configured options\nfunc (v *sqlValidator) validateSelectStmt(stmt *pg_query.SelectStmt, result *SQLValidationResult) error {\n\ttablesInQuery := make(map[string]string) // table name -> alias\n\n\t// Check for UNION/INTERSECT/EXCEPT (compound queries)\n\tif stmt.Op != pg_query.SetOperation_SETOP_NONE {\n\t\treturn fmt.Errorf(\"compound queries (UNION/INTERSECT/EXCEPT) are not allowed\")\n\t}\n\n\t// Check for WITH clause (CTEs)\n\tif v.checkCTEs && stmt.WithClause != nil {\n\t\treturn fmt.Errorf(\"WITH clause (CTEs) is not allowed\")\n\t}\n\n\t// Check for INTO clause (SELECT INTO)\n\tif stmt.IntoClause != nil {\n\t\treturn fmt.Errorf(\"SELECT INTO is not allowed\")\n\t}\n\n\t// Check for LOCKING clause (FOR UPDATE, etc.)\n\tif len(stmt.LockingClause) > 0 {\n\t\treturn fmt.Errorf(\"locking clauses (FOR UPDATE, etc.) are not allowed\")\n\t}\n\n\t// Validate FROM clause\n\tfor _, fromItem := range stmt.FromClause {\n\t\tif err := v.validateFromItem(fromItem, tablesInQuery, result); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Validate target list (SELECT columns)\n\tfor _, target := range stmt.TargetList {\n\t\tif err := v.validateNode(target, result); err != nil {\n\t\t\treturn err","sourceCodeStart":1340,"sourceCodeEnd":1376,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/utils/inject.go#L1340-L1376","documentation":"validateSelectStmt rejects any statement with a non-nil IntoClause, i.e. SELECT ... INTO. This is a data-modifying construct (it creates a table), so the validator treats it as out of scope for a read-only SELECT validator and as a potential abuse vector. It is rejected unconditionally with no bypass option.","triggerScenarios":"Passing \"SELECT * INTO new_table FROM users\" (or INTO TEMP/UNLOGGED forms) to the validation API; stmt.IntoClause is non-nil in the pg_query parse tree.","commonSituations":"Developer needs to materialize results server-side and reaches for SELECT INTO; ported scripts from psql workflows; accidental copy-paste of DDL-ish queries into a read-path validator.","solutions":["Remove the INTO clause; use CREATE TABLE AS (ctas) separately outside this validator if you need a new table.","Use a plain SELECT and create/insert the target table in application code.","Run SELECT INTO outside the validated read path, with proper authorization."],"exampleFix":"// before (rejected)\nq := \"SELECT * INTO users_backup FROM users\"\n\n// after\nvalidate(\"SELECT * FROM users\")\ndb.Exec(\"CREATE TABLE users_backup AS SELECT * FROM users\")","handlingStrategy":"validation","validationCode":"re := regexp.MustCompile(`(?i)\\bINTO\\s+(TEMP\\s+|UNLOGGED\\s+)?[\\w.\"]+\\s+FROM\\b`)\nif re.MatchString(sql) {\n    return fmt.Errorf(\"SELECT INTO rejected; use CREATE TABLE AS outside the validated read path\")\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never route table-creating statements through the read-only SELECT validator.","Use CREATE TABLE AS on a separate authorized execution path.","Keep DDL and read paths in distinct code layers.","Review ported psql scripts for SELECT INTO before integrating."],"tags":["sql","validation","postgres","ddl"],"backgroundTag":"unsupported-sql-construct","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}