Rails 8 introduces Parameters#expect for safer parameter handling

Strong Parameters have been a Rails security staple since Rails 4, but they had a vulnerability: carefully crafted parameters could trigger 500 errors instead of the expected 400 Bad Request, potentially exposing application internals.

Before

Previously, handling nested parameters with permit could be exploited:

1
2
3
4
5
6
7
class UsersController < ApplicationController
  def create
    user_params = params.require(:user).permit(:name, :email, tags: [])
    @user = User.create!(user_params)
    render json: @user
  end
end

An attacker could send malformed parameters to trigger a 500 error:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Expected usage
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"user": {"name": "Alice", "tags": ["ruby", "rails"]}}'

# Malicious request causing 500 error
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"user": {"name": "Alice", "tags": "not-an-array"}}'
# => 500 Internal Server Error (ActionController::UnpermittedParameters)

# Another attack vector
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"user": {"name": "Alice", "tags": {"0": "ruby", "1": "rails"}}}'
# => 500 Internal Server Error

These 500 errors could reveal stack traces in development or trigger unnecessary error alerts in production.

Rails 8

Rails 8 introduces Parameters#expect which validates parameter structure and returns 400 Bad Request for malformed input:

1
2
3
4
5
6
7
class UsersController < ApplicationController
  def create
    user_params = params.expect(user: [:name, :email, tags: []])
    @user = User.create!(user_params)
    render json: @user
  end
end

Now the same malicious requests raise ActionController::ParameterMissing which Rails handles as a 400 Bad Request:

1
2
3
4
5
6
7
8
9
10
11
# Malicious request now returns 400
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"user": {"name": "Alice", "tags": "not-an-array"}}'
# => 400 Bad Request

# Hash instead of array also caught
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"user": {"name": "Alice", "tags": {"0": "ruby"}}}'
# => 400 Bad Request

Complex Nested Parameters

expect really shines with deeply nested structures:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ProjectsController < ApplicationController
  def create
    # Define expected structure with nested arrays
    project_params = params.expect(
      project: [
        :name,
        :description,
        { settings: [:theme, :notifications] },
        { team_members: [[:name, :role, permissions: []]] }
      ]
    )
    
    @project = Project.create!(project_params)
    render json: @project
  end
end

Valid request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "project": {
    "name": "New App",
    "description": "Rails application",
    "settings": {
      "theme": "dark",
      "notifications": true
    },
    "team_members": [
      {
        "name": "Alice",
        "role": "developer",
        "permissions": ["read", "write"]
      },
      {
        "name": "Bob",
        "role": "designer",
        "permissions": ["read"]
      }
    ]
  }
}

Conclusion

Parameters#expect is a small but important security improvement. It transforms potential 500 errors into proper 400 responses, making Rails APIs more robust against parameter manipulation attacks while providing clearer feedback to API consumers.

References

Prateek Choudhary
Prateek Choudhary
Technology Leader