Developers working with gorm#v1.31.2 must assume responsibility for securing all dynamic query parameters, boundaries, and model lifecycles against unauthorized access and injection. While the framework provides parameterized query builders and struct tagging by default, it does not automatically enforce multi-tenant isolation, row-level access controls, or safe association mapping. Security-sensitive surfaces include raw SQL escape hatches, dynamic identifier construction, association persistence, and custom type deserializers, all of which must fail closed when errors occur.
Essential implementation rules
Parameterize All Untrusted Input in Queries and Raw SQL
Always pass untrusted user input using parameterized placeholders like ? or named arguments within Where, raw expressions (clause.Expr), and Joins to prevent SQL injection. Never concatenate raw strings directly into query conditions or JOIN clauses.
Prevent Identifier Injection in Dynamic Table and Column Names
Keep Raw set to false when utilizing clause.Column or clause.Table structs. Disabling identifier escaping allows attackers to inject malicious SQL fragments since database drivers do not support standard positional parameter binding for database identifiers.
Verify RowsAffected and Record NotFound Errors After Scans
Always check db.Error using errors.Is(err, gorm.ErrRecordNotFound) and verify db.RowsAffected > 0 before trusting scanned destination structs or maps in authorization, authentication, or business logic.
Enforce Strict Field Permissions and Whitelist Associations on Write
Define field-level permissions using GORM struct tags such as gorm:"->", gorm:"<-:create", and gorm:"-" to restrict model access. During record creation and updates, explicitly whitelist allowed fields using DB.Select or omit nested relationships using DB.Omit with clause.Associations to prevent mass assignment.
Restrict Association Mutability and Avoid Unscoped Queries in Workflows
Control association field mutability by explicitly specifying select or omit clauses when persisting structs. Avoid calling Unscoped() in standard request handlers as it bypasses soft-delete constraints, and always supply explicit WHERE clauses or primary keys on delete operations to prevent mass deletion.
Safely Handle Dynamic Types in Custom Serializers
When writing custom field deserializers with schema.RegisterSerializer or custom Scanner types, avoid unchecked type assertions on database values. Use an explicit type switch across expected driver types such as []byte and string, and return an error for unexpected types to prevent runtime panics.
Enforce Query Timeouts and Resource Limits
Configure DefaultContextTimeout, PrepareStmtMaxSize, and PrepareStmtTTL in gorm.Config to bound resource utilization. Always pass explicit context deadlines using .WithContext(ctx) on query sessions to prevent operations from hanging indefinitely.
Redact Sensitive Parameters in Query Logs
Enable ParameterizedQueries within logger.Config or implement the ParamsFilter interface on custom loggers to suppress raw parameter values and prevent secrets, tokens, and personal data from leaking into execution logs.
Ensure Lifecycle Hooks Remain Active for Security Validation
Ensure that SkipHooks remains false and avoid using direct update methods like UpdateColumn if your model lifecycle hooks handle access control, input validation, or cryptographic hashing. Use Update or Updates to guarantee registered hooks execute properly.
gorm: All Security Cards
Approximately 2,670 tokens
On this card
Category: access control
Enforce explicit query filters and field permissions to prevent unauthorized access
Use when
Use when querying, updating, or deleting records to ensure that unauthorized actors cannot bypass soft-delete boundaries, expose sensitive fields, or execute unconstrained destructive queries.
Secure rules
Rule 1: Avoid using Unscoped in standard user-facing workflows
Do not call Unscoped() in standard request handlers as it bypasses soft-delete constraints and can expose deleted or revoked resources. Use Unscoped() strictly in administrative workflows that require access to archived data.
var user Userif err := db.Where("id = ?", userID).First(&user).Error; err != nil { // Handle error or record not found}
Rule 2: Configure field permission struct tags to restrict model access
Define field-level permissions using GORM struct tags such as gorm:"->", gorm:"<-:create", and gorm:"-" to explicitly enforce read and write permissions on models and prevent unauthorized field updates or exposures.
type UserProfile struct { ID uint `gorm:"primaryKey"` // Read and write enabled by default Internal string `gorm:"-"` // Completely ignored by GORM ReadOnly string `gorm:"->"` // Read-only; creates and updates are ignored CreatedAt string `gorm:"<-:create"` // Writeable on create only; updates ignored UpdateOnly string `gorm:"<-:update"` // Writeable on update only; creates ignored WriteOnly string `gorm:"->:false;<-:create,update"` // Write-only; reads are ignored}
Rule 3: Require explicit WHERE conditions on delete queries
Supply explicit WHERE clauses or primary key values when performing delete operations to prevent accidental mass data deletion across database tables or unauthorized record destruction.
db.Where("id = ? AND tenant_id = ?", targetID, tenantID).Delete(&User{})
Category: api contract misuse
Check RowsAffected and ErrRecordNotFound after scanning query results
Use when
When executing row scans with GORM’s Scan API and utilizing the returned results in authorization, authentication, or business logic.
Secure rules
Rule 1: Verify both database errors and row counts after executing a scan query.
Always check db.Error using errors.Is(err, gorm.ErrRecordNotFound) and verify that db.RowsAffected > 0 before trusting destination structs or maps in your application logic. When zero rows match the query, db.RowsAffected remains 0 and GORM will not add gorm.ErrRecordNotFound to db.Error unless explicitly configured, risking the use of uninitialized or zero-valued structures.
var user Userresult := db.Table("users").Where("id = ? AND active = ?", userID, true).Scan(&user)if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, errors.New("user not found") } return nil, result.Error}if result.RowsAffected == 0 { return nil, errors.New("user not found")}
Category: boundary control
Enforce boundary checks on association field mutability during data persistence
Use when
Persisting structs with relationships such as BelongsTo, HasOne, HasMany, or Many2Many using untrusted input.
Secure rules
Rule 1: Restrict association field mutability by explicitly specifying select or omit clauses.
When saving user-provided struct data, developers must enforce strict boundary control by explicitly utilizing Select or Omit clauses to limit which associated models and nested fields are processed. Avoid executing persistence operations with global full-save behaviors on structs containing untrusted nested fields.
// Safe: Explicitly omit association saves when updating parent modeldb.Omit(clause.Associations).Save(&user)// Safe: Explicitly limit persistence to specific association fieldsdb.Select("Profile.Bio").Save(&user)
Category: deserialization
Safely Handle Dynamic Database Types in Custom Serializers
Use when
When implementing custom field serializers using schema.RegisterSerializer or custom Scanner types where database values are received as empty interfaces.
Secure rules
Rule 1: Explicitly type-switch across expected driver types and return an error for unexpected types when scanning database values.
When writing custom field deserializers, avoid unchecked type assertions on values provided by database drivers. Use an explicit type switch to handle types such as []byte and string, and return an error when an unexpected type is encountered to prevent runtime panics and application crashes.
type CustomSerializer struct { prefix string}func (c *CustomSerializer) Scan(ctx context.Context, field *schema.Field, dst reflect.Value, dbValue interface{}) error { switch v := dbValue.(type) { case []byte: return field.Set(ctx, dst, strings.TrimPrefix(string(v), c.prefix)) case string: return field.Set(ctx, dst, strings.TrimPrefix(v, c.prefix)) default: return fmt.Errorf("unsupported database value type %T", dbValue) }}
Category: escape hatch
Avoid Disabling Identifier Escaping on Untrusted Table and Column Names
Use when
When constructing database queries dynamically where table or column names might be influenced by user input.
Secure rules
Rule 1: Do not set Raw: true on clause.Column or clause.Table structs when using dynamic or untrusted identifier names.
Keep Raw set to false so that GORM automatically quotes identifiers. Disabling identifier escaping allows attackers to inject malicious SQL fragments into query structures since database drivers do not support standard positional parameter binding for table or column names.
col := clause.Column{ Table: "users", Name: "email", Raw: false,}
Category: injection
Parameterize Dynamic Query Clauses and Expressions in GORM
Use when
When building query clauses, raw statements, custom valuers, associations, or joins where dynamic or untrusted input is included in database operations.
Secure rules
Rule 1: Use parameter placeholders for untrusted input in query conditions.
Always pass untrusted user input using parameterized placeholders such as ? or named parameters in GORM query methods like Where to prevent SQL injection.
var user UseruserInput := "jinzhu; delete * from users"if err := db.Where("name = ?", userInput).First(&user).Error; err != nil { // handle error}
Rule 2: Use parameter placeholders when building raw SQL expressions
When constructing custom SQL expressions with clause.Expr or clause.NamedExpr, always use parameter placeholders in the SQL string and pass user inputs separately via the Vars slice or named arguments.
expr := clause.Expr{ SQL: "role = ? AND status = ?", Vars: []interface{}{userRole, userStatus},}
Rule 3: Use parameterized placeholders for untrusted inputs in Where Conditions
Never concatenate untrusted user input directly into query strings. Use ? placeholders and pass user inputs as separate arguments.
db.Where("username = ? AND status = ?", userInput, "active")
Rule 4: Parameterize raw JOIN clauses to prevent SQL injection
When constructing custom JOIN clauses with user-supplied parameters, pass raw SQL fragments using parameter placeholders or named arguments instead of string concatenation.
DB.Joins("INNER JOIN pets ON pets.user_id = users.id AND pets.name = ?", userInputName). Where("users.name = ?", username). Find(&users)
Category: input contract definition
Restrict Mass Assignment and Associations During Record Creation
Use when
When creating database records from user-controlled input structs or maps to enforce input structure and field boundaries.
Secure rules
Rule 1: Explicitly whitelist allowed fields or exclude sensitive associations using DB.Select or DB.Omit during record creation.
Use DB.Select to specify allowed input fields for insertion, or use DB.Omit with clause.Associations to prevent the persistence of unintended nested relationships and mass assignment vulnerabilities.
user := User{Name: "Alice", Account: untrustedAccount}// Prevent creating or cascading into associated modelsif err := db.Omit(clause.Associations).Create(&user).Error; err != nil { // handle error}// Alternatively, whitelist specific fields for insertionif err := db.Select("Name", "Age").Create(&user).Error; err != nil { // handle error}
Category: resource exhaustion
Enforce Query Timeouts and Resource Limits in GORM Sessions
Use when
Configuring GORM database instances and executing queries that require bounds on execution time or connection duration.
Secure rules
Rule 1: Configure default execution timeouts and statement cache bounds on GORM configuration.
Set DefaultContextTimeout, PrepareStmtMaxSize, and PrepareStmtTTL in gorm.Config to ensure all queries execute within bounded resource limits and prevent unbounded memory growth.
Rule 2: Pass explicit context timeouts to query sessions.
Use .WithContext(ctx) with a timeout deadline when executing queries to prevent operations from hanging indefinitely under network outages or lock contention.
When configuring GORM loggers or implementing custom loggers to prevent the exposure of secrets, tokens, and personal data in query execution logs.
Secure rules
Rule 1: Enable ParameterizedQueries in logger configuration to suppress raw parameter values.
Set ParameterizedQueries to true within logger.Config when initializing standard or slog loggers to omit raw query values from execution logs and prevent sensitive data exposure.
Rule 2: Implement the ParamsFilter interface to sanitize parameters in custom loggers.
Implement the ParamsFilter interface on custom logger types or filtering structs to inspect and redact sensitive parameter values prior to downstream execution or logging.
type SensitiveFilter struct{}func (f SensitiveFilter) ParamsFilter(ctx context.Context, sql string, params ...interface{}) (string, []interface{}) { sanitizedParams := make([]interface{}, len(params)) for i, p := range params { if isSensitiveField(p) { sanitizedParams[i] = "[REDACTED]" } else { sanitizedParams[i] = p } } return sql, sanitizedParams}
Category: security control integrity
Prevent Security Control Bypass by Ensuring Lifecycle Hooks Remain Active
Use when
Use when performing model updates, deletions, or session configurations where lifecycle hooks enforce critical security validations, audit logging, or field transformations.
Secure rules
Rule 1: Avoid setting SkipHooks to true or using direct update methods that bypass model hooks when hooks contain mandatory security controls.
Ensure that SkipHooks remains false and avoid using UpdateColumn or UpdateColumns if your model lifecycle hooks handle access control, input validation, or cryptographic hashing. Use Update or Updates to ensure registered hooks execute properly.
Enforce explicit query filters and field permissions to prevent unauthorized access
Approximately 453 tokens
Use when
Use when querying, updating, or deleting records to ensure that unauthorized actors cannot bypass soft-delete boundaries, expose sensitive fields, or execute unconstrained destructive queries.
Secure rules
Rule 1: Avoid using Unscoped in standard user-facing workflows
Do not call Unscoped() in standard request handlers as it bypasses soft-delete constraints and can expose deleted or revoked resources. Use Unscoped() strictly in administrative workflows that require access to archived data.
var user Userif err := db.Where("id = ?", userID).First(&user).Error; err != nil { // Handle error or record not found}
Rule 2: Configure field permission struct tags to restrict model access
Define field-level permissions using GORM struct tags such as gorm:"->", gorm:"<-:create", and gorm:"-" to explicitly enforce read and write permissions on models and prevent unauthorized field updates or exposures.
type UserProfile struct { ID uint `gorm:"primaryKey"` // Read and write enabled by default Internal string `gorm:"-"` // Completely ignored by GORM ReadOnly string `gorm:"->"` // Read-only; creates and updates are ignored CreatedAt string `gorm:"<-:create"` // Writeable on create only; updates ignored UpdateOnly string `gorm:"<-:update"` // Writeable on update only; creates ignored WriteOnly string `gorm:"->:false;<-:create,update"` // Write-only; reads are ignored}
Rule 3: Require explicit WHERE conditions on delete queries
Supply explicit WHERE clauses or primary key values when performing delete operations to prevent accidental mass data deletion across database tables or unauthorized record destruction.
db.Where("id = ? AND tenant_id = ?", targetID, tenantID).Delete(&User{})
Check RowsAffected and ErrRecordNotFound after scanning query results
Approximately 288 tokens
Use when
When executing row scans with GORM’s Scan API and utilizing the returned results in authorization, authentication, or business logic.
Secure rules
Rule 1: Verify both database errors and row counts after executing a scan query.
Always check db.Error using errors.Is(err, gorm.ErrRecordNotFound) and verify that db.RowsAffected > 0 before trusting destination structs or maps in your application logic. When zero rows match the query, db.RowsAffected remains 0 and GORM will not add gorm.ErrRecordNotFound to db.Error unless explicitly configured, risking the use of uninitialized or zero-valued structures.
var user Userresult := db.Table("users").Where("id = ? AND active = ?", userID, true).Scan(&user)if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, errors.New("user not found") } return nil, result.Error}if result.RowsAffected == 0 { return nil, errors.New("user not found")}
Enforce boundary checks on association field mutability during data persistence
Approximately 212 tokens
Use when
Persisting structs with relationships such as BelongsTo, HasOne, HasMany, or Many2Many using untrusted input.
Secure rules
Rule 1: Restrict association field mutability by explicitly specifying select or omit clauses.
When saving user-provided struct data, developers must enforce strict boundary control by explicitly utilizing Select or Omit clauses to limit which associated models and nested fields are processed. Avoid executing persistence operations with global full-save behaviors on structs containing untrusted nested fields.
// Safe: Explicitly omit association saves when updating parent modeldb.Omit(clause.Associations).Save(&user)// Safe: Explicitly limit persistence to specific association fieldsdb.Select("Profile.Bio").Save(&user)
Safely Handle Dynamic Database Types in Custom Serializers
Approximately 272 tokens
Use when
When implementing custom field serializers using schema.RegisterSerializer or custom Scanner types where database values are received as empty interfaces.
Secure rules
Rule 1: Explicitly type-switch across expected driver types and return an error for unexpected types when scanning database values.
When writing custom field deserializers, avoid unchecked type assertions on values provided by database drivers. Use an explicit type switch to handle types such as []byte and string, and return an error when an unexpected type is encountered to prevent runtime panics and application crashes.
type CustomSerializer struct { prefix string}func (c *CustomSerializer) Scan(ctx context.Context, field *schema.Field, dst reflect.Value, dbValue interface{}) error { switch v := dbValue.(type) { case []byte: return field.Set(ctx, dst, strings.TrimPrefix(string(v), c.prefix)) case string: return field.Set(ctx, dst, strings.TrimPrefix(v, c.prefix)) default: return fmt.Errorf("unsupported database value type %T", dbValue) }}
Avoid Disabling Identifier Escaping on Untrusted Table and Column Names
Approximately 184 tokens
Use when
When constructing database queries dynamically where table or column names might be influenced by user input.
Secure rules
Rule 1: Do not set Raw: true on clause.Column or clause.Table structs when using dynamic or untrusted identifier names.
Keep Raw set to false so that GORM automatically quotes identifiers. Disabling identifier escaping allows attackers to inject malicious SQL fragments into query structures since database drivers do not support standard positional parameter binding for table or column names.
col := clause.Column{ Table: "users", Name: "email", Raw: false,}
Parameterize Dynamic Query Clauses and Expressions in GORM
Approximately 416 tokens
Use when
When building query clauses, raw statements, custom valuers, associations, or joins where dynamic or untrusted input is included in database operations.
Secure rules
Rule 1: Use parameter placeholders for untrusted input in query conditions.
Always pass untrusted user input using parameterized placeholders such as ? or named parameters in GORM query methods like Where to prevent SQL injection.
var user UseruserInput := "jinzhu; delete * from users"if err := db.Where("name = ?", userInput).First(&user).Error; err != nil { // handle error}
Rule 2: Use parameter placeholders when building raw SQL expressions
When constructing custom SQL expressions with clause.Expr or clause.NamedExpr, always use parameter placeholders in the SQL string and pass user inputs separately via the Vars slice or named arguments.
expr := clause.Expr{ SQL: "role = ? AND status = ?", Vars: []interface{}{userRole, userStatus},}
Rule 3: Use parameterized placeholders for untrusted inputs in Where Conditions
Never concatenate untrusted user input directly into query strings. Use ? placeholders and pass user inputs as separate arguments.
db.Where("username = ? AND status = ?", userInput, "active")
Rule 4: Parameterize raw JOIN clauses to prevent SQL injection
When constructing custom JOIN clauses with user-supplied parameters, pass raw SQL fragments using parameter placeholders or named arguments instead of string concatenation.
DB.Joins("INNER JOIN pets ON pets.user_id = users.id AND pets.name = ?", userInputName). Where("users.name = ?", username). Find(&users)
Restrict Mass Assignment and Associations During Record Creation
Approximately 232 tokens
Use when
When creating database records from user-controlled input structs or maps to enforce input structure and field boundaries.
Secure rules
Rule 1: Explicitly whitelist allowed fields or exclude sensitive associations using DB.Select or DB.Omit during record creation.
Use DB.Select to specify allowed input fields for insertion, or use DB.Omit with clause.Associations to prevent the persistence of unintended nested relationships and mass assignment vulnerabilities.
user := User{Name: "Alice", Account: untrustedAccount}// Prevent creating or cascading into associated modelsif err := db.Omit(clause.Associations).Create(&user).Error; err != nil { // handle error}// Alternatively, whitelist specific fields for insertionif err := db.Select("Name", "Age").Create(&user).Error; err != nil { // handle error}
Enforce Query Timeouts and Resource Limits in GORM Sessions
Approximately 295 tokens
Use when
Configuring GORM database instances and executing queries that require bounds on execution time or connection duration.
Secure rules
Rule 1: Configure default execution timeouts and statement cache bounds on GORM configuration.
Set DefaultContextTimeout, PrepareStmtMaxSize, and PrepareStmtTTL in gorm.Config to ensure all queries execute within bounded resource limits and prevent unbounded memory growth.
Rule 2: Pass explicit context timeouts to query sessions.
Use .WithContext(ctx) with a timeout deadline when executing queries to prevent operations from hanging indefinitely under network outages or lock contention.
When configuring GORM loggers or implementing custom loggers to prevent the exposure of secrets, tokens, and personal data in query execution logs.
Secure rules
Rule 1: Enable ParameterizedQueries in logger configuration to suppress raw parameter values.
Set ParameterizedQueries to true within logger.Config when initializing standard or slog loggers to omit raw query values from execution logs and prevent sensitive data exposure.
Rule 2: Implement the ParamsFilter interface to sanitize parameters in custom loggers.
Implement the ParamsFilter interface on custom logger types or filtering structs to inspect and redact sensitive parameter values prior to downstream execution or logging.
type SensitiveFilter struct{}func (f SensitiveFilter) ParamsFilter(ctx context.Context, sql string, params ...interface{}) (string, []interface{}) { sanitizedParams := make([]interface{}, len(params)) for i, p := range params { if isSensitiveField(p) { sanitizedParams[i] = "[REDACTED]" } else { sanitizedParams[i] = p } } return sql, sanitizedParams}
Prevent Security Control Bypass by Ensuring Lifecycle Hooks Remain Active
Approximately 190 tokens
Use when
Use when performing model updates, deletions, or session configurations where lifecycle hooks enforce critical security validations, audit logging, or field transformations.
Secure rules
Rule 1: Avoid setting SkipHooks to true or using direct update methods that bypass model hooks when hooks contain mandatory security controls.
Ensure that SkipHooks remains false and avoid using UpdateColumn or UpdateColumns if your model lifecycle hooks handle access control, input validation, or cryptographic hashing. Use Update or Updates to ensure registered hooks execute properly.