Sensitive parameters in PHP 8.2

Debugging errors can leak sensitive information, let's see how we can solve this using PHP 8.2

This post was first published on the Flare blog.

When debugging errors, we as developers have a few tools available: an exception of a particular class, a message, a status code, and a stack trace. The stack trace is a report that provides information about the sequence of function calls that led to an error or an exception. It shows the order in which functions were called.

We name each call to a function/method within a stack trace a frame. It has lots of useful info like the file name, line number, function name, class (sometimes), and arguments that were used to make the call. Having arguments within a stack trace can be dangerous. For example, let's say you have a function like this:

function getPayments(string $apiKey): array 
{
    throw new Exception('Could not fetch payments');
}

Usually, you would have some application code here, fetching payments through an external API, but to simplify things, let's go straight to the case where we throw an exception because the payments couldn't be fetched.

You can call this function as such:

getPayments('dj8gd4sl05db32xjch7989dsxws');

This function call, of course, fails miserably, providing you with the following exception message:

Fatal error: Uncaught Exception: Could not fetch payments in /in/UoFa1:6
Stack trace:
#0 /in/UoFa1(32): getPayments('dj8gd4sl05db32x...')
#1 {main}
  thrown in /in/UoFa1 on line 6
<br/><i>Process exited with code <b title="Generic Error">255</b>.</i>

Take a look at the third line:

#0 /index.php(34): getPayments('dj8gd4sl05db32x...')

That's our API key in plain sight, which we want to avoid at all costs!

Above, we saw the stringified version of the stack trace. When we catch the exception, we can take a look at a more normalized array version:

try {
    getPayments('dj8gd4sl05db32xjch7989dsxws');
} catch (Exception $e) {
    var_dump($e->getTrace());
}

Catching the exception gives us the following stack trace:

array(1) {
  [0]=>
  array(4) {
    ["file"]=>
    string(9) "/index.php"
    ["line"]=>
    int(34)
    ["function"]=>
    string(11) "getPayments"
    ["args"]=>
    array(1) {
      [0]=>
      string(27) "dj8gd4sl05db32xjch7989dsxws"
    }
  }
}

As you can see, we're passing one argument to the getPayments function, which can be helpful when debugging. But this information can also end up in the wrong hands when displayed in logs or error pages like Ignition.

That's the reason why we refrained from showing these parameters in Ignition:

And thus, due to the tight coupling of Flare and Ignition, these parameters won't end up in Flare.

But this all might change due to a recent addition to PHP 8.2.

Sensitive Parameters

Starting from PHP 8.2, some parameters can now be tagged as 'sensitive' using an attribute, meaning a parameter will be hidden from a stack trace.

Let's rewrite our getPayments function as follows:

function getPayments(
    #[SensitiveParameter]
    string $apiKey
): array {
    throw new Exception('Could not fetch payments');
}

Now when we execute the function, we get the following:

Fatal error: Uncaught Exception: Could not fetch payments in /in/Ko3d8:13
Stack trace:
#0 /in/Ko3d8(32): getPayments(Object(SensitiveParameterValue))
#1 {main}
  thrown in /in/Ko3d8 on line 13
<br/><i>Process exited with code <b title="Generic Error">255</b>.</i>

The API key is replaced with Object(SensitiveParameterValue), hiding the original value! Let's see how the stack trace looks:

array(1) {
  [0]=>
  array(4) {
    ["file"]=>
    string(9) "/in/cv0er"
    ["line"]=>
    int(33)
    ["function"]=>
    string(11) "getPayments"
    ["args"]=>
    array(1) {
      [0]=>
      object(SensitiveParameterValue)#2 (0) {
      }
    }
  }
}

Again the API key is gone from traces, but the value still exists. The original string value is replaced with an object SensitiveParameterValue, which wraps the original value. We can see this by retrieving the original value as such:

try {
    getPayments('dj8gd4sl05db32xjch7989dsxws');
} catch (Exception $e) {
    var_dump($e->getTrace()[0]['args'][0]->getValue()); // dj8gd4sl05db32xj...
}

Classes

The example mentioned above, used functions to make things easy to understand, but you can perfectly tag parameters as sensitive in a class:

class PaymentApi
{
    public function getPayments(
    	 int $tenantId,
        #[SensitiveParameter]
        string $apiKey
    ): string {
        throw new Exception('Could not fetch payments');
    }
}

Running this:

try {
    $api = new PaymentApi();
    $api->getPayments(314, 'dj8gd4sl05db32xjch7989dsxws');
} catch (Exception $e) {
    var_dump($e->getTrace());
}

It gives us the following stack trace:

array(1) {
  [0]=>
  array(6) {
    ["file"]=>
    string(90) "/index.php"
    ["line"]=>
    int(36)
    ["function"]=>
    string(11) "getPayments"
    ["class"]=>
    string(10) "PaymentApi"
    ["type"]=>
    string(2) "->"
    ["args"]=>
    array(2) {
      [0]=>
      int(314)
      [1]=>
      object(SensitiveParameterValue)#3 (0) {
      }
    }
  }
}

As you can see, the tenantId is a non-sensitive parameter, so its value still appears in the stack trace. However, apiKey, the sensitive parameter value, has been wrapped into a SensitiveParameterValue object.

Flare

As mentioned, we don't support stack trace arguments in Ignition for security reasons, and this new sensitive parameter feature might let us rethink this. On our roadmap, I've created a new feature request, allowing arguments to be shown in Ignition and Flare stack traces. Give it a thumbs up if you want to see this implemented.