PHP Tricks: Access control bypass

in PHP2 years ago

This is old-hat to some people, but it's still a nice trick.

PHP has long supported Java-esque property and method access control. Object properties and methods may be marked public, protected, or private to control what other code may access them. The general recommendation for years is that properties should be non-public in almost all cases, unless you're using the new readonly flag in PHP 8.1.

(There's a good case to be made that object-level encapsulation is the wrong model in the first place, and package-level visibility is the superior model. I happen to agree with that argument, but that's neither here nor there for the time being.)

Most of the time, you want to respect the visibility set by the code's author. It's telling you how an object should be used, and if you do otherwise you're on your own. However, there are cases in meta-programming situations where you need to be able to bypass the visibility in order to manipulate the state of an object in a dynamic fashion. The most common scenarios are serialization, de-serialization, hydration from an ORM, and similar.

Being reflective

It is possible to use the Reflection API to change the visibility of a property or method. However, that API is rather cumbersome, plus it means you're leaving the visibility changed for anyone else using that object in the future. If mutable data is bad, mutable structure sounds even worse! (At least when not in a language with built-in dependent types, but that's a topic for another time.)

Scope it out

There is a more robust, simpler, and overall easier approach, however. Since PHP 5.4, PHP has supported closures (anonymous functions with bound data) that can be re-bound to an object scope. In other words, the $this variable inside that function can be changed out from under it. That's out "in."

Any anonymous function is actually an instance of the \Closure class, which has a few methods on it. For our purposes, the important ones are call() and bindTo().

The call() method invokes the function, but it first "swaps out" the scope of the closure for a provided object. That is, the function will run but $this will refer to the object that's passed. Because it is now "in" that object's scope... it will have access to any private variables, and to setting an uninitialized readonly property. That means we can do this:

class Fort
{
    public function __construct(private string $secretPlans) {}

    private function buryTreasure() { /* ... */ }

    // ...
}

$fort = new Fort('ssssh');

$propReader = fn (string $prop): mixed => $this->$prop ?? null;

$stolenData = $propReader->call($fort, 'secretPlans');

The $propReader function will now run "in the context of" Fort, and since it's in Fort's scope, it will have access to that object's private variables. Depending on what you're doing, a less generic reader function may make more sense but in my use cases I almost always need something this generic. (The ?? null part also folds uninitialized properties to null without an error.)

In my experience, I more often want to use the closure multiple times on the same object. For that, we have bindTo(). bindTo() takes both a dynamic and static scope to use and returns a new closure with that scope bound. That is:

$fortReader = $propReader->bindTo($fort, $fort);

$stolenData = $fortReader('secretPlans');

The first argument is the object that will become $this inside $fortReader. The second argument is the class that will be self, which determines what methods are accessible. (Specifying an object means it will use the class of that object.) To be honest, I have yet to come across a use case where I don't want to use the same object for both arguments.

In practice, the closure is so small that I often find myself doing it all in one line:

$propReader = (fn (string $prop): mixed => $this->$prop ?? null)->bindTo($fort, $fort);

A closure of this sort can just as easily write a value as read it. Usually I want to write several properties at once, leading to this simple utility:

$populator = function (array $props) {
    foreach ($props as $k => $v) {
        $this->$k = $v;
    }
};


$populator->call($new, $props);
// or
$populator->bindTo($new, $new)($props);

Again, more purpose-built options are also possible.

At this point you can probably guess how to call private methods, but just in case not here's my method (no pun intended):

$methodCaller = fn (string $fn) => $this->$fn();

$methodCaller->call($fort, 'buryTreasure');
// or
$invoker = $this->methodCaller->bindTo($fort, $fort);
$invoker('buryTreasure');

If you want to be able to pass through arguments, try this instead:

$methodCaller = fn (string $fn, mixed ...$args) => $this->$fn(...$args);

Now don't use it

This is an effective way to do meta-programming that requires operating on arbitrary objects' data regardless of their visibility. It's a simple, compact track that works well across PHP versions. And you should probably never use it, because odds are you are not writing an ORM or serializer. If you are, this is a simple and elegant way to just ignore visibility.

If you're not, then please don't ignore visibility. :-)

Sort:  

Just found a use case of this.

$routes = \Route::getRoutes();
$caller = fn() => $this->routes;
dd($caller->call($routes)['GET']);

Very useful, thank you.