Skip to content

Commit

Permalink
better
Browse files Browse the repository at this point in the history
  • Loading branch information
JanTvrdik committed May 9, 2016
1 parent 1c5f608 commit 35fa92f
Show file tree
Hide file tree
Showing 9 changed files with 483 additions and 14 deletions.
8 changes: 6 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@
},
"require-dev": {
"nette/di": "~2.2",
"nette/robot-loader": "~2.2",
"nette/tester": "~1.3",
"tracy/tracy": "~2.2",
"mockery/mockery": "~0.9"
"mockery/mockery": "~0.9",
"nikic/php-parser": "~2.0"
},
"suggest": {
"nette/di": "to use SecuredLinksExtension",
"nikic/php-parser": "to detect return types without @return annotation"
},
"extra": {
"branch-alias": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

/**
* This file is part of the Nextras Secured Links library.
*
* @license MIT
* @link https://github.com/nextras/secured-links
*/

namespace Nextras\SecuredLinks;
namespace Nextras\SecuredLinks\Bridges\NetteDI;

use Generator;
use Nette;
Expand All @@ -15,16 +16,22 @@
use Nette\DI\PhpReflection;
use Nette\Neon\Neon;
use Nette\Utils\Strings;
use Nextras\SecuredLinks\Bridges\PhpParser\ReturnTypeResolver;
use Nextras\SecuredLinks\RedirectChecker;
use Nextras\SecuredLinks\SecuredRouterFactory;
use PhpParser\Node;
use ReflectionClass;
use ReflectionMethod;


class SecuredLinksExtension extends Nette\DI\CompilerExtension
{

/** @var array */
public $defaults = [
'annotation' => 'secured', // can be NULL to disable
'destinations' => [],
'strictMode' => TRUE,
];


Expand Down Expand Up @@ -52,6 +59,12 @@ public function beforeCompile()
->setClass(IRouter::class)
->setFactory("@{$this->name}.routerFactory::create", ["@$innerRouter"])
->setAutowired(TRUE);

$builder->addDefinition($this->prefix('redirectChecker'))
->setClass(RedirectChecker::class);

$builder->getDefinition($builder->getByType(Nette\Application\Application::class))
->addSetup('?::$onResponse[] = ?', ['@self', '@Nextras\SecuredLinks\RedirectChecker']);
}


Expand Down Expand Up @@ -88,18 +101,20 @@ private function findSecuredDestinations()
{
$config = $this->validateConfig($this->defaults);

foreach ($config['destinations'] as $presenterClass => $destinations) {
yield $presenterClass => $destinations;
}

if ($config['annotation']) {
$presenters = $this->getContainerBuilder()->findByType(Presenter::class);
foreach ($presenters as $presenterDef) {
$presenterClass = $presenterDef->getClass();
$presenterRef = new \ReflectionClass($presenterClass);
yield $presenterClass => $this->findSecuredMethods($presenterRef);
if (!isset($config['destinations'][$presenterClass])) {
$presenterRef = new \ReflectionClass($presenterClass);
yield $presenterClass => $this->findSecuredMethods($presenterRef);
}
}
}

foreach ($config['destinations'] as $presenterClass => $destinations) {
yield $presenterClass => $destinations;
}
}


Expand Down Expand Up @@ -135,13 +150,20 @@ private function findTargetMethods(ReflectionClass $classRef)
yield $destination => $methodRef;

} elseif (Strings::startsWith($methodName, 'createComponent')) {
$returnType = PhpReflection::getReturnType($methodRef);
$returnType = $this->getMethodReturnType($methodRef);
if ($returnType !== NULL) {
$returnTypeRef = new ReflectionClass($returnType);
$componentName = Strings::firstLower(Strings::after($methodName, 'createComponent'));
foreach ($this->findTargetMethods($returnTypeRef) as $innerDestination => $innerRef) {
yield "$componentName-$innerDestination" => $innerRef;
}

} elseif ($this->config['strictMode']) {
$className = $methodRef->getDeclaringClass()->getName();
throw new \LogicException(
"Unable to deduce return type for method $className::$methodName(); " .
"add @return annotation, install nikic/php-parser or disable strictMode in config"
);
}
}
}
Expand Down Expand Up @@ -169,4 +191,19 @@ private function isSecured(ReflectionMethod $ref, & $params)
return FALSE;
}
}


/**
* @param ReflectionMethod $methodRef
* @return NULL|string
*/
private function getMethodReturnType(ReflectionMethod $methodRef)
{
$returnType = PhpReflection::getReturnType($methodRef);
if ($returnType !== NULL || !interface_exists(\PhpParser\Node::class)) {
return $returnType;
} else {
return ReturnTypeResolver::getReturnType($methodRef);
}
}
}
188 changes: 188 additions & 0 deletions src/Bridges/PhpParser/ReturnTypeResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<?php

/**
* This file is part of the Nextras Secured Links library.
*
* @license MIT
* @link https://github.com/nextras/secured-links
*/

namespace Nextras\SecuredLinks\Bridges\PhpParser;

use Nette;
use Nette\DI\PhpReflection;
use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\NodeVisitorAbstract;
use PhpParser\ParserFactory;
use ReflectionMethod;


