Drupal's Bodged AJAX system - Post-DrupalGeddon 2

Published On2018-04-12

I hope you remember DrupalGeddon 2, also formerly known as SA-Core-2018-002. It's now over a couple of weeks, and I came across a PoC posted online, which is a simple Google search away now. The first Drupalgeddon, SA-CORE-2014-005, was used wildly to hack vulnerable sites, but this one was not used wildly as far as I know. Now that there is a PoC online, I'm expecting a lot more attacks spawned, so please update your Drupal sites as fast as possible.

When the security update was released, the fix was to sanitize all GET, POST and cookie variables to strip off their # sign if the key is prefixed with one. Having done quite a lot of work with Drupal's Ajax framework, I did a wild guess where the vulnerability might be:

Form API is a good feature that allowed Drupal to evolve into the highly customizable CMS it is today. You can alter almost any aspect of a Drupal form with a custom module without having to hack the code that builds the form. I vividly remember Larry Garfield's opinion on the Form API that we still use regular arrays to build the forms instead of regular PHP classes.

I'm not satisfied that we still have render arrays. I've made no secret of my dislike for render arrays, including calling them "Drupal's biggest mistake".

In addition to the DrupalGeddon 2, there is a security issue with a module that was included with Drupal 8 (was a contributed module in Drupal 6 and 7) that has a flaw in its Ajax logic that might expose unintended information. I will not discuss that issue here because we are still working on the issue, and there is no official fix for it yet.

With all the security concerns aside, Drupal's Ajax system is not in a good shape, even with the improvements we have made in Drupal 8. Sure, I use Drupal quite a lot in my day to day work (although I have slowed down a bit lately), but I must put forth this post -- specially now with DrupalCon Nashville these days.

Ajax callbacks send rendered HTML back

Open your browser console and inspect that any regular Ajax form sends to the server and receives back. The response is often a JSON object, that the receiving end will work on and make additional chances in. For Drupal, though, this is slightly different. This Ajax feature in fact was called AHAH back in the Drupal 5 era, and nothing much has changed in terms of the overall functionality. Browser sends the form data, and server sends back an JSON object containing a command that would replace a section of a form with the new rendered HTML the server sends back.

Ideally, Ajax calls should be small and fast to provide the best user experience. If you must send form data and receive HTML back, you are only saving a few bytes off the full form. Drupal's Ajax framework is quite easy to abuse, and one might end up with a form that is rather slower than a regular form submission! Read below for all the small tricks Drupal had to do to bodge together an Ajax framework just so its users do not have to write any JavaScript.

Drupal.settings merge issues

Whenever you send an Ajax-powered element to the browser, you also send a bunch of additional variables for Drupal's Ajax-handling JavaScript code to figure out where to send the Ajax response to, and information about the form element itself. Unless you manually clean up the variables, each of the Ajax responses will also send additional data to the Drupal.settings browser variable that might end up the browser using too much memory until the browser tab becomes slow of the tab crashes itself.

Sending all HTML IDs used in the page

Drupal Ajax framework makes the browser send all HTML ID attributes used in the page, just so the backend can figure out which HTML IDs are used in the page and can use unique IDs in the response HTML it is about to send. This was a big issue in one of the sites I helped to fix. If you have an Ajax form that has 100 fields, browser sends _at least_200 variables back to the server: 100 for the form fields, plus 100 HTML IDs of each form field it has. PHP has an internal limit max_input_vars that limits how many variables it allows in a single HTTP request. The default value is 1000, which means if you have a Drupal Ajax form that has more than 500 fields, your form wouldn't work because the Ajax request will hit this limit unless you manually increase this configuration value.

Works even with JavaScript disabled, unless...

This is both a blessing and a curse. If implemented correctly, Ajax forms will continue to work even if the user has JavaScript turned off. This was a great selling point a few years back to progressively enhance user experience. However, if you turn off JavaScript in your browser today (which often requires some advanced configuration updated in browsers), majority of the web sites you use will not work at all. To keep this fallback behavior, Drupal must rebuild the Ajax form, which defeats the server-side advantage of Ajax forms.

From an accessibility point of view, this is a great feature and the cost to rebuild a form is negligible. However, the bad news is that not all Ajax forms work this way. If your Ajax callback (backend code that handles incoming Ajax requests) sends an Ajax response that cannot be mimicked at a form rebuild, there will be no fallback behavior for no-JS users. For example, if you have a form that shows a second field based on a value you select on the first field, you can make the Form API display this second form when a no-JS user submits a button. However, anything beyond that, for example opening a modal popup, will not work for no-JS users, making this fallback feature useless for them.

Drupal.behaviors hell

This is something many non-Drupal front-end developers are baffled about. If you have custom JavaScript functionality that should run after the page has loaded, you often run this on document on-ready event. While this works in Drupal too, the recommended way is to use Drupal.behaviors. On a page load, right after the HTML content is downloaded, Drupal.behaviors handlers are called. Whenever an Ajax callback has completed, Drupal.behaviors would run too, allowing the handlers to modify the new HTML. Majority of these behavior handlers are not properly written in a way with Ajax modifications in mind, which leaves memory leaks in browser and slows down every Ajax callback.

Trusting the client to send same data back

This is one area that allowed the DrupalGeddon 2 to happen. The Ajax settings are sent to the client, and Drupal expects the client to send them back. While it is necessary to allocate a form ID and a build ID (which are used to lookup the form data during Ajax calls), there are certain information that must be validated to prevent someone from fooling Drupal to load/process data and execute certain functionality that is not meant to happen. If we send some data to the user and expect the user to use/send them back without tampering, the least we could do is at least check the signature of this data.

How can we fix this?

I don't honestly know ¯_(ツ)_/¯
What we should ideally have is a proper Ajax system that does not hammer browsers and uses a modern front-end framework to help us with all Ajax modifications because they have done a better job. As for the Form/Render APIs, I would love to see an OOP-ish element system, such as the one suggested by very Larry Garfield: HtmlModel.