Security Tip: Watch Out for Type Juggling
[Tip#26] Type Juggling is still very much a problem.
PHP and type juggling…
In my opinion, type juggling is one of PHP’s strengths, but also one of PHP’s easily exploitable weaknesses.
The weakness is the result of using a loose comparison ( ==
and !=
), rather than a strict comparison ( ===
and !==
), and if I can impart just one thing from this post, it’s a reminder to always use a strict comparison, unless you specifically need a loose comparison. You also need to be aware of which functions are strict and loose, and code accordingly.
Important note: If you’re comparing sensitive strings like API keys or passwords, you really should use the hash_equals()
method. It ensures a type and timing safe comparison. I wrote about it here!
Being vulnerable to type juggling is a common argument used against PHP and it was such a big concern that PHP 8 introduced a backwards incompatible change that changed string-to-number comparisons so they would convert to strings and not numbers.
This should show you the before and after behaviour:
Comparison PHP 7.4 and earlier PHP 8.0+
0 == "0" true true
0 == "0.0" true true
0 == "foo" true false
0 == "" true false
42 == " 42" true true
42 == "42foo" true false
While this was a great security update to PHP, it didn’t solve the whole problem. Sure you can no longer easily juggle numbers and strings maliciously, but there are more inputs you can provide that you still can juggle with.
For example: Booleans!
The easiest way to exploit type juggling is through an API endpoint, because you’re not limited to passing everything in as a string. Instead, you can submit a Boolean directly to the API and the code will see it as one.
Let’s take a look at a super simple example:
Route::post('test', function (Request $request) {
return [
'$request->input' => $input = $request->input,
"'string' == \$input" => 'string' == $input,
"'string' != \$input" => 'string' != $input,
"'string' === \$input" => 'string' === $input,
"'string' !== \$input" => 'string' !== $input,
];
});
Using the above API route, let’s send some true
and false
values:
>>> Http::post('https://larasec/api/test', [
'input' => true,
])->json();
=> [
"$request->input" => true,
"'string' == $input" => true,
"'string' != $input" => false,
"'string' === $input" => false,
"'string' !== $input" => true,
]
>>> Http::post('https://larasec/api/test', [
'input' => false,
])->json();
=> [
"$request->input" => false,
"'string' == $input" => false,
"'string' != $input" => true,
"'string' === $input" => false,
"'string' !== $input" => true,
]
You’ll note that passing in the Boolean value true
allows us to pretend we’ve passed in a valid string:
'string' == $input => true
'string' != $input => false
In this example, we’ve got an API checking a string value, but what if that string value is the API authentication key that provides access to the API?
We’ve bypassed the auth key by passing true
instead… Full access granted, no API key required. 😱
I have found this exact vulnerability in a number of my security audits. An auth token is received by the app via a JSON payload, swapping it to `true`
instantly gave me access. Every. Single. Time. I even did it via a cookie once. 😈
Do you do this in your app?
Type juggling is a problem, and it’s not just limited to Boolean values and API endpoints. Check your app to ensure you’re doing strict comparisons and validate & typecast inputs to ensure they are in the formats you need.