Forum: RSpec Testing arbitrary post action parameters

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
171ea139761951336b844e708d1547ab?d=identicon&s=25 James Byrne (byrnejb)
on 2009-01-16 19:00
I am working on our (newly renamed) authentication feature.  The current
scenario is:

  Scenario: Non-administrators should not set administrator ability
    Given I have no users
      And I add a user named "admin" as an administrator
      And I add a user named "myuser" as not an administrator
    When the user named "myuser" authenticates
      And the user enables the administrator role
    Then the user named "myuser" should not be an administrator

Now, what I am looking for is an example of how an authenticated user
would craft a post request in their browser to set the
user.administrator flag to true.

Crafting these sorts of http requests may be obvious and simple to some
of you, but I have no clue how this is done.

On some lists, asking questions on how to breach security are themselves
a breach of list etiquette.  If this is the case here then I ask your
indulgence and the favour of a private reply if that is deemed more
suitable.  I do require the information though, since I have to defend
against it.
171ea139761951336b844e708d1547ab?d=identicon&s=25 James Byrne (byrnejb)
on 2009-01-16 20:17
James Byrne wrote:

>
> Now, what I am looking for is an example of how an authenticated user
> would craft a post request in their browser to set the
> user.administrator flag to true.
>

OK, I figured out how to do this, as an authenticated user, from the
browser.  I am now going to try the syntax as a string in visits and see
if that triggers the problem.
42172acdf3c6046f84d644cb0b94642c?d=identicon&s=25 Pat Maddox (pergesu)
on 2009-01-16 20:20
(Received via mailing list)
On Fri, Jan 16, 2009 at 10:00 AM, James Byrne <lists@ruby-forum.com>
wrote:
>
> Now, what I am looking for is an example of how an authenticated user
> would craft a post request in their browser to set the
> user.administrator flag to true.
>
> Crafting these sorts of http requests may be obvious and simple to some
> of you, but I have no clue how this is done.

Well, do you have a "set administrator" button?  Use webrat to click
it if you do.

I assume you don't though, cause that'd be kinda weird.  How about
passing it in the POST params:

put users_url(user), :user => {:administrator => true}

Something along those lines...


> On some lists, asking questions on how to breach security are themselves
> a breach of list etiquette.  If this is the case here then I ask your
> indulgence and the favour of a private reply if that is deemed more
> suitable.  I do require the information though, since I have to defend
> against it.

Asking how to test a security feature that you're building is very
different from asking how to hack somebody's site :)

Pat
171ea139761951336b844e708d1547ab?d=identicon&s=25 James Byrne (byrnejb)
on 2009-01-16 21:25
Pat Maddox wrote:

> I assume you don't though, cause that'd be kinda weird.  How about
> passing it in the POST params:
>
> put users_url(user), :user => {:administrator => true}
>
> Something along those lines...

That is the problem, I am not sure what syntax to use int the step
definition. I tried this:

  visits "#{edit_user_path}?user[administrator]=1"

Which produces the same type of url that the RoR security guide uses in
its examples:

http://www.example.com/user/signup?user[name]=ow3n...

