Introduction
Rails strong parameters protect against mass assignment by requiring explicit whitelisting of permitted attributes. Unpermitted parameters are silently removed from the params hash without raising an error in production. This means form fields can be submitted successfully but never saved to the database, creating confusing bugs where data appears to be lost.
Symptoms
- Form submits successfully but certain fields are blank in database
- User profile updates silently ignore new fields after model changes
- Nested attributes not saved for
has_manyassociations - API requests return 200 OK but missing fields in response
- No error logged when parameters are filtered
Check for unpermitted params in logs:
``
Unpermitted parameter: :phone_number. Context: {
controller: UsersController,
action: update,
request: #<ActionDispatch::Request>,
params: {"user"=>{"name"=>"John", "phone_number"=>"+1234567890"}}
}
Common Causes
- New field added to form but not to
permitlist in controller - Nested attributes structure changed (array vs hash)
requirecalled on wrong param key level- Permitted scalar types vs array types confusion
- ActionController::Parameters not converted to hash before use
Step-by-Step Fix
- 1.Enable logging of unpermitted parameters:
- 2.```ruby
- 3.# config/environments/development.rb
- 4.# Already on by default in development
# config/environments/production.rb # Turn on in production temporarily to debug config.action_controller.action_on_unpermitted_parameters = :log
# Or raise error (useful in test environment) config.action_controller.action_on_unpermitted_parameters = :raise ```
- 1.Fix permit list for nested attributes:
- 2.```ruby
- 3.class UsersController < ApplicationController
- 4.def user_params
- 5.params.require(:user).permit(
- 6.:name,
- 7.:email,
- 8.:phone_number,
- 9.:avatar,
- 10.# Nested has_many
- 11.addresses_attributes: [:id, :street, :city, :state, :zip, :_destroy],
- 12.# Nested has_one
- 13.profile_attributes: [:id, :bio, :website],
- 14.# Array of scalars
- 15.role_ids: [],
- 16.# Nested array of objects
- 17.preferences: [:theme, :language, :notifications],
- 18.)
- 19.end
- 20.end
- 21.
` - 22.Handle dynamic permitted parameters safely:
- 23.```ruby
- 24.class UsersController < ApplicationController
- 25.def user_params
- 26.# Safe approach: derive from model columns
- 27.params.require(:user).permit(permitted_user_attributes)
- 28.end
private
def permitted_user_attributes # Only permit attributes that exist on the model User.column_names.map(&:to_sym) + [ # Virtual attributes :current_password, :password_confirmation, # Nested attributes addresses_attributes: Address.attribute_names.map(&:to_sym) + [:id, :_destroy], ] end end ```
- 1.Add test to catch missing permits:
- 2.```ruby
- 3.# test/controllers/users_controller_test.rb
- 4.test "should permit all submitted parameters" do
- 5.Rails.application.config.action_controller.action_on_unpermitted_parameters = :raise
patch user_url(@user), params: { user: { name: "Updated", email: "new@example.com", phone_number: "+1234567890", # Will raise if not permitted } }
assert_response :redirect end ```
- 1.Debug missing params in development console:
- 2.```ruby
- 3.# Check what params actually contains
- 4.puts params.inspect
- 5.puts params.to_unsafe_h.inspect # Shows ALL params including unpermitted
# Compare permitted vs submitted permitted = params.require(:user).permit(:name, :email) all_params = params.require(:user).to_unsafe_h missing = all_params.keys - permitted.keys
puts "Missing parameters: #{missing}" ```
Prevention
- Set
action_on_unpermitted_parameters = :login production - Add controller tests that submit all expected fields with
:raiseenabled - Use
params.to_unsafe_hin logging for debugging (never in production code) - Keep
permitlists close to the model definition for easy review - Use a form object pattern (Reform, Dry-Validation) for complex forms
- Document parameter structure in API documentation