Skip to main content

Hijacking your code with streams

26 Oct 2015
Tags: php streams

Lately, you might have noticed that streams are getting more important in the PHP scene. They are for example widely used in PSR-7 and implemented in Diactoros. Yesterday I was scrolling down the issues list of Phpspec and bumped in on this pull request. This pull request shows that it is possible to convert code in-memory and later require the modified content to be executed. Wow, that is awesome! Let's take a look at it.

The concept

You probably already used some built-in PHP streams like regular files, `php://memory` or `data://...` in combination with the `fopen()` method or the `StdFileObject` class. Beside those built-in streams, it is possible to configure your own stream wrappers. For this post I will be creating a blacklist stream that will remove blacklisted methods from your code. The blacklist stream wrapper will read a PHP file and replaces blacklisted methods from the code. When the file is required through the blacklist wrapper, all blacklisted methods will be removed from the code.

Show me some code

<?php

class StreamWrapper
{

    private $realPath;
    private $fileResource;
    private static $blacklistedMethods = [];

    public static function blacklistMethod($method)
    {
        self::$blacklistedMethods[] = $method;
    }

    public function stream_open($path, $mode, $options, &$opened_path)
    {
        $this->realPath = preg_replace('|^blacklist://|', '', $path);
        if (!file_exists($this->realPath)) {
            return false;
        }

        $content = file_get_contents($this->realPath);
        foreach (self::$blacklistedMethods as $blacklistedMethod) {
            $content = preg_replace('/^' . $blacklistedMethod . '\([^\)]*\);$/im', '', $content);
        }

        $this->fileResource = fopen('php://memory', 'w+');
        fwrite($this->fileResource, $content);
        rewind($this->fileResource);

        $opened_path = $this->realPath;
        return true;
    }

    public function stream_stat()
    {
        return stat($this->realPath);
    }

    public function stream_read($count)
    {
        return fread($this->fileResource, $count);
    }

    public function stream_eof()
    {
        return feof($this->fileResource);
    }
}

Usage

To use the stream wrapper, you have to register it with the name blacklist. Next you can require the file with the new stream URL. Internally PHP will open the file with your custom stream reader. This means that you can alter the content of the PHP file in an in-memory stream which will be used by PHP. Here is what the file looks like.

<?php
// run.php

require('StreamWrapper.php');
StreamWrapper::blacklistMethod('die');
stream_wrapper_register('blacklist', 'StreamWrapper');

require 'blacklist://test.php';

The above code needs a file test.php. This file looks like this:

<?php
// test.php

die('now you see me ...');
echo('now you dont!');

When executing the run.php you will see the result:

$ php run.php
now you dont!

What the hell did I just read…

I know, most projects won't need in-memory code adjustments. The blacklist wrapper in this post will probably never be needed, but it is used to show you what is possible. Streams are pretty cool and will probably become more important in the future.

whois VeeWee

Selfie

Hi there!

Glad you made it to my blog. Please feel free to take a look around. You will find some interesting stuff, mostly about web development and PHP.

Still can't get enough of me? Quick! Take a look at my Speakerdeck, Twitter, PHPC.Social, or Github account.