PHP 8: Override internal functions with disable_functions INI directive

Published On07 Jul 2020

Redefine disable_functions in PHP 8 In PHP, it is possible to disable certain functions and classes with php.ini directives.

  • disable_functions: A comma-separated list of function names that PHP does not allow to be used.
  • disable_classes: Similar to functions, a list of classes that is now allowed to use.

These directives were used commonly to disable potentially unsafe functions from being used even if a malicious actor gets code-execution access in a server.

In PHP 8, disabled functions are not included in the functions table, which means function_exists() calls will return false for disabled functions, and most importantly, it will be possible to redefine the disabled functions.

Example

Here is an example of redefining sleep() function with a user-land implementation.

1. Create the new function definitions

Once we disable the functions, they will not be available to anywhere in the code under test. If you override certain features that your testing framework itself uses (such as microtime(), which is often used to calculate the execution time of tests), you will need to be careful to not break that functionality.

// test.php
function sleep() {
    return;
}

\sleep(1662815);

In a test.php file, we redefine the sleep() function to immediately return, without actually holding the execution.

Notice how we used a fully-qualified function name and it even works there.

2. Execute the script with disable_functions INI directive

php -d disable_functions=sleep test.php

When you run the code, PHP will first disable the internal sleep function, and due to PHP 8 changes, our custom test.php file is able to define a new sleep function.

Use cases

¯\(ツ)

It appears that disable_functions + redefine is the only way to effectively override a PHP internal function with user-land code.

Log/Fail unsafe internal functions

You can redefine functions such as system and create a log entry to detect whenever this function is used.

Note that language constructs, such as echo, include cannot be redefined. This unfortunately includes eval as well.

// troll.php
function system() {
    echo 'Ha!';
}
# php.ini file
[PHP]
disable_functions=system
auto_prepend_file=troll.php

Seamlessly polyfill legacy code with modern libraries

While it's not the most convenient approach, you can overwrite legacy functionality such as the whole mcrypt_* suite, or even mysql_* functions with modern libraries such as Sodium and PDO.

A user-land implementation that wraps an alternative library/extension with legacy function names can be technically done. Note that neither mcrypt, nor mysql extensions are in MySQL 8, so you are not actually redefining anything in this particular example.

function mcrypt_encrypt(string $cipher, string $key, string $data): string {
    return sodium_crypto_secretbox(...);
}

Make bad decisions

// hackers-go-brrrr.php
function password_hash(string $password): string {
    return md5($password);
}

function password_verify(string $password , string $hash): bool {
    return md5($password) === $hash;
}

var_dump(password_hash('hunter2'));
var_dump(password_verify('hunter2', '2ab96390c7dbe3439de74d0c9b0b1767'));
php -d disable_functions=password_hash,password_verify hackers-go-brrrr.php

More Sane approaches

Symfony PHPUnit Bridge Mocks

Symfony PHPUnit Bridge has ClockMock and DnsMock that dynamically declares time and DNS related features right before PHPUnit tests are run.

It is not possible to use fully-qualified function names with PHPUnit Bridge.

Autoload modified code

Another approach albeit being a bit more complicated, would be override the class autoloader mechanism, and load (automatically) modified code converts fully-qualified function names to namespaced function names, and define those functions in the same namespace.

PHP autoload override library does exactly that.

Advanced PHP Debugger

APD extension has features to override an internal function.

Updated

This post was updated updated on 2020 July 08. A prior version pointed that it was not possible to override certain functions that are inlined in PHP. This has then been fixed.

Recent Articles on PHP.Watch

All ArticlesFeed 
PHP 8.4 Installation and Upgrade guide for Ubuntu and Debian

PHP 8.4 Installation and Upgrade guide for Ubuntu and Debian

A guide for Debian and Ubuntu on how to install PHP 8.4 on a new server or how to upgrade an existing PHP setup to PHP 8.4.
How to fix `mysql_native_password` not loaded errors on MySQL 8.4

How to fix mysql_native_password not loaded errors on MySQL 8.4

How to fix the SQLSTATE[HY000] [1524] Plugin 'mysql_native_password' is not loaded errors caused in MySQL 8.4 no longer enabling the mysql_native_password plugin by default.
How to fix PHP Curl HTTPS Certificate Authority issues on Windows

How to fix PHP Curl HTTPS Certificate Authority issues on Windows

On Windows, HTTPS requests made with the Curl extension can fail because Curl has no root certificate list to validate the server certificates. This article discusses the secure and effective solutions, and highlights bad advice that can leave PHP applications insecure.
Subscribe to PHP.Watch newsletter for monthly updates

You will receive an email on last Wednesday of every month and on major PHP releases with new articles related to PHP, upcoming changes, new features and what's changing in the language. No marketing emails, no selling of your contacts, no click-tracking, and one-click instant unsubscribe from any email you receive.

Support PHP.Watch — If you find the articles, version information, Codex, and other PHP.Watch contributions useful, consider supporting through GitHub Sponsors. Your sponsorship helps dedicate more time to creating valuable content and improving the PHP community. Together, we can keep the momentum going — thank you for your support!

Thanks to the highest tier sponsor: @TomasVotruba for your generous support to keep PHP.Watch moving 💜