Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Security Fixes #1926

Merged
merged 4 commits into from Oct 4, 2013
Merged

Security Fixes #1926

merged 4 commits into from Oct 4, 2013

Conversation

seanlinsley
Copy link
Contributor

Until now we've been passing the entire params hash to url_for, which calls symbolize_keys on the hash passed in. The result is that an attacker can DOS your website by passing arbitrary strings as parameters. An example being:

victim.com/somewhere?really_long_malicious_key

Symbols never leave memory (until app restart), so this attack vector is quite effective against Ruby apps.

The good news is, only registered users are able to exploit this vulnerability.

I think this whole adventure was pretty cool. ✨ If you want to try it out yourself, here's some tools.

@seanlinsley
Copy link
Contributor Author

I just broke up my monster commit into three that each have their own general theme, so it's easier to tell what's going on.

TODO: add tests that fail if arbitrary params are symbolized

@seanlinsley
Copy link
Contributor Author

I tried the below method to add tests:

describe Admin::PostsController, :type => :controller do
  render_views
  it "should not leak on index page" do
    get :index, 'really_long_malicious_key' => nil
    Symbol.all_symbols.map(&:to_s).should_not include('really_long_malicious_key'), 'oh noes!'
  end
end

But it kept failing even when switching to this feature branch... The assumption being that any params passed to get are symbolized before it even hits the application. I'll have to try another method later... maybe bypassing the controller and interacting directly with the view layer?

@template.url_for params.merge(@param_name => (page <= 1 ? nil : page))
end
end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gregbell, @macfanatic, thoughts?

A lot of our symbol leakage is directly caused by url_for... I don't understand why it needs to symbolize keys just to build a URL.

@gregbell
Copy link
Contributor

gregbell commented Apr 3, 2013

@daxter any update on the status of this PR?

Overall I'm concerned with the whitelisting of the params. It means that it is impossible to add additional features without modifying the code. For example, if I add some custom filtering to my application that runs on a different param, the pagination work work anymore.

Now with that being said, if there is a security or DOS attack vulnerability I am all for plugging the wholes. But it seems like the issue is more with url_for than it is with AA.

@seanlinsley
Copy link
Contributor Author

That seems to be the case. I haven't tried bugging the Rails team as of yet; it's astounding that url_for is so naive.

@macfanatic
Copy link
Contributor

I'm closing the issue in favor of fixing or submitting the issue to Rails, instead of worrying about this in AA since it's not AA specific.

@daxter - Will you post with the Rails team?

@macfanatic macfanatic closed this Apr 4, 2013
@seanlinsley seanlinsley reopened this Apr 4, 2013
@seanlinsley
Copy link
Contributor Author

I will open a ticket with the Rails team, but this shouldn't be closed as the underlying problem hasn't been resolved.

@seanlinsley
Copy link
Contributor Author

Haha, looks like we're just using url_for incorrectly. From the console:

include Rails.application.routes.url_helpers
url_for action: 'index', controller: 'employees', host: 'foo.bar', params: {'eee' => 3}
# => "http://foo.bar/employees?eee=3"
Symbol.all_symbols.map(&:to_s).include? 'eee'
# => false

I'm going to re-make this PR so that user parameters are namespaced properly under the :params key.

@macfanatic
Copy link
Contributor

Is that in a particular area of the app, or scattered throughout you think?

@seanlinsley
Copy link
Contributor Author

The issue exists anywhere we build links that include the params hash. So it's specific to the view layer.

Don't actually have the tests passing yet, so we'll see.

view.request.stub!(:query_parameters).and_return({:controller => 'admin/posts', :action => 'index', :page => '1'})
view.controller.params = {:controller => 'admin/posts', :action => 'index'}
view.request.stub!(:query_parameters).and_return :page => '1'
view.request.stub!(:path_parameters).and_return :controller => 'admin/posts', :action => 'index'
view
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to this guide, we were populating the parameters above incorrectly.

9.1.1 path_parameters, query_parameters, and request_parameters

Rails collects all of the parameters sent along with the request in the params hash, whether they are sent as part of the query string or the post body. The request object has three accessors that give you access to these parameters depending on where they came from. The query_parameters hash contains parameters that were sent as part of the query string while the request_parameters hash contains parameters sent as part of the post body. The path_parameters hash contains parameters that were recognized by the routing as being part of the path leading to this particular controller and action.

@seanlinsley
Copy link
Contributor Author

Hmm.. So apparently Rails before 3.2 doesn't support the :params option I'm passing. What to do...

@seanlinsley
Copy link
Contributor Author

After thinking about it for a while, it seems that the best option would be to utilize default_url_options:

# in your initializer
config.safe_params.push :foobar

# Then we'd automatically preserve those parameters in our URLs across web requests
def default_url_options
  params.slice *safe_params # with safe_params being that config option above.
end

@seanlinsley
Copy link
Contributor Author

