Similar Articles

Using Canaries for Input Detection and Response

Canary in a Coal Mine

I'm sure you've heard the common phrase "a canary in a coal mine" when people talk about safety and detection of issues. When miners used to go down to work, there was a danger of trapped gasses being released as they were digging. Some of these gasses were hard for humans to detect and, if enough was breathed in, could lead to illness or even death. In order to help the miners detect and avoid these kinds of issues, they would take a canary (the bird) down into the mine with them. The birds were more sensitive and, if something was released or some other danger might be near, the bird would react and alert the miners of the danger.

The idea of a "canary" value in the security world is pretty similar. A "canary" value is one that - real or faked - is somehow exposed outside of your own system. If this value is used you need to be notified immediately so you can take action and gather more information about the usage and any other associated issues. Some examples might be:

  • A username on an admin account that's locked
  • An API token that would always return a failure
  • An entity ID that doesn't actually exist

Basically, a canary value can be any piece of data - real or fake - that you want to detect the use of. The idea is that, if an attacker uses one of these values you're notified about it. It could mean that someone is poking around where they shouldn't or that other information from your application could have been compromised. PHP applications now have an easy way to handle the detection and notification process using the psecio/canary library.

The Canary Package

The psecio/canary package was created out of a need to detect certain values when they were detected and trigger notifications to various services. The package provides two pieces of functionality:

  • It allows for the definition of "canary" key/value pairs to look for in the incoming data. By default this data will include the PHP superglobals $_GET, $_POST and the $_SERVER['REQUEST_URI'].
  • It provides various adapters to handle the notifications when the key/value matches are detected.

At the time of this article, the notification methods offered are: the standard PHP error log, logging via Monolog, messaging via PagerDuty and Slack notifications. It also allows for the definition of a custom callback allowing for even more customization to your needs.

Installation

The Canary package is easily installable via the Composer package management tool. Use this command to pull it into your project:

composer require psecio/canary

The library has a few optional dependencies (noted as "suggestions") that provide the functionality for the different notification services:

If you don't define a custom handler for your matches the package will default to reporting matches to the standard PHP error log.

Basic Usage

Once you have the package installed, using it is simple. Canary was designed with a fluent interface making it very "user-friendly" and understandable. You define the matching and notifications with if/then relationships. The keys and values to match are defined in the if and the notification method in the then.

Here's a basic example. Remember that the notification method defaults to the PHP error log:

<?php
require_once 'vendor/autoload.php';

\Psecio\Canary\Instance::build()
    ->if('username', 'foobar')
    ->execute();

?>

When this code is executed, the incoming data is checked for a username value. If one is found, the value is checked against our criteria. In this case, if the value matches our "foobar" requirement, a notification will be sent to the PHP error log like this:

Canary match: {"type":"equals","key":"username","value":"foobar"}

The output includes the JSON data from the match: the type of match, the key detected and the value of that key. This JSON output is consistent across the different notification methods.

NOTE: As it currently stands there's not a way to change up this output string or its contents. However, if you use the Monolog integration you can always modify the message contents as they come through with a custom processor.

If you'd like to use one of the other integrations, you'll need to set it up with a then method call. For example, if we wanted to set up Slack notifications, we create our client first then pass it in via the then on our matching:

<?php
require_once 'vendor/autoload.php';

$settings = [
    'channel' => '#notifications'
];

$slack = new Maknz\Slack\Client('https://hooks.slack.com/services/....', $settings);

\Psecio\Canary\Instance::build()
    ->if('username', 'foobar')
    ->then($slack)
    ->execute();

?>

In the code above we create the Maknz\Slack\Client instance with our own configuration (the $settings) of where to send the messages to. This instance is then added to the match with a fluent call to then and passing it in. The result is similar to the previous example except that instead of going to the error log the message is sent to a Slack channel via the webhook integration.

So, you may be asking yourself "what's the use of just matching one key/value set at a time?? That doesn't seem very flexible." Canary allows you to define multiple key/value pairs to watch for all at the same time and send the notifications to a single destination. Here's what that looks like:

<?php
require_once 'vendor/autoload.php';

$matches = [
  'username' => 'foo',
  'test' => 'bar'
];

\Psecio\Canary\Instance::build()->if($matches)->execute();

?>

This shows the use of a basic array to define multiple matches all at once. Since there's not a call to then following it, if there's a match and notification will be sent to the error log. When multiple matches are defined like this, the notifications can only go to one destination. If you want different canary values to notify to different destinations, you'll need to do multiple if/then calls:

<?php
require_once 'vendor/autoload.php';

$canary = \Psecio\Canary\Instance::build();
$slack = new Maknz\Slack\Client('https://hooks.slack.com/services/....', [...]);

