Happy Thursday!
all posts

Be careful with params.merge()

Published on Mar 16, 2014

Sven Pachnit GitHub Twitter StackOverflow

I've seen a lot of people doing it and I used to do it too, use params.merge(…) in link_to or redirect_to calls. You do this because you want to change one ore more parameters (like the locale, current page or search query) but leave the rest as it is.

But heads up! There is an issue with this solution:

link_to("Page 2", params.merge(page: 2)) # /users?page=2
link_to("parlez vous français?", params.merge(locale: "fr")) # /fr/about

What do you think would this request url do to the second link? Hint: You won't get a french customer anymore :)

http://localhost:3000/en/about?%68%6f%73%74=%77%77%77%2e%35%30%72%65%61%73%6f%6e%73%74%6f%68%61%74%65%74%68%65%66%72%65%6e%63%68%2e%63%6f%6d%2f%23&%70%6f%72%74=%38%30

equals

http://localhost:3000/en/about?host=www.50reasonstohatethefrench.com/%23&port=80

which results in you link target being

http://www.50reasonstohatethefrench.com/#/fr/about

There is also protocol you could tamper with. So if you have french customers you might want to know how to get out of this misery.

Usually it is the best to explicitly define which parameters you want and strip the others away but in some cases it is really annoying or just impossible. You should be on the safe side if you pass only_path: true to the url_for method. This will ensure that you just get the path instead of the full URL. The three parameters protocol, host and port are getting stripped away entirely.

link_to("parlez vous français?", params.merge(locale: "fr", only_path: true)) # /fr/about

This can get a bit cumbersome so if you need this multiple times you might want to add this to your application controller:

  # safe url parameters to use instead of params.merge
  def safe_params unsafe = {}
    params.merge(unsafe).merge(only_path: true, script_name: nil)
  end
  helper_method :safe_params

This way you can save a bit on the link_to side:

link_to("parlez vous français?", safe_params(locale: "fr"))

And don't think you can solve this with default_url_params as you can also pass this via the URL!

Rails escapes the parameters good enough that you won't directly run into XSS issues here but there are exceptions in regard to javascript: or data: links. So if you link to a user defined webpage, be sure to blacklist or better whitelist and enforce the allowed protocols.

Update: script_name

There is another parameter which can alter the URL and is therefore dangerous to ignore. The popular pagination gem will_paginate ran into this problem as well. The safe_params helper was modified accordingly.