The benefit of this approach is that default_url_options are automatically included in every url_for call, as well, that developers can register parameters that they want persisted.

The downside is that those parameters could potentially never leave your query string, even when they're no longer applicable. So for example, q[] definitely shouldn't be an auto-included parameter.

@seanlinsley
Copy link
Contributor Author

Okay here we go. You could set the default url options to only be included on specific page actions. So you might want the pagination stuff to persist on links on the index page, but if you're linking to a show page, that doesn't make any sense.

# so you could set it like this
config.safe_params[:index].push :foo # accepting :index, :show, or :all

# with an implementation like this
safe_params.slice(:all, params[:action]).values.flatten # the keys that should be included

@seanlinsley
Copy link
Contributor Author

Comments @gregbell?

@seanlinsley
Copy link
Contributor Author

Of the two options I suggested above, I'm going with the first one. YAGNI and all that.

This approach seems to solve the problem of symbol leakage without removing any functionality from Active Admin.

@seanlinsley
Copy link
Contributor Author

Okay, everything should be working now 🎉

@@ -72,6 +72,7 @@ def mock_action_view(assigns = {})
ActionView::Base.send :include, ActionView::Helpers
ActionView::Base.send :include, ActiveAdmin::ViewHelpers
ActionView::Base.send :include, Rails.application.routes.url_helpers
ActionView::Base.send :include, Module.new{ def safe_params; {}; end }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^^ dirty hack ^^

@seanlinsley
Copy link
Contributor Author

MetaSearch tries too hard to be dynamic. That's a problem because all of these symbolize the string given:

respond_to?
defined?
send

The only real solution is to check if there's a DB attribute (or predefined search method) for each q[] parameter.

I imagine Ransack has the very same problem.

@seanlinsley
Copy link
Contributor Author

Looking at this again, I almost want to remove all the safe_params stuff and let the developer define default_url_options as they see fit. Especially once #1935 is pulled in, that would just make sense.

class MetaSearch::Builder
def assign_attributes(opts)
opts.each_pair do |k, v|
send "#{k}=", v if base.column_names.include? k[/(.+)_.+\z/, 1]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually won't work in all situations.

:name_contains # works
:name_starts_with # breaks

@seanlinsley
Copy link
Contributor Author

Nevermind. I just did a local merge of this PR and the metasearch->ransack branch from #1979, commenting out my Metasearch hack, and all of the tests in this PR passed :}

@seanlinsley
Copy link
Contributor Author

So the only question left is whether we want to remove the safe_params stuff in this PR.

@seanlinsley
Copy link
Contributor Author

FYI @gregbell and @macfanatic I'm setting this to the 0.7.0 milestone. My plan is to merge #1979, then update this PR appropriately and merge it in.

@seanlinsley
Copy link
Contributor Author

After rebasing this on master, without the fixes:

$ cucumber features/symbol_leak.feature 
Using the default profile...
............F...................F

(::) failed steps (::)

symbol detected! (RSpec::Expectations::ExpectationNotMetError)
./features/step_definitions/symbol_leak_steps.rb:2:in `/^"(.*?)" shouldn't be a symbol$/'
features/symbol_leak.feature:17:in `Then "really_long_malicious_key1" shouldn't be a symbol'

symbol detected! (RSpec::Expectations::ExpectationNotMetError)
./features/step_definitions/symbol_leak_steps.rb:2:in `/^"(.*?)" shouldn't be a symbol$/'
features/symbol_leak.feature:35:in `Then "really_long_malicious_key5" shouldn't be a symbol'

Failing Scenarios:
cucumber features/symbol_leak.feature:15 # Scenario: The index page doesn't leak
cucumber features/symbol_leak.feature:33 # Scenario: Batch Actions don't leak

6 scenarios (2 failed, 4 passed)
30 steps (2 failed, 28 passed)

@seanlinsley
Copy link
Contributor Author

In case I ever want it (and as an experiment to see if GitHub will hold onto the commits for a long time), this branch was at 8781916 before I rebased & updated it.

url_for normally symbolizes the keys passed to it. This is a problem
for Active Admin, since we don't want to limit extensibility by
whitelisting which query parameters are persisted on the index page. We
get around this problem by passing the query parameters back into new
URL creation, namespaced under the `:params` key. This way they're
persisted, but arbitrary symbols aren't created in memory.
@coveralls
Copy link

Coverage Status

Coverage increased (+0.01%) when pulling dcbfd66 on Daxter:security-refactor into 9f3fe3d on gregbell:master.

@seanlinsley
Copy link
Contributor Author

Finally, a simple solution to this problem.

seanlinsley added a commit that referenced this pull request Oct 4, 2013
@seanlinsley seanlinsley merged commit b869cb1 into activeadmin:master Oct 4, 2013
@seanlinsley seanlinsley deleted the security-refactor branch October 4, 2013 06:59
@seanlinsley seanlinsley mentioned this pull request Oct 20, 2013
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants