Artificial truth

The more you see, the less you believe.

[archives] [latest] | [homepage] | [atom/rss/twitter]

PHP8, from a security point of view
Fri 04 December 2020 — download

PHP8 was released on the 26th of November 2020. It brought a lot of interesting things, security-wise, but also showcases a couple of (minor) missed opportunities in my opinion.

Type safety

I'm a big fan on relying on typing to ensure security properties, like Trusted types in javascript: it shouldn't compile if it's not secure.

PHP8 won't try to cast string into numbers anymore, thanks to the Saner string to number comparisons RFC, meaning that collision with hashes starting with 0e and the likes are finally a thing of the past! This is a subset of Snuffleupagus' sloppy comparison prevention feature.

The Consistent type errors for internal functions RFC will prevent things like 0 == strcmp($_GET['username'], $password) bypasses, since strcmp won't return null and spit a warning any longer, but will throw a proper exception instead. This was also a nice opportunity for PHP to add annotations for functions parameters and return types.

The Stricter type checks for arithmetic/bitwise operators and PHP RFC: Reclassifying engine warnings RFC are in the same spirit.

JIT

PHP8 comes with a JIT based on DynASM, bringing an RWX memory space into PHP's memory space, into a shared allocation, meaning that its offset won't change between different PHP8+ processes.

Moreover, DynASM isn't designed with processing/compilation/execution of untrusted code in mind, and doesn't do things like constants blinding and advanced folding to mitigate against spraying, nor random padding/nop insertion, nor ensuring that the memory region is never both writeable and executable to prevent direct code injection. This means that it's now way easier to gain native code execution when exploiting memory corruptions, albeit to be fair, most attackers are happy with a php code execution, and won't push further.

Having a JIT comes with a lot of code complexity and maintenance burden. I'll be without doubt a great source of bugs, for a minor speed improvement on real-life workloads.

Cryptography

  • password_hash now automatically generates a salt, accepting a user-provided one is deprecated.
  • crypt will now fail instead of silently falling back to DES when an unknown salt format was provided. The parameter is also made mandatory, hashing without a salt is now unsupported.
  • RFC 5652 is now exposed via the OpenSSL extension.

Misc

  • The error control operator, aka @ won't silence fatal errors anymore, meaning that poorly written webshells will have more chances to leave traces in your logs.
  • libxml_disable_entity_loader is now deprecated, even if it's not (yet) reflected in php's documentation. This is acceptable since PHP8 now requires at least libxml 2.9.0, which comes with external entity loading disabled by default.
  • Access to undefined constants will throw an error, instead of being silently interpreted as a string, no more SALT being silently converted to "SALT".
  • create_function was removed, closing its infamous code injection vector.
  • array_key_exists throws when passed an array/object, instead of silently doing nonsense.
  • The e modifier in mb_ereg_replace has been removed.
  • Metadata associated with a phar will no longer be unserialized, killing a low-hanging RCE vector.
  • FILTER_SANITIZE_MAGIC_QUOTES, get_magic_quotes_gpc and get_magic_quotes_runtime have been removed, people will now have to do proper sanitization instead.
  • As usual, a couple of memory safety issues were fixed, some exploitable.
  • Snuffleupagus is currently being ported to php8!

Missed opportunities

Undefined variables, as opposed to constants, are still not an error, meaning that things like solt instead of salt might (and will) go unnoticed.

Converting an Array to a string will only yield a Warning instead of an error, albeit that now that __toString can finally throw, it might hopefully change in the near future.

Albeit significant CSPRNG improvements have been merged in PHP7, PHP8 didn't seize the opportunity to keep the momentum and to aliases rand and mt_rand to random_int, like Snuffleupagus is doing.

An other missed opportunity in my opinion is that there is still no way to disable some PHP's wrappers, except via stream_wrapper_unregister but this can be reversed with stream_wrapper_restore. Wrappers are scary: the main use I've seen for the filter:// one is exfiltrating data via php://filter/convert.base64-encode/resource=/some/file, and is a decent amount of arcane stuff lurking in the shadows. Providing a way to reduce this attack surface (like making streams opt-in) would be welcome.