Whereas I generate

 HTTP headers
{"HTTP_REFERER"=>"http://www.example.com/account/edit?user[administr...

But this URL attack does not seem to work as advertised.  The key
"administrator" does not make it into the params hash:

200 OK [http://www.example.com/account/edit?user[administrator]=1]
REQUESTING PAGE: POST /account with {
"user"=>{
 "name_middle"=>"Middle-myuser",
 "password_confirmation"=>"",
 "username"=>"myuser",
 "password"=>"",
 "email"=>"myuser@example.com",
 "name_first"=>"First-myuser",
 "name_last"=>"Last-myuser"},
 "commit"=>"Update",
 "_method"=>"put"}

I realize this is a silly thing to ask, but how do you do this for
testing?
F86901feca747abbb5c6c020362ef2e7?d=identicon&s=25 Zach Dennis (zdennis)
on 2009-01-16 22:38
(Received via mailing list)
In the past we've done the following:

Story: Users without hierarchy manager role accessing the hierarchy

  In order to ensure users that shouldn't have access to the hierarchy
don't
  As a user who isn't a hierarchy manager
  I should not be able to access the hierarchy

  Scenario: Non hierarchy manager attempting to access locations
    Given I've log in as a user without the 'hierarchy manager' role
    When I try to GET /locations
    Then I am notified that I do not have access to that

    More Examples:
    | role              | request_method | path          |
    | hierarchy manager | POST           | /locations    |
    | hierarchy manager | PUT             | /locations/1  |
    | hierarchy manager | DELETE       | /locations/1  |
    etc ....

The "Then" step ensures that the user is redirected to an access
denied page.  Granted, this doesn't go the granularity you may be
trying to get at, but knowing you aren't actually getting through to
the underlying action (by being redirected to the access denied page)
has worked well for me,

Zach

On Fri, Jan 16, 2009 at 3:25 PM, James Byrne <lists@ruby-forum.com>
wrote:
> definition. I tried this:
>  HTTP headers
>  "username"=>"myuser",
> --
> Posted via http://www.ruby-forum.com/.
> _______________________________________________
> rspec-users mailing list
> rspec-users@rubyforge.org
> http://rubyforge.org/mailman/listinfo/rspec-users
>



--
Zach Dennis
http://www.continuousthinking.com
http://www.mutuallyhuman.com
171ea139761951336b844e708d1547ab?d=identicon&s=25 James Byrne (byrnejb)
on 2009-01-16 22:58
Zach Dennis wrote:
>
>
> The "Then" step ensures that the user is redirected to an access
> denied page.  Granted, this doesn't go the granularity you may be
> trying to get at, but knowing you aren't actually getting through to
> the underlying action (by being redirected to the access denied page)
> has worked well for me,


I am already testing for that.  What I am trying to accomplish now is to
find a malevolently crafted URL that will trigger the
users_controller/update action with arbitrary contents in the params
hash.  Once I have one that "works" then we will code the
model/controller to prevent it.

I have gotten to the point where I believe that the url has to look
somewhat like this:

   http://www.example.com/users/2/<some_action>?user[...

Where some_action is one of account, edit, update or nothing. I cannot
yet determine which is the case.

Regardless, I cannot seem to find a way to push this to the controller
as a POST, which is apparently what the controller needs, from either a
step definition or a browser.
42172acdf3c6046f84d644cb0b94642c?d=identicon&s=25 Pat Maddox (pergesu)
on 2009-01-17 00:21
(Received via mailing list)
On Fri, Jan 16, 2009 at 12:25 PM, James Byrne <lists@ruby-forum.com>
wrote:
> definition.
Does
put users_url(user), :user => {:administrator => true}
not work?

Also, I would probably be more likely to test this in a controller
spec (where I know for sure the line I pasted will work), but I
totally understand the desire to put it in an acceptance test.

Pat
171ea139761951336b844e708d1547ab?d=identicon&s=25 James Byrne (byrnejb)
on 2009-01-17 23:51
Pat Maddox wrote:

> Does
> put users_url(user), :user => {:administrator => true}
> not work?
>

I have spent a good deal of time trying to get this to work with no
success.  I have read the api regarding the put method in:

 ActionController::Integration::Session

which is what I believe provides the put method in the example, but I
cannot seem to get the signature to work out.

   put(path, parameters = nil, headers = nil)

If the path argument is "users_url(user)" then what is user?  The Rails
documentation describes the *_url methods thus:

> Behind the scenes the *_url method wraps a call to url_for on the route hash.
>
> The *_url method takes a single optional argument which is a hash that
> is merged on top of the url_for’ed route. This makes it so that a given
> named route can be parametrized when used with redirect_to, link_to, etc.

If I read this aright then in your example user should be a hash, but a
hash of what? Where would I create it in my test and what should it
contain?
171ea139761951336b844e708d1547ab?d=identicon&s=25 James Byrne (byrnejb)
on 2009-01-18 00:34
James Byrne wrote:

>
> I have spent a good deal of time trying to get this to work with no
> success.  I have read the api regarding the put method in:
>
>  ActionController::Integration::Session
>
> If I read this aright then in your example user should be a hash, but a
> hash of what? Where would I create it in my test and what should it
> contain?

I ended up with this:

 :user => User.find(:first)

When /user named "(.*)" enables the administrator role/ do |name|

 put users_url( :user => User.find_by_username!(name) ), :user =>
{:administrator => true}

end

Which at least does not blow up on me.  But it does not set the
administrator flag to true either.
171ea139761951336b844e708d1547ab?d=identicon&s=25 James Byrne (byrnejb)
on 2009-01-18 00:38
James Byrne wrote:

>
> When /user named "(.*)" enables the administrator role/ do |name|
>
>  put users_url( :user => User.find_by_username!(name) ), :user =>
> {:administrator => true}
>
> end
>
> Which at least does not blow up on me.  But it does not set the
> administrator flag to true either.

Arrrgh... I seem to anticipated this type of thing:


#user.rb
class User < ActiveRecord::Base
...
  def administrator=(setting=false)
  end
...


Sigh....
171ea139761951336b844e708d1547ab?d=identicon&s=25 James Byrne (byrnejb)
on 2009-01-18 00:49
I still cannot get this to work.  Tracing the actual calls in the log I
see this:


Processing UsersController#index (for 127.0.0.1 at 2009-01-17 18:32:54)
[PUT]
  Parameters: {"action"=>"index", "controller"=>"users", "user"=>"2"}
  User Load (16.0ms)   SELECT * FROM "users"
Rendering template within layouts/application
Rendering users/index
  SQL (0.0ms)   SELECT count(*) AS count_all FROM "users" WHERE
(last_request_at > '2009-01-17 18:22:54')
  User Load (0.0ms)   SELECT * FROM "users" WHERE ("users"."id" = 1)
LIMIT 1
  User Update (0.0ms)   UPDATE "users" SET "updated_at" = '2009-01-17
23:32:54', "last_request_at" = '2009-01-17 18:32:54' WHERE "id" = 1
Completed in 110ms (View: 94, DB: 16) | 200 OK
[http://www.example.com/users?user=2]
  User Load (0.0ms)   SELECT * FROM "users" WHERE ("users"."username" =
'myuser')

This is my users_controller/update action:

  # PUT /users/1
  # PUT /users/1.xml
  def update
    @user = @current_user
    if @user.update_attributes(params[:user])
      flash[:notice] = 'User Account Changes Saved'
      redirect_to account_url
    else
        render :action => "edit"
    end
  end
Cdf378de2284d8acf137122e541caa28?d=identicon&s=25 Matt Wynne (mattwynne)
on 2009-01-18 01:03
(Received via mailing list)
On 17 Jan 2009, at 23:34, James Byrne wrote:

>> hash of what? Where would I create it in my test and what should it
>
> end
>
> Which at least does not blow up on me.  But it does not set the
> administrator flag to true either.

If you look at the Rails documentation for the named route methods, I
think you might want to try something a bit more like this:

     user = User.find_by_username!(name)
     put users_url(user, :user => {:administrator => true})

If you think you know the URL you want, why not try hard-coding it for
a while in the step until you get the desired effect on the
controller? Then you can add a spec for your routing which proves that
the call to users_url() with the parameters you're trying gives you
the same hard-coded value. Then you'll have discovered for yourself
exactly the right parameters to use to send the right PUT request all
the way to the controller.

Make sense?

Matt Wynne
http://blog.mattwynne.net
http://www.songkick.com
42172acdf3c6046f84d644cb0b94642c?d=identicon&s=25 Pat Maddox (pergesu)
on 2009-01-18 01:29
(Received via mailing list)
On Sat, Jan 17, 2009 at 3:34 PM, James Byrne <lists@ruby-forum.com>
wrote:
>> contain?
> end
>
> Which at least does not blow up on me.  But it does not set the
> administrator flag to true either.

Okay well it's been a little frustrating watching this thread, after
I've made suggestions and I don't think you've tried them.  So, I
decided to verify this myself and I made a little demo app to see if
it works...

put user_url(@user), :user => {:admin => true}

does in fact work.  So, yeah.  Last post I'm making in this thread.

Also, to easily get the behavior you want, you can do

class User < ActiveRecord::Base
  attr_protected :admin
end

which will ignore the admin key when bulk-updating attributes (such as
using update_attributes).

Pat
171ea139761951336b844e708d1547ab?d=identicon&s=25 James Byrne (byrnejb)
on 2009-01-18 03:47
Matt Wynne wrote:

>
> If you look at the Rails documentation for the named route methods, I
> think you might want to try something a bit more like this:
>
>      user = User.find_by_username!(name)
>      put users_url(user, :user => {:administrator => true})
>

Funnily enough, I tried this exact thing before I ended up with

 put users_url( :user => User.find_by_username!(name) ), :user =>
{:administrator => true}


When I used the form that Pat provided
  put users_url(user), :user => {:administrator => true}
then I saw this:

    And the user named "myuser" enables the administrator role       #
features/
app/models/users/step_definitions/user_steps.rb:28
      You have a nil object when you didn't expect it!
      The error occurred while evaluating nil.to_sym (NoMethodError)
      /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/routing
/route.rb:243:in `extra_keys'
      /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/routing
/route.rb:243:in `map'
      /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/routing
/route.rb:243:in `extra_keys'
      generated code
(/usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_co
ntroller/routing/route.rb:154):3:in `generate'
      /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/routing
/route_set.rb:339:in `generate'
      /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/url_rew
riter.rb:208:in `rewrite_path'
      /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/url_rew
riter.rb:187:in `rewrite_url'
      /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/url_rew
riter.rb:165:in `rewrite'
      /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/base.rb
:626:in `url_for'
      /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/integra
tion.rb:227:in `url_for'
      (eval):18:in `users_url'
      /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/integra
tion.rb:498:in `__send__'
      /usr/lib/ruby/gems/1.8/gems/actionpack-2.2.2/lib/action_controller/integra
tion.rb:498:in `method_missing'
      ./features/app/models/users/step_definitions/user_steps.rb:30:in
`And /use
r named "(.*)" enables the administrator role/'
      features/app/models/users/user.feature:44:in `And the user named
"myuser"
enables the administrator role'

If I removed the enclosing parenthesis around my_user then I saw the
same thing.


> If you think you know the URL you want, why not try hard-coding it for
> a while in the step until you get the desired effect on the
> controller? Then you can add a spec for your routing which proves that
> the call to users_url() with the parameters you're trying gives you
> the same hard-coded value. Then you'll have discovered for yourself
> exactly the right parameters to use to send the right PUT request all
> the way to the controller.
>
> Make sense?

Yes, and in rough form this is what I ended up doing.  The step
definition I ended up with appears to be the correct method signature
and it is equivalent to the example you provide.  It seems that the
first parameter to _put_ must be a hash.

Nonetheless, I cannot seem to duplicate the exact effect of an update
via the /account action (yes, I have repeated the test with put
account_url(... and obtained the same results as trying put
users_url(... )
171ea139761951336b844e708d1547ab?d=identicon&s=25 James Byrne (byrnejb)
on 2009-01-18 05:20
Matt Wynne wrote:

> If you think you know the URL you want, why not try hard-coding it for
> a while in the step until you get the desired effect on the
> controller? Then you can add a spec for your routing which proves that
> the call to users_url() with the parameters you're trying gives you
> the same hard-coded value. Then you'll have discovered for yourself
> exactly the right parameters to use to send the right PUT request all
> the way to the controller.


I took your advice and, after a very long session of trial and error
(mostly error), I derived this formulation:

  post account_url, { :user=> { "administrator" => true }, :action =>
"update", :commit => "Update", :_method => "put" }

Now, this has the following effect in the tests:

> Processing UsersController#update (for 127.0.0.1 at 2009-01-17 22:58:54)
> [PUT]
> Parameters: {"commit"=>"Update", "_method"=>"put", "action"=>"update",
> "controller"=>"users", "user"=>{"administrator"=>"true"}}

That seems to be what I was trying to pass.

The stuff that follows next is authlogic updating the last_request_at
field in the users table:

> User Load (0.0ms)   SELECT * FROM "users" WHERE ("users"."id" = 3) LIMIT 1
> User Update (0.0ms)   UPDATE "users" SET "updated_at" = '2009-01-18 03:58:54',
> "last_request_at" = '2009-01-17 22:58:54' WHERE "id" = 3
> User Exists (0.0ms)   SELECT "users".id FROM "users" WHERE (
> "users"."perishable_token" = 'TuHra3ozvoDVJLEtFQIX' AND "users".id <> 3)
> LIMIT 1
> User Exists (0.0ms)   SELECT "users".id FROM "users" WHERE (
> "users"."username" = 'myuser' AND "users".id <> 3) LIMIT 1
> User Exists (0.0ms)   SELECT "users".id FROM "users" WHERE (
> "users"."email" = 'myuser@example.com' AND "users".id <> 3) LIMIT 1

This is the magic SQL statement I was trying to produce:

> User Update (0.0ms)   UPDATE "users" SET "perishable_token" =
> 'TuHra3ozvoDVJLEtFQIX', "administrator" = 't' WHERE "id" = 3

This next bit seems to be right too:

> Redirected to http://www.example.com/account
Completed in 0ms (DB: 0) | 302 Found [http://www.example.com/account]
  User Load (0.0ms)   SELECT * FROM "users" WHERE ("users"."username" =
'myuser'
) LIMIT 1

AND... The next feature step fails as desired:


When /user named "(.*)" should not be an administrator/ do |name|
  my_user = User.find_by_username!(name)
  fail if my_user.administrator
end

    Then the user named "myuser" should not be an administrator      #
features/
app/models/users/step_definitions/user_steps.rb:41
       (RuntimeError)
      ./features/app/models/users/step_definitions/user_steps.rb:43:in
`Then /us
er named "(.*)" should not be an administrator/'
      features/app/models/users/user.feature:45:in `Then the user named
"myuser"
 should not be an administrator'

So, I now have my failing test and can write (uncomment) the code needed
to protect against it.

Thanks for the help.  It was extremely valuable in leading me to the
right answer.
171ea139761951336b844e708d1547ab?d=identicon&s=25 James Byrne (byrnejb)
on 2009-01-18 06:02
James Byrne wrote:
>
  It seems that the first parameter to _put_ must be a hash.

Which is completely wrong of course, the first argument to _put_ must be
a path.  I confused the argument to the named route method with that of
the put method.  It is the argument to the named route that must be a
hash as far as I can determine from experimentation.
42172acdf3c6046f84d644cb0b94642c?d=identicon&s=25 Pat Maddox (pergesu)
on 2009-01-18 09:32
(Received via mailing list)
On Sat, Jan 17, 2009 at 6:47 PM, James Byrne <lists@ruby-forum.com>
wrote:
> When I used the form that Pat provided
>  put users_url(user), :user => {:administrator => true}

The reason this didn't work originally is because it was supposed to
be user_url, not users_url.  My most recent post had the fix, but I
forgot to highlight that the earlier post was incorrect.

Pat
This topic is locked and can not be replied to.