Combining two related fields in a url to prevent tampering
Usually when you select an item from a search results list you only need to pass the id in the url and that's enough to lookup the rest of the details when processing the response. Recently I had a case where I couldn't lookup the item from the id because it wasn't in my database. I'm going to describe how I passed several pieces of information in a single url and what I did to ensure users could not be manipulate them independently.
My application was searching and displaying a list of companies then keeping track of which one the user selects.
When someone clicks on a company name it would save the company's id and display a nice message "Thanks for selecting Google Inc." Since the companies came from a slow external service I didn't want to call again, I passed both the id and name in the url.
I built a couple of simple haml files to show my company list. Each row had a link to display the company name and passed both the name and id in the url.
The controller code was also pretty straightforward.
This worked great and worked, but then we started looking at the urls in our browser and noticed they looked like
Hmm ... What would happen if someone changed the name in the url? We tried loading
http://my.site.com/companies/60902413&name=Some+Silly+Name. Uh-oh, our database now stored the selected company Some Silly Name with id #60902413. This could be confusing or worse a security risk where a clever (or malicious) user could store inaccurate information in our database.
Our solution was to combine the two fields into a single query parameter that was resistant to user tampering and is passed as a single unit. Fortunately Rails passes the session back and forth in a cookie with just that requirement. The session is a hash of many different key-value pairs that need to be encoded as a single cookie, it also contains sensitive information that should be resistant to tampering (and unreadable) and, most importantly it turns out to be something we could reuse.
Starting from the outside in what we wanted to do was rewrite our
_company.html.haml partial view to put the single encrypted form of the company on the url.
and parse that in the controller
Ok, but what do those to_encrypted_s and from_encrypted_s methods do?
I still haven't told you how they work, we're just working down from the outside-in figuring out what other classes we'll need. So what does the Encryption class look like?
We make use of the ActiveSupport::MessageVerifier class which as the documentation says "makes it easy to generate and verify messages which are signed to prevent tampering".
Now when we select a company we get a url with a crazy long id like http://my.site.com/company/BAh7BzoJZHVuc2kDE%2BAUOhFjb21wYW55X25hbWUiMEludGVybmF0aW9uYWwgQnVzaW5lc3MgTWFjaGluZXMgQ29ycG9yYXRpb24%3D--9027b2c449c0b4a1aea375cb1722fa9db8e56066. If someone were to try and change the really long id in the url the application will throw an ActiveSupport::MessageVerifier::InvalidSignature exception instead of saving bad data.
We've given up readability in the url but in exchange we're guaranteed the id and name we get in the controller go with each other.