O bug que o params.expect existe para corrigir
O curso para o qual Parâmetros e segurança apontou
Parâmetros e segurança terminou deixando o expect de lado: como require e permit funcionam sozinhos, e como a chamada única do expect substitui os dois, ficou para um curso próprio, este. Tudo o que aquele curso ensinou sobre require e permit continua valendo. Este curso é sobre o expect em si.
O crash contra o qual require/permit não protegem
Um atacante manda { catalog_course: "hack" }, uma string simples onde o Admin::Catalog::CoursesController deste app espera um hash aninhado:
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) retorna a string "hack", porque uma string está presente e não está em branco, e é só isso que o .require confere. .permit, chamado em seguida, não existe em String. O resultado é um NoMethodError não tratado, não um 400 limpo: um dos erros 500 provocados pelo usuário contra os quais o comentário de documentação do expect diz proteger.
Uma versão mais silenciosa e mais perigosa do mesmo 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, não um Array
pets: foi declarado como um array de hashes, pets: [:name], e o atacante mandou um único hash. Desta vez o .permit não quebra. Ele retorna result[:pets] como um objeto Parameters solto, em vez do array que o código adiante espera. Não há exceção nem linha de log. Um código que supõe result[:pets].each { |p| ... } levanta um NoMethodError confuso no .each em algum ponto depois. Pior, um código que lê result[:pets][:name] recebe o que o atacante mandou e funciona, enquanto está errado sobre o que foi validado.
Os mesmos dois payloads, pelo 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
O primeiro caso levanta um ParameterMissing limpo em vez de um crash não tratado; o segundo retorna nil em vez do formato errado. O expect confere o formato do que recebe, além de conferir se uma chave está presente, e é por isso que o admin/catalog/courses_controller.rb deste app o usa.
O que este curso cobre
Sete aulas, cada uma rodada contra objetos ActionController::Parameters. Esta aula trata do bug e da correção. A aula 2 trata da forma de uma chamada só do expect, com a linha deste app por inteiro. A aula 3 explica a sintaxe de colchete duplo [[:name]] da correção desta aula, e a aula 4 trata de várias chaves raiz e desestruturação. A aula 5 trata do expect! e de um detalhe surpreendente dos internos do Rails, lido no código-fonte instalado. A aula 6 migra um dos controllers require/permit deste app para expect e confere que a saída é idêntica.
Continue com a aula 2
Uma chamada, dois trabalhos: a linha params.expect deste app
Lê a linha course_params deste app e o código-fonte do Rails, mostrando que o expect usa permit_filters e require.
Com uma conta grátis você também tem os quizzes, seu progresso e suas anotações, e a discussão.