The bug params.expect exists to fix
The course Controller params & security pointed to
Controller params & security ended by setting expect aside: how require and permit work on their own, and how expect's single call replaces both, was left for a dedicated course, this one. Everything that course taught about require and permit still holds. This course is about expect itself.
The crash require/permit doesn't protect against
An attacker sends { catalog_course: "hack" }, a plain string where this app's Admin::Catalog::CoursesController expects a nested hash:
params = ActionController::Parameters.new(catalog_course: "hack")
params.require(:catalog_course).permit(:title_en)
# NoMethodError: undefined method 'permit' for an instance of String
.require(:catalog_course) returns the string "hack", because a string is present and not blank, and that's all .require checks. .permit, called next, doesn't exist on String. The result is an unhandled NoMethodError, not a clean 400: one of the user-triggered 500 errors that Rails' doc comment for expect says it guards against.
A quieter, more dangerous version of the same bug
params2 = ActionController::Parameters.new(user: { name: "Martin", pets: { name: "hack" } })
result = params2.require(:user).permit(:name, pets: [:name])
result[:pets]
# => #<ActionController::Parameters {"name" => "hack"} permitted: true>
# result[:pets].class => ActionController::Parameters, not an Array
pets: was declared as an array of hashes, pets: [:name], and the attacker sent a single hash instead. .permit doesn't crash this time. It returns result[:pets] as a lone Parameters object instead of the array the code downstream expects. There's no exception and no log line. Code that assumes result[:pets].each { |p| ... } raises a confusing NoMethodError on .each somewhere later. Worse, code that reads result[:pets][:name] gets what the attacker sent and works, while being wrong about what was validated.
The same two payloads, through expect
params.expect(catalog_course: [:title_en])
# ActionController::ParameterMissing: param is missing or the value is empty or invalid: catalog_course
params2.expect(user: [:name, pets: [[:name]]])
# result[:pets] => nil
The first case raises a clean ParameterMissing instead of an unhandled crash; the second returns nil instead of the wrong shape. expect checks the shape of what it's given as well as whether a key is present, which is why this app's admin/catalog/courses_controller.rb uses it.
What this course covers
Seven lessons, each run against ActionController::Parameters objects. This lesson covers the bug and its fix. Lesson 2 covers expect's one-call form, using this app's line in full. Lesson 3 explains the double-bracket [[:name]] syntax from this lesson's fix, and lesson 4 covers multiple root keys and destructuring. Lesson 5 covers expect! and a surprising detail of Rails' internals, read from the installed source. Lesson 6 migrates one of this app's require/permit controllers to expect and checks that the output is identical.
Keep going with lesson 2
One call, both jobs: this app's params.expect line
Reads this app's course_params line and Rails' source, showing expect is built on permit_filters and require.
A free account also gives you the quizzes, your progress and notes, and the discussion.