Security Issues: ctype_digit() Weirdness
If you've ever attended a PHP course, when considering adding form data validation, most instructors will follow the conventional wisdom, which says to avoid using regular expressions if you are able to use any of the PHP ctype_*() family of functions instead. This handy set of functions is indeed convenient, fast, and have all been in the language for ages. In contrast with the is_*() functions, instead of determining the data type of a variable, the ctype_*() family examines the contents of the variable. There is a little bit of weirdness surrounding ctype_digit(), however, of which you might not be aware. In this article we examine this function's dirty little secret and show you how to avoid ending up with false positives when performing form data validation.
So What About ctype_digit()?
Probably a good starting point when discussing ctype_digit() is to have a look at it's signature. Consulting the documentation page, we come up with this:
What's important about this signature is that the data type of the variable to be examined is mixed. This means that, at least theoretically, ctype_digit() is able to examine any variable and return TRUE or FALSE depending on whether or not its contents are 0 - 9 only.
Using ctype_digit() for Form Data Validation
In the following example, let's assume that you're validating form data. For the sake of this example, let's assume that $_POST contains the following:
$_POST = [ 'id' => '1111', 'age' => '49', 'gender' => 'M', 'amount' => '99.99', 'life_universe_and_everything' => '42' ];
If we then run the post data through a foreach() loop, and validate using ctype_digit():
$ptn = "%30s : %s\n"; printf($ptn, 'Form Field', 'Only Digits'); foreach ($_POST as $key => $value) printf($ptn, $key, (ctype_digit($value) ? 'Y' : 'N'));
The resulting output would appear as follows:
Form Field : Only Digits id : Y age : Y gender : N amount : N life_universe_and_everything : Y
OK, so far so good. So ... what's the problem?
Houston We Have a Problem!
What a developer might next do with the data, prior to validation, is perform a normal bit of sanitization. Here's how the sanitizing code might appear:
$id = $_POST['id'] ?? 0; $age = $_POST['age'] ?? 0; $gender = $_POST['gender'] ?? ''; $amount = $_POST['amount'] ?? 0.00; $life_etc = $_POST['life_universe_and_everything'] ?? 0; $_POST['id'] = (int) $id; $_POST['age'] = (int) $age; $_POST['gender'] = (in_array($gender, ['M','F','X'])) ? $gender : 'X'; $_POST['amount'] = (float) $amount; $_POST['life_universe_and_everything'] = (int) $life_etc;
However, if we then run the sanitized code through the same loop as shown above, the result is slightly different, as seen here:
Form Field : Only Digits id : Y age : Y gender : N amount : N life_universe_and_everything : N
The last item should come back as containing only digits, as it's the number 42. But, as you can see from the output, we now have a false positive. Help!!! What's going on?
ctype_digit's Dirty Little Secret
The answer to this perplexing problem can be found if we return the the PHP documentation page for ctype_digit().
As you can see from the documentation, although
accepts arguments of mixed data type, if the data type is
and the value is between -128 and 255, it's
treated as an ASCII code! So, in the example shown
just above, the
field, with a value of 1111, passed without a
field, on the other hand, was treated as an ASCII character
42, which happens to be the code for an asterisk
(*). Since an asterisk is not a digit, the
return value from ctype_digit()
Got It ... But What About Age?
But now we have a bigger problem: why did
return TRUE? The answer to this again takes us back
to the ASCII table. The value for
was 49. A look at the ASCII table shows us that 49
is the ASCII code for the number one!
returns a value of TRUE, as one is certainly a digit.
When performing form data validation using any of
family of functions, especially ctype_digit(),
it would be best to leave the data present in
$_GET) in its original state while performing
validation. The ctype_*()
family works best with string data, despite the fact that it
technically accepts any data type. You can then perform
sanitization after the validation has occurred.