Tencent/WeKnora · error

access to system column '%s' is not allowed

Error message

access to system column '%s' is not allowed

What it means

The identifier validator rejected a column reference to a PostgreSQL system column (xmin, xmax, cmin, cmax, ctid, tableoid). These columns expose internal row versioning and physical location metadata, which can be used for row-level probing and break portability, so they are blocked in injected expressions. Comparison is case-insensitive against the lowercased identifier.

Source

Thrown at internal/utils/inject.go:2346

	return nil
}

// validateColumnRef validates a column reference
func (v *sqlValidator) validateColumnRef(cr *pg_query.ColumnRef) error {
	if !v.checkSystemColumns {
		return nil
	}

	// Check for system column access
	for _, field := range cr.Fields {
		if s := field.GetString_(); s != nil {
			colName := strings.ToLower(s.Sval)
			// Block access to system columns
			systemColumns := []string{"xmin", "xmax", "cmin", "cmax", "ctid", "tableoid"}
			for _, sysCol := range systemColumns {
				if colName == sysCol {
					return fmt.Errorf("access to system column '%s' is not allowed", colName)
				}
			}
			// Block pg_ prefixed identifiers
			if strings.HasPrefix(colName, "pg_") {
				return fmt.Errorf("access to '%s' is not allowed", colName)
			}
		}
	}
	return nil
}

// getTypeName extracts the type name from a TypeName node
func (v *sqlValidator) getTypeName(tn *pg_query.TypeName) string {
	var parts []string
	for _, name := range tn.Names {
		if s := name.GetString_(); s != nil {
			parts = append(parts, s.Sval)
		}

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Remove the system column reference and use an explicit version/timestamp column maintained by the application
  2. Implement optimistic locking with an app-level updated_at/revision column instead of xmin
  3. Use the database's native API (e.g. direct SQL session, not the injection validator) if system column access is truly required for DBA work
  4. If filtering duplicates, use DISTINCT or an allowlisted unique key rather than ctid

Example fix

// before
filter := "xmin > 100"
// after
filter := "revision > 100" // app-managed version column
Defensive patterns

Strategy: validation

Validate before calling

var systemColumns = []string{"xmin", "xmax", "cmin", "cmax", "ctid", "tableoid"}
func referencesSystemColumn(col string) bool {
    c := strings.ToLower(col)
    return slices.Contains(systemColumns, c)
}

Type guard

func isSystemColumn(name string) bool {
    switch strings.ToLower(name) {
    case "xmin", "xmax", "cmin", "cmax", "ctid", "tableoid":
        return true
    }
    return false
}

Try / catch

if err := injector.Validate(expr); err != nil {
    if strings.Contains(err.Error(), "system column") {
        http.Error(w, "system columns cannot be used in filters; use an application-managed version column", http.StatusBadRequest)
        return
    }
    return
}

Prevention

When it happens

Trigger: An expression referencing a system column such as SELECT xmin FROM t via the validator, or a WHERE/filter string like "xmin::text LIKE '%'", passed as a field identifier node with Sval matching one of the six system column names.

Common situations: Porting legacy Postgres queries that use xmin for optimistic concurrency or change detection; MVCC debugging queries copied into user-facing filters; attackers enumerating row visibility via ctid.

Related errors


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/8705a87b01e7660f. Report an issue: GitHub.