class ReturnTypeResolver extends NodeVisitorAbstract
{
/** @var string */
private $className;

/** @var string */
private $methodName;

/** @var string[] */
private $returnTypes = [];

/** @var string[][] */
private $varTypes = [];

/** @var bool */
private $inClass = FALSE;

/** @var bool */
private $inMethod = FALSE;


/**
* @param string $className
* @param string $methodName
*/
public function __construct($className, $methodName)
{
$this->className = $className;
$this->methodName = $methodName;
$this->varTypes['this'][] = $className;
}


/**
* @param ReflectionMethod $methodRef
* @return NULL|string
*/
public static function getReturnType(ReflectionMethod $methodRef)
{
$fileContent = file_get_contents($methodRef->getDeclaringClass()->getFileName());

$traverser = new NodeTraverser();
$traverser->addVisitor(new NameResolver);
$traverser->addVisitor($resolver = new self($methodRef->getDeclaringClass()->getName(), $methodRef->getName()));
$traverser->traverse((new ParserFactory)->create(ParserFactory::PREFER_PHP7)->parse($fileContent));

return count($resolver->returnTypes) === 1 ? $resolver->returnTypes[0] : NULL;
}


/**
* @inheritdoc
*/
public function enterNode(Node $node)
{
if ($node instanceof Node\Stmt\Class_ && $node->name === $this->className) {
$this->inClass = TRUE;

} elseif ($this->inClass && $node instanceof Node\Stmt\ClassMethod && $node->name === $this->methodName) {
$this->inMethod = TRUE;

} elseif ($this->inMethod) {
if ($node instanceof Node\Stmt\Return_ && $node->expr !== NULL) {
foreach ($this->getExpressionTypes($node->expr) as $type) {
$this->addReturnType($type);
}

} elseif ($node instanceof Node\Expr\Assign) {
foreach ($this->getExpressionTypes($node->expr) as $type) {
$this->addVarType($node, $type);
}
}
}
}


/**
* @inheritdoc
*/
public function leaveNode(Node $node)
{
if ($this->inMethod && $node instanceof Node\Stmt\ClassMethod) {
$this->inMethod = FALSE;

} elseif ($this->inClass && $node instanceof Node\Stmt\Class_) {
$this->inClass = FALSE;
}
}


/**
* @param Node\Expr $expr
* @return string[]
*/
private function getExpressionTypes(Node\Expr $expr)
{
$result = [];

if ($expr instanceof Node\Expr\New_) {
if ($expr->class instanceof Node\Name) {
$result[] = (string) $expr->class;
}

} elseif ($expr instanceof Node\Expr\Variable) {
if (is_string($expr->name) && isset($this->varTypes[$expr->name])) {
$result = $this->varTypes[$expr->name];
}

} elseif ($expr instanceof Node\Expr\PropertyFetch) {
if (is_string($expr->name)) {
foreach ($this->getExpressionTypes($expr->var) as $objType) {
$propertyRef = new \ReflectionProperty($objType, $expr->name);
$type = PhpReflection::parseAnnotation($propertyRef, 'var');
$type = $type ? PhpReflection::expandClassName($type, PhpReflection::getDeclaringClass($propertyRef)) : NULL;
$result[] = $type;
}
}

} elseif ($expr instanceof Node\Expr\MethodCall) {
if (is_string($expr->name)) {
foreach ($this->getExpressionTypes($expr->var) as $objType) {
$methodRef = new \ReflectionMethod($objType, $expr->name);
$result[] = PhpReflection::getReturnType($methodRef);
}
}

} elseif ($expr instanceof Node\Expr\Assign) {
foreach ($this->getExpressionTypes($expr->expr) as $type) {
$this->addVarType($expr, $type);
$result[] = $type;
}

} elseif ($expr instanceof Node\Expr\Clone_) {
$result = $this->getExpressionTypes($expr->expr);
}

return $result;
}


/**
* @param string $exprType
* @return void
*/
private function addReturnType($exprType)
{
if ($exprType !== NULL && class_exists($exprType) && !in_array($exprType, $this->returnTypes)) {
$this->returnTypes[] = $exprType;
}
}


/**
* @param Node\Expr\Assign $node
* @param string $exprType
* @return void
*/
private function addVarType($node, $exprType)
{
if ($node->var instanceof Node\Expr\Variable && is_string($node->var->name)
&& (empty($this->varTypes[$node->var->name]) || !in_array($exprType, $this->varTypes[$node->var->name]))
&& $exprType !== NULL && class_exists($exprType)
) {
$this->varTypes[$node->var->name][] = $exprType;
}
}
}
33 changes: 33 additions & 0 deletions src/RedirectChecker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/**
* This file is part of the Nextras Secured Links library.
* @license MIT
* @link https://github.com/nextras/secured-links
*/

namespace Nextras\SecuredLinks;

use Nette;
use Nette\Application\Application;
use Nette\Application\IResponse;
use Nette\Application\Responses\RedirectResponse;


class RedirectChecker
{
/**
* @param Application $app
* @param IResponse $response
* @return void
*/
public function checkResponse(Application $app, IResponse $response)
{
$requests = $app->getRequests();
$request = $requests[count($requests) - 1];

if ($request->hasFlag(SecuredRouter::SIGNED) && !$response instanceof RedirectResponse) {
throw new \LogicException('Secured request did not redirect. Possible CSRF-token reveal by HTTP referer header.');
}
}
}
7 changes: 5 additions & 2 deletions src/SecuredRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@

class SecuredRouter implements IRouter
{
/** signed flag, marks requests which has been signed */
const SIGNED = 'signed';

/** length of secret token stored in session */
const SECURITY_TOKEN_LENGTH = 16;

Expand Down Expand Up @@ -57,8 +60,8 @@ public function __construct(IRouter $inner, IPresenterFactory $presenterFactory,
public function match(Nette\Http\IRequest $httpRequest)
{
$appRequest = $this->inner->match($httpRequest);
if ($appRequest !== NULL && !$this->isSignatureOk($appRequest)) {
return NULL;
if ($appRequest !== NULL && $this->isSignatureOk($appRequest)) {
$appRequest->setFlag(self::SIGNED);
}

return $appRequest;
Expand Down
Loading

0 comments on commit 35fa92f

Please sign in to comment.