<?php
/**
* File the for PHONValidator class.
* @package PHON
*/
/**
* An utility class that validates PHON, preventing possible
* security threats that could come from executing unvalidated
* PHON data. It won't check that the PHON represents valid PHP
* code, just that there is no harmful code.
* @package PHON
*/
final class PHONValidator {
/**
* Singleton instance.
* @var PHONValidator
*/
private static $instance;
/**
* Singleton access.
* @return PHONValidator Singleton instance.
*/
public static function getInstance() {
if (!self::$instance) {
self::$instance = new PHONValidator();
}
return self::$instance;
}
/**
* An array of classes associated with a boolean flag which
* indicates if this value can be safely created from PHON data.
* If the class is not in the array it must be checked through
* Reflection.
* @var array
*/
private $classes;
/**
* Constructor.
*/
private function __construct() {
$this->classes = array();
}
/**
* Indicates if a class can be safely created from PHON data.
* @param string $classname The class name.
* @return bool True if the class can be safely created from PHON data.
*/
public function isSecureClass($classname) {
if (!isset($this->classes[$classname])) {
$class = new ReflectionClass($classname);
$this->classes[$classname] = $class->implementsInterface('SecurePHONClass');
}
return $this->classes[$classname];
}
/**
* Checks if PHON data can be safely executed as PHP code.
* @param string $phon The PHON data.
* @return bool True if the PHON data is secure.
*/
public function isSecure($phon) {
$tokens = token_get_all("<?php $phon");
// we were forced to add the first token, so we remove it
array_shift($tokens);
for ($i = 0; $i < count($tokens); $i++) {
$token = $tokens[$i];
// check one char secure tokens
if ($token === ',') continue;
if ($token === '(') continue;
if ($token === ')') continue;
// no other one char token is secure
if (!is_array($token)) return false;
// whitespace and comments are ok
if ($token[0] === T_WHITESPACE) continue;
if ($token[0] === T_COMMENT) continue;
if ($token[0] === T_DOC_COMMENT) continue;
// literals are ok
if ($token[0] === T_CONSTANT_ENCAPSED_STRING) continue;
if ($token[0] === T_LNUMBER) continue;
if ($token[0] === T_DNUMBER) continue;
// array and => are ok
if ($token[0] === T_ARRAY) continue;
if ($token[0] === T_DOUBLE_ARROW) continue;
// T_STRING must be handled carefully
if ($token[0] === T_STRING) {
// T_STRING is ok if followed by ::__set_state
// and is the name of a secure class
if ($this->lookAheadSetStateCall($tokens, $i)) {
if ($this->isSecureClass($token[1])) continue;
return false;
}
// T_STRING is ok if represents a boolean literal
if ($token[1] === 'true') continue;
if ($token[1] === 'false') continue;
return false;
}
// otherwise, we found something we shouldn't
return false;
}
// nothing suspicious to report
return true;
}
/**
* Checks subsequent tokens for the pattern ::__set_state.
* @param array $tokens The list of tokens.
* @param int $i The index of the current token.
* If the pattern is found, this variable will point
* to the place where the __set_state was found.
* @return bool True if the pattern was found.
*/
private function lookAheadSetStateCall($tokens, &$i) {
$foundDoubleColon = false;
for ($j = $i+1; $j < count($tokens); $j++) {
$token = $tokens[$j];
// we do not expect one char tokens
if (!is_array($tokens)) return false;
// ignore whitespace and comments
if ($token[0] === T_WHITESPACE) continue;
if ($token[0] === T_COMMENT) continue;
if ($token[0] === T_DOC_COMMENT) continue;
// look for double colon
if (!$foundDoubleColon) {
if ($token[0] === T_DOUBLE_COLON) {
$foundDoubleColon = true;
continue;
}
return false;
}
// look for __set_state
if ($token[0] === T_STRING) {
if ($token[1] === '__set_state') {
$i = $j;
return true;
}
}
// we didn't find what we were looking for
return false;
}
// we ran out of tokens
return false;
}
}
|