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
- Pull Request #51674 introducing Parameters#expect
- Rails 8 Security Guide
- ActionController::Parameters API