DEV Community

James Turner
James Turner

Posted on • Edited on • Originally published at turnerj.com

The anatomy of a critical vulnerability

Note: This was responsibly disclosed to SilverStripe immediately when it was discovered. It has now been publicly disclosed by SilverStripe (CVE-2019-5715 / SS-2018-021) as of the 18th of February with patches for all supported versions released.

Where I have worked for the last 7 years, we have primarily built sites in PHP, specifically SilverStripe. As a final little surprise for my colleagues before I left, I discovered an SQL injection vulnerability.

Before going into detail of what the security vulnerability is, I need to explain a little about SilverStripe.

SilverStripe

SilverStripe is an open source PHP content management system. At its core, you have a DataObject class that basically everything that writes to the database uses. That is from your HomePage class to your ImageSlide class to your Member class.

A typical DataObject might look something like this (namespacing removed for brevity):

class MyCustomObject extends DataObject
{
    private $db = [
        'MyProperty' => 'Varchar'
    ];

    private $has_many = [
        'Slides' => ImageSlide::class
    ];
}
Enter fullscreen mode Exit fullscreen mode

When you have an instance of one of these objects, you can read/write MyProperty through magic methods:

$myPropertyVal = $myCustomObject->MyProperty;
$slides = $myCustomObject->Slides();

$myCustomObject->MyProperty = 'Hello world!';
$myCustomObject->write();
Enter fullscreen mode Exit fullscreen mode

This is the main way you will deal with getting and setting values. There are other helper methods which can bulk set these values from arrays etc.

That's all you need to know to get the gist of the issue that is to come.

Discovery

While I did discover the issue, there are two important things to mention in the discovery process:

  • A site of ours was having a security audit through one of those automatic vulnerability testing systems. An error from a specific attempt pointed me in the right direction.
  • When I disclosed the issue to SilverStripe, they discovered further issues which expanded on this vulnerability.

The error triggered by the automated security audit was with a SilverStripe module called SwipeStripe however it wasn't a bug with the module, just in this case initially exposed through the module.

There is an OrderForm class with a specific update method which is where the problems start:

public function update(SS_HTTPRequest $request) {
    if ($request->isPOST()) {
        $member = Customer::currentUser() ? Customer::currentUser() : singleton('Customer');
        $order = Cart::get_current_order();
        //Update the Order 
        $order->update($request->postVars());
        $order->updateModifications($request->postVars())
            ->write();
        $form = OrderForm::create(
            $this->controller, 
            'OrderForm'
        )->disableSecurityToken();
        // $form->validate();
        return $form->renderWith('OrderFormCart');
    }
}
Enter fullscreen mode Exit fullscreen mode

It is $order->update($request->postVars()); that is a problem. The update function call on the $order object is a built-in SilverStripe function where you pass an array of fields and it will set them for you. The passing of POST variables straight into it is a Very Bad Idea™ (you should be checking your fields properly) but I digress.

SilverStripe would normally safely prepare queries so SQL injection can't happen but there was an edge case where it didn't work.

It turns out that if your POST request looks less like SomeProperty=1 and more like SomeProperty[MySneakySQLStatement]=1, things start going weird, skipping how the prepared statements work and ending up in the raw query.

This issue presents itself here because postVars returns SomeProperty[MySneakySQLStatement]=1 to something a little like this:

{
    "SomeProperty": {
        "MySneakySQLStatement": 1
    }
}
Enter fullscreen mode Exit fullscreen mode

If internally it looked more like the following, it likely would have not been an issue:

{
    "SomeProperty[MySneakySQLStatement]": 1
}
Enter fullscreen mode Exit fullscreen mode

Now to actually have a working exploit, it is a little more complex than just sticking an SQL query in a field name but you get the idea.

I gave everything I knew to SilverStripe and they took it from there, finding that it is a bit bigger of an issue than it seemed.

The actual issue

In terms of a method to exploit this issue, I wasn't wrong but it wasn't the only method. It turns out that the update function I mentioned earlier isn't the problem, it is the code that it depended on which backs basically all writing to the database.

In SilverStripe's blog post about the issue, they state:

Both direct assignment on DataObject (update(), setters via method calls, setters via magic methods) and indirect assignment (e.g. Form->saveInto()) are affected.

So not just the update method that I originally noticed but basically every method of writing was affected! This bug just turned from a "bad but unlikely to affect many" to a "oh this might affect everyone".

Their blog post goes onto say:

The vulnerability is related to the DBField classes underpinning the DataObject logic. Most DBField types in SilverStripe 3 are affected. Only the DBYear field type is confirmed to be affected in SilverStripe 4, limiting the impact in our current release line to a relatively uncommon use case.

While still a critical vulnerability, it is great that through other fixes and improvements that the latest major version of SilverStripe (Version 4) is less impacted.

The fix

The fix is actually fairly small, split across 5 files (for SilverStripe 4) which you can see here.

It really amount to double checking that field assignments are not arrays.

For example, in part of the DataObject class:

// Make sure none of our field assignment are arrays
foreach ($manipulation as $tableManipulation) {
    if (!isset($tableManipulation['fields'])) {
        continue;
    }
    foreach ($tableManipulation['fields'] as $fieldValue) {
        if (is_array($fieldValue)) {
            user_error(
                'DataObject::writeManipulation: parameterised field assignments are disallowed',
                E_USER_ERROR
            );
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Always, always, ALWAYS responsibly disclose security issues. Thanks to Matt, Maxime, Ingo and everyone else at SilverStripe who worked on testing and resolving this vulnerability! 🙂

For SilverStripe users, you can view all security releases here or view the security release processes here.

Top comments (0)