lisachenko / z-engine

:zap: PHP Engine Direct API
MIT License
448 stars 22 forks source link

Is it possible to handle '===' opcode? #39

Open dzentota opened 4 years ago

dzentota commented 4 years ago

How to intercept is identical (===) check ? e.g.

$o1 == $o2;

will trigger __compare() handler, while

$o1 === $o2;

doesn't trigger neither compare() nor doOperation() with \ZEngine\System\OpCode::IS_IDENTICAL;

lisachenko commented 4 years ago

As I know, is identical implemented directly as an opcode handler and it doesn't call object handlers at all.

But you can install an opcode handler for OpCode::IS_IDENTICAL code via OpCode->setHandler() to handle comparison manually. But this handler could be changed soon to general hook scheme.

dzentota commented 4 years ago

Thank you for quick reply. As I understood:

\ZEngine\System\OpCode::setHandler(\ZEngine\System\OpCode::IS_IDENTICAL,
    function (\ZEngine\System\ExecutionData $executionState): int {
       echo 'is_identical';
    });
$o1 === $o2;

it should echo is_identical, but I see nothing;

Actually what I want to archive - I want to do taint analysis based on your lib. So I need a wrapper class (ValueObject) which can behave like the value it wraps in most cases and in the same time give the ability to call some methods on it. POC:

<?php

use ZEngine\ClassExtension\ObjectCastInterface;
use ZEngine\ClassExtension\ObjectCompareValuesInterface;
use ZEngine\ClassExtension\ObjectCreateInterface;
use ZEngine\ClassExtension\ObjectCreateTrait;
use ZEngine\ClassExtension\ObjectDoOperationInterface;

class Tainted implements
    ObjectCastInterface,
    ObjectCreateInterface,
    ObjectCompareValuesInterface,
    ObjectDoOperationInterface
{
    use ObjectCreateTrait;

    private $value;
    private $isSafe = false;

    public function __construct($value)
    {
        $this->value = $value;
    }

    public function value()
    {
        return $this->value;
    }

    public function markSafe()
    {
        $this->isSafe = true;
    }

    public function markUnsafe()
    {
        $this->isSafe = false;
    }

   public function isSafe(): bool
   {
        return $this->isSafe;
   }

    /**
     * Performs comparison of given object with another value
     *
     * @param \ZEngine\ClassExtension\Hook\CompareValuesHook $hook Instance of current hook
     *
     * @return int Result of comparison: 1 is greater, -1 is less, 0 is equal
     */
    public static function __compare(\ZEngine\ClassExtension\Hook\CompareValuesHook $hook): int
    {
        $first = $hook->getFirst();
        $second = $hook->getSecond();
        if (is_object($first) && get_class($first) === __CLASS__) {
            $first = $first->value();
        }
        if (is_object($second) && get_class($second) === __CLASS__) {
            $second = $second->value();
        }
        return $first <=> $second;
    }

    /**
     * Performs an operation on given object
     *
     * @param \ZEngine\ClassExtension\Hook\DoOperationHook $hook Instance of current hook
     *
     * @return mixed Result of operation value
     */
    public static function __doOperation(\ZEngine\ClassExtension\Hook\DoOperationHook $hook)
    {
        $first = $hook->getFirst();
        $second = $hook->getSecond();
        if (is_object($first) && get_class($first) === __CLASS__) {
            $first = $first->value();
        }
        if (is_object($second) && get_class($second) === __CLASS__) {
            $second = $second->value();
        }
        $opcode = $hook->getOpcode();
        switch (true) {
            case $opcode === \ZEngine\System\OpCode::ADD;
                return new Tainted($first + $second);
            case $opcode === \ZEngine\System\OpCode::SUB:
                return new Tainted($first - $second);
            case $opcode === \ZEngine\System\OpCode::CONCAT:
                return new Tainted($first . $second);
            case $opcode === \ZEngine\System\OpCode::IS_IDENTICAL:
                return $first === $second;
           // Here should be handling for all other opcodes...
        }
    }

    /**
     * Performs casting of given object to another value
     *
     * @param \ZEngine\ClassExtension\Hook\CastObjectHook $hook Instance of current hook
     *
     * @return mixed Casted value
     */
    public static function __cast(\ZEngine\ClassExtension\Hook\CastObjectHook $hook)
    {
        return $hook->getObject()->value();
    }
}

I expect next behavior:

$foo = new Tainted('foo');
$bar = new Tainted('bar');
$one = new Tainted(1);
$two = new Tainted(2);
echo $foo . $bar; //foobar
$one === 1; //true
$one == 1; //true
$two > $one;//true
// also instance of Tainted should pass type hints
function add(int $a, int $b) {
    return $a + $b;
}
echo add($one, $two); //3
//And another key feature: ability to know function name in which tainted variable was passed
$xss = '<script>alert(1)</script>';
if (rand(0,1) {
    //xss mitigation. **Tainted** should be some how notified in case it was to htmlspecialchars()
    $xss = htmlspecialchars($xss); 
}
var_dump($xss->isSafe());

Having such class I think it should be possible to wrap any variable at the beginning of the script and check if it safe at the end.

lisachenko commented 4 years ago

I'm not sure if your idea is possible right now, because messing with typehints and value boxing/unboxing in runtime is unpredictable. Of course, it is possible to wrap everything during AST processing, but object will violate your int typehint anyway.

Regarding your question about opcode handler: you should install it before the first inclusion of file that contains operations itself. PHP checks if specific opcode has user handler and generates specific version of opcodes to invoke custom handler. If you try to install handler after that - nothing will work...

dzentota commented 4 years ago

Thank you. Yes, it triggers handler if I put it in a separate file. Could provide a little example of how two write such handlers. It's not clear how to access $arguments and which integer should be returned. e.g. if I want to return true if my objects are identical:

$o1 === $o2;
\ZEngine\System\OpCode::setHandler(\ZEngine\System\OpCode::IS_IDENTICAL,
    function (\ZEngine\System\ExecutionData $executionState): int {
        //$executionState->getArguments() returns empty array. $o1 and $o2 expected
        return Core::ZEND_USER_OPCODE_DISPATCH;// What should be returned here?
    });