使用Rails 3.2.11和RSpec POST原始JSON数据

46

为了确保我的应用程序不容易受到这个漏洞的攻击,我正在尝试创建一个控制器测试来覆盖它。为此,我需要能够发布原始的JSON数据,但我似乎没有找到一种方法来实现。经过一些研究,我已经确定至少曾经可以使用RAW_POST_DATA头部来实现,但现在似乎已经行不通了:

it "should not be exploitable by using an integer token value" do
  request.env["CONTENT_TYPE"] = "application/json"
  request.env["RAW_POST_DATA"]  = { token: 0 }.to_json
  post :reset_password
end

当我查看参数哈希表时,token根本没有被设置,它只包含{"controller" => "user", "action" => "reset_password"}。无论是尝试使用XML还是仅使用常规的POST数据,在所有情况下似乎都没有设置它。

我知道最近Rails存在漏洞,参数哈希表的加密方式已经更改,但是否仍有办法通过RSpec发布原始数据?我是否可以直接使用Rack::Test::Methods


截至Rails 4.2.6,我在RSpec控制器规范中设置request.env["RAW_POST_DATA"]是有效的。 - Simon Woodside
7个回答

84

据我所知,在控制器规范中发送原始的POST数据已不再可能。然而,在请求规范中可以轻松完成此操作:

describe "Example", :type => :request do
  params = { token: 0 }
  post "/user/reset_password", params.to_json, { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
  #=> params contains { "controller" => "user", "action" => "reset_password", "token" => 0 }
end

1
这是我发现的最干净的测试控制器期望原始JSON POST请求的方法。谢谢。 - sockmonk
1
'CONTENT_TYPE' 头部已足够 - Sergii Mostovyi
15
在Rails 3.2.13中,这个解决方案对我不起作用。我的解决方法是编写params = { token: 0, format: :json }。同时在示例中删除.to_json和其后面的哈希表。另外,您可能需要在spec_helper.rb中包含config.include Rails.application.routes.url_helpers。使用response.header['Content-Type'].should include 'application/json'来验证JSON响应。 - davidtingsu
3
我这样做的:在Rails 3.2.14中,使用post "/user/reset_password",并合并params.merge(format: 'json')。 - untidyhair
1
在Rails 5中,尝试使用as: :json而不是format: :json来转换有效载荷。 - nicb
显示剩余5条评论

11
我们在控制器测试中所做的是明确设置RAW_POST_DATA:
before do
  @request.env['RAW_POST_DATA'] = payload.to_json
  post :my_action
end

如果你正在构建一个需要这种设置的库,这是使用符合Rails 3、4和5语法的唯一方法来获得这种行为。 - Ryan McGeary

11

以下是将原始 JSON 发送到控制器操作(Rails 3+)的方法:

假设我们有这样一个路由:

post "/users/:username/posts" => "posts#create"

并且假设您期望请求正文是一个 JSON,您可以通过以下方式读取它:

JSON.parse(request.body.read)

那么您的测试应该像这样:

it "should create a post from a json body" do
  json_payload = '{"message": "My opinion is very important"}'
  post :create, json_payload, {format: 'json', username: "larry" }
end
{format: 'json'} 是实现这一切的魔法。此外,如果我们查看TestCase#post的源代码http://api.rubyonrails.org/classes/ActionController/TestCase/Behavior.html#method-i-process,您会发现它将动作后的第一个参数(json_payload)设置为原始发布正文(如果它是字符串),并将其余参数解析为常规参数。
另外需要指出的是rspec只是Rails测试框架中的DSL。上面的post方法是ActionController :: TestCase#post,而不是某种rspec发明。

太棒了,我已经挖掘了相当长的时间来寻找这个确切的答案。 - iturgeon
奇怪的事情发生了,这个在一个文件上可以工作但在另一个文件上却不能。我总是遇到一些奇怪的问题。 :) - Yakob Ubaidi

10

Rails 5 示例:

RSpec.describe "Sessions responds to JSON", :type => :request do

  scenario 'with correct authentication' do
    params = {id: 1, format: :json}
    post "/users/sign_in", params: params.to_json, headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
    expect(response.header['Content-Type']).to include 'application/json'
  end
end

6
这里有一个完整的控制器测试示例,发送原始JSON数据:
describe UsersController, :type => :controller do

  describe "#update" do
    context 'when resource is found' do
      before(:each) do
        @user = FactoryGirl.create(:user)
      end

      it 'updates the resource with valid data' do
        @request.headers['Content-Type'] = 'application/vnd.api+json'
        old_email = @user.email
        new_email = Faker::Internet.email
        jsondata = 
        {
          "data" => {
            "type" => "users",
            "id" => @user.id,
            "attributes" => {
              "email" => new_email
            }
          }
        }

        patch :update, jsondata.to_json, jsondata.merge({:id => old_id})

        expect(response.status).to eq(200)
        json_response = JSON.parse(response.body)
        expect(json_response['data']['id']).to eq(@user.id)
        expect(json_response['data']['attributes']['email']).to eq(new_email)
      end
    end
  end
end

重要部分如下:

@request.headers['Content-Type'] = 'application/vnd.api+json'

并且

patch :update, jsondata.to_json, jsondata.merge({:id => old_id})

第一部分确保您的请求正确设置了内容类型,这非常简单。

第二部分让我头疼了几个小时,我的初始方法有很大不同,但事实证明存在一个Rails bug,它阻止我们在功能测试中发送原始POST数据(但允许在集成测试中),这是一个丑陋的解决方法,但它有效(在rails 4.1.8和rspec-rails 3.0.0上)。


0

关于Rails 4:

params = { shop: { shop_id: new_subscrip.shop.id } }
post api_v1_shop_stats_path, params.to_json, { 'CONTENT_TYPE' => 'application/json',
                                                     'ACCEPT' => 'application/json' }

0
一个与@daniel-vandersluis答案略有不同的选择,在rails 3.0.6中,使用rspec 2.99和rspec-rails 2.99:
describe "Example", :type => :request do
  params = { token: 0 }
  post "/user/reset_password", params.merge({format: 'json'}).to_json, { 'CONTENT_TYPE' => 'application/json', 'HTTP_ACCEPT' => 'application/json' }
end

HTTP_ACCEPT头部并没有太大的区别,(可以是HTTP_ACCEPT或者只是ACCEPT)。但在我的情况下,为了使其工作,参数必须:具有.merge({format: 'json'}).to_json

另一种变化:

describe "Example", :type => :request do
  params = { token: 0 }
  post "/user/reset_password", params.merge({format: 'json'}).to_json, { 'CONTENT_TYPE' => Mime::JSON.to_s, 'HTTP_ACCEPT' => Mime::JSON }
end

它使用 Mime::JSONMime::JSON.to_s 替代 application/json 作为头部值。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接