$matches = [
  'username' => 'foo',
  'test' => 'bar'
];
// Goes to just the error log
$canary->if($matches);

// Send this one to Slack instead
$canary->if('username', 'testuser')->then($slack);

// Execute all checks and notify
$canary->execute();

?>

Dyanmic Criteria & Callbacks

There are two other pieces of functionality I want to cover that help make the Canary library a bit more flexible: using a class/method path to define multiple matches and using a custom closure callback for a notifier.

First lets look at an example of dynamically pulling the matches using a class path:

<?php
require_once 'vendor/autoload.php';

$path = '\Foo\Bar::criteria';
\Psecio\Canary\Instance::build()->if($criteria)->execute();

?>

The code defines a $path value with our source that points to a static method that will return an array. Using this approach we can then abstract out the logic so it's not all hard-coded into our detection point. The class definition would look like this:

<?php
namespace Foo;

class Bar
{
  public static function criteria()
  {
    return [
      'username' => 'foo'
    ];
  }
}

Obviously, in this example, the method is just returning a set of hard-coded key/value pairs but you can see how this could be updated to pull from any data source you'd like. This prevents you from having to define custom logic in the code at each entry point.

Next, I want to show an example of the custom closure for the then handler. While there are already several integrations with external services sometimes you may have a need for a custom solution to log to something outside of those. In that case, you could use a callback and use to import the dependencies for the call. Here's an example of this:

<?php
require_once 'vendor/autoload.php';

$path = '\Foo\Bar::criteria';
\Psecio\Canary\Instance::build()
  ->if($criteria)
  ->then(function($criteria) use ($adapter){
    $adapter->send($criteria->toArray());

    // It also allows for JSON encoding
    echo json_encode($criteria);
  })
  ->execute();

?>

This example is pretty simplistic but it shows the basic idea. The closure is passed in manually through a then call and, when a match is found, the $adapter is called with the $criteria information for the match. The data for the criteria can then either be pulled out via the toArray call or you can json_encode it to get a string similar to the output in the other destination types.

Putting it to use in middleware

Now that I've shown how to use it in isolation, let's take a look at it in use in a middleware inside of an actual PHP application. For simplicity, I'm just going to use a Slim framework application with a custom middleware included for each request.

First let's get the dependencies installed:

composer require psecio/canary slim/slim

Once those are installed, create an index.php file in your current directory with the following inside:

<?php
require_once 'vendor/autoload.php';

$app = new \Slim\App();

$app->add(function($request, $response, $next) {
  \Psecio\Canary\Instance::build()->if('username', 'test')->execute();

  $response = $next($request, $response);
  return $response;
});

$app->get('/', function($request, $response) {
  $output = '<a href="/?username=foo">no trigger</a><br/>';
  $output .= '<a href="/?username=test">trigger</a><br/>';

  $response->getBody()->write($output);
  return $response;
});

// Run the application
$app->run();

?>

To run the application you can use PHP's built-in web server. In the same directory as your index.php file use the following on the command line:

php -S localhost:8080

Then visit http://localhost:8080. You should see the page with two links: "no trigger" and "trigger". You'll also notice that the window where you ran the php command shows a log of the request to the / path. This same logging is where, when the match is triggered, the error will show (essentially the "error log" for the built-in PHP web server).

If you click on the link for "no trigger" you'll see that it redirects you to a page with a username value on the URL as a parameter. In this case, however, the value is "foo". In our middleware we're looking for a different value so no notification is triggered. Now click on the other link - the "trigger" one - and you'll see something different in the logs:

[Tue Feb 27 14:22:30 2018] Canary match: {"type":"equals","key":"username","value":"test"}
[Tue Feb 27 14:22:30 2018] ::1:49471 [200]: /?username=test

The username value matches the one we defined in the if check so the notification fires and the output is sent to the error logging. The notification comes before the recording of the page hit because the middleware fires before any route processing happens. That's more about how Slim operates, though, and isn't always true for all frameworks. This example shows it in use in a middleware and being called on every request but it can definitely be more specialized than that. You could potentially only call it when certain endpoints are requested or even restricting it using more complex criteria.

Wrapping Up

The Canary library provides an easy way to detect input and handle the notifications when they're matched. There's advanced usage of the library that isn't covered in this article but is detailed in the project README. Of course, Canary is an open source project so if you have any recommendations or find issues, feel free to submit an issue or even submit a pull request with your updates!

Resources

by Chris Cornutt

With over 12 years of experience in development and a focus on application security Chris is on a quest to bring his knowledge to the masses, making application security accessible to everyone. He also is an avodcate for security in the PHP community and provides application security training and consulting services.