Skip to content

Commit 70bf9d4

Browse files
committed
better
1 parent 1c5f608 commit 70bf9d4

File tree

9 files changed

+483
-14
lines changed

9 files changed

+483
-14
lines changed

composer.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@
1818
},
1919
"require-dev": {
2020
"nette/di": "~2.2",
21-
"nette/robot-loader": "~2.2",
2221
"nette/tester": "~1.3",
2322
"tracy/tracy": "~2.2",
24-
"mockery/mockery": "~0.9"
23+
"mockery/mockery": "~0.9",
24+
"nikic/php-parser": "~2.0"
25+
},
26+
"suggest": {
27+
"nette/di": "to use SecuredLinksExtension",
28+
"nikic/php-parser": "to detect return types without @return annotation"
2529
},
2630
"extra": {
2731
"branch-alias": {

src/SecuredLinksExtension.php renamed to src/Bridges/NetteDI/SecuredLinksExtension.php

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
/**
44
* This file is part of the Nextras Secured Links library.
5+
*
56
* @license MIT
67
* @link https://github.com/nextras/secured-links
78
*/
89

9-
namespace Nextras\SecuredLinks;
10+
namespace Nextras\SecuredLinks\Bridges\NetteDI;
1011

1112
use Generator;
1213
use Nette;
@@ -15,16 +16,22 @@
1516
use Nette\DI\PhpReflection;
1617
use Nette\Neon\Neon;
1718
use Nette\Utils\Strings;
19+
use Nextras\SecuredLinks\Bridges\PhpParser\ReturnTypeResolver;
20+
use Nextras\SecuredLinks\RedirectChecker;
21+
use Nextras\SecuredLinks\SecuredRouterFactory;
22+
use PhpParser\Node;
1823
use ReflectionClass;
1924
use ReflectionMethod;
2025

2126

2227
class SecuredLinksExtension extends Nette\DI\CompilerExtension
2328
{
29+
2430
/** @var array */
2531
public $defaults = [
2632
'annotation' => 'secured', // can be NULL to disable
2733
'destinations' => [],
34+
'strictMode' => TRUE,
2835
];
2936

3037

@@ -52,6 +59,12 @@ public function beforeCompile()
5259
->setClass(IRouter::class)
5360
->setFactory("@{$this->name}.routerFactory::create", ["@$innerRouter"])
5461
->setAutowired(TRUE);
62+
63+
$builder->addDefinition($this->prefix('redirectChecker'))
64+
->setClass(RedirectChecker::class);
65+
66+
$builder->getDefinition($builder->getByType(Nette\Application\Application::class))
67+
->addSetup('?->onResponse[] = [?, ?]', ['@self', '@Nextras\SecuredLinks\RedirectChecker', 'checkResponse']);
5568
}
5669

5770

@@ -88,18 +101,20 @@ private function findSecuredDestinations()
88101
{
89102
$config = $this->validateConfig($this->defaults);
90103

104+
foreach ($config['destinations'] as $presenterClass => $destinations) {
105+
yield $presenterClass => $destinations;
106+
}
107+
91108
if ($config['annotation']) {
92109
$presenters = $this->getContainerBuilder()->findByType(Presenter::class);
93110
foreach ($presenters as $presenterDef) {
94111
$presenterClass = $presenterDef->getClass();
95-
$presenterRef = new \ReflectionClass($presenterClass);
96-
yield $presenterClass => $this->findSecuredMethods($presenterRef);
112+
if (!isset($config['destinations'][$presenterClass])) {
113+
$presenterRef = new \ReflectionClass($presenterClass);
114+
yield $presenterClass => $this->findSecuredMethods($presenterRef);
115+
}
97116
}
98117
}
99-
100-
foreach ($config['destinations'] as $presenterClass => $destinations) {
101-
yield $presenterClass => $destinations;
102-
}
103118
}
104119

105120

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

137152
} elseif (Strings::startsWith($methodName, 'createComponent')) {
138-
$returnType = PhpReflection::getReturnType($methodRef);
153+
$returnType = $this->getMethodReturnType($methodRef);
139154
if ($returnType !== NULL) {
140155
$returnTypeRef = new ReflectionClass($returnType);
141156
$componentName = Strings::firstLower(Strings::after($methodName, 'createComponent'));
142157
foreach ($this->findTargetMethods($returnTypeRef) as $innerDestination => $innerRef) {
143158
yield "$componentName-$innerDestination" => $innerRef;
144159
}
160+
161+
} elseif ($this->config['strictMode']) {
162+
$className = $methodRef->getDeclaringClass()->getName();
163+
throw new \LogicException(
164+
"Unable to deduce return type for method $className::$methodName(); " .
165+
"add @return annotation, install nikic/php-parser or disable strictMode in config"
166+
);
145167
}
146168
}
147169
}
@@ -169,4 +191,19 @@ private function isSecured(ReflectionMethod $ref, & $params)
169191
return FALSE;
170192
}
171193
}
194+
195+
196+
/**
197+
* @param ReflectionMethod $methodRef
198+
* @return NULL|string
199+
*/
200+
private function getMethodReturnType(ReflectionMethod $methodRef)
201+
{
202+
$returnType = PhpReflection::getReturnType($methodRef);
203+
if ($returnType !== NULL || !interface_exists(\PhpParser\Node::class)) {
204+
return $returnType;
205+
} else {
206+
return ReturnTypeResolver::getReturnType($methodRef);
207+
}
208+
}
172209
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Nextras Secured Links library.
5+
*
6+
* @license MIT
7+
* @link https://github.com/nextras/secured-links
8+
*/
9+
10+
namespace Nextras\SecuredLinks\Bridges\PhpParser;
11+
12+
use Nette;
13+
use Nette\DI\PhpReflection;
14+
use PhpParser\Node;
15+
use PhpParser\NodeTraverser;
16+
use PhpParser\NodeVisitor\NameResolver;
17+
use PhpParser\NodeVisitorAbstract;
18+
use PhpParser\ParserFactory;
19+
use ReflectionMethod;
20+
21+
22+
class ReturnTypeResolver extends NodeVisitorAbstract
23+
{
24+
/** @var string */
25+
private $className;
26+
27+
/** @var string */
28+
private $methodName;
29+
30+
/** @var string[] */
31+
private $returnTypes = [];
32+
33+
/** @var string[][] */
34+
private $varTypes = [];
35+
36+
/** @var bool */
37+
private $inClass = FALSE;
38+
39+
/** @var bool */
40+
private $inMethod = FALSE;
41+
42+
43+
/**
44+
* @param string $className
45+
* @param string $methodName
46+
*/
47+
public function __construct($className, $methodName)
48+
{
49+
$this->className = $className;
50+
$this->methodName = $methodName;
51+
$this->varTypes['this'][] = $className;
52+
}
53+
54+
55+
/**
56+
* @param ReflectionMethod $methodRef
57+
* @return NULL|string
58+
*/
59+
public static function getReturnType(ReflectionMethod $methodRef)
60+
{
61+
$fileContent = file_get_contents($methodRef->getDeclaringClass()->getFileName());
62+
63+
$traverser = new NodeTraverser();
64+
$traverser->addVisitor(new NameResolver);
65+
$traverser->addVisitor($resolver = new self($methodRef->getDeclaringClass()->getName(), $methodRef->getName()));
66+
$traverser->traverse((new ParserFactory)->create(ParserFactory::PREFER_PHP7)->parse($fileContent));
67+
68+
return count($resolver->returnTypes) === 1 ? $resolver->returnTypes[0] : NULL;
69+
}
70+
71+
72+
/**
73+
* @inheritdoc
74+
*/
75+
public function enterNode(Node $node)
76+
{
77+
if ($node instanceof Node\Stmt\Class_ && $node->name === $this->className) {
78+
$this->inClass = TRUE;
79+
80+
} elseif ($this->inClass && $node instanceof Node\Stmt\ClassMethod && $node->name === $this->methodName) {
81+
$this->inMethod = TRUE;
82+
83+
} elseif ($this->inMethod) {
84+
if ($node instanceof Node\Stmt\Return_ && $node->expr !== NULL) {
85+
foreach ($this->getExpressionTypes($node->expr) as $type) {
86+
$this->addReturnType($type);
87+
}
88+
89+
} elseif ($node instanceof Node\Expr\Assign) {
90+
foreach ($this->getExpressionTypes($node->expr) as $type) {
91+
$this->addVarType($node, $type);
92+
}
93+
}
94+
}
95+
}
96+
97+
98+
/**
99+
* @inheritdoc
100+
*/
101+
public function leaveNode(Node $node)
102+
{
103+
if ($this->inMethod && $node instanceof Node\Stmt\ClassMethod) {
104+
$this->inMethod = FALSE;
105+
106+
} elseif ($this->inClass && $node instanceof Node\Stmt\Class_) {
107+
$this->inClass = FALSE;
108+
}
109+
}
110+
111+
112+
/**
113+
* @param Node\Expr $expr
114+
* @return string[]
115+
*/
116+
private function getExpressionTypes(Node\Expr $expr)
117+
{
118+
$result = [];
119+
120+
if ($expr instanceof Node\Expr\New_) {
121+
if ($expr->class instanceof Node\Name) {
122+
$result[] = (string) $expr->class;
123+
}
124+
125+
} elseif ($expr instanceof Node\Expr\Variable) {
126+
if (is_string($expr->name) && isset($this->varTypes[$expr->name])) {
127+
$result = $this->varTypes[$expr->name];
128+
}
129+
130+
} elseif ($expr instanceof Node\Expr\PropertyFetch) {
131+
if (is_string($expr->name)) {
132+
foreach ($this->getExpressionTypes($expr->var) as $objType) {
133+
$propertyRef = new \ReflectionProperty($objType, $expr->name);
134+
$type = PhpReflection::parseAnnotation($propertyRef, 'var');
135+
$type = $type ? PhpReflection::expandClassName($type, PhpReflection::getDeclaringClass($propertyRef)) : NULL;
136+
$result[] = $type;
137+
}
138+
}
139+
140+
} elseif ($expr instanceof Node\Expr\MethodCall) {
141+
if (is_string($expr->name)) {
142+
foreach ($this->getExpressionTypes($expr->var) as $objType) {
143+
$methodRef = new \ReflectionMethod($objType, $expr->name);
144+
$result[] = PhpReflection::getReturnType($methodRef);
145+
}
146+
}
147+
148+
} elseif ($expr instanceof Node\Expr\Assign) {
149+
foreach ($this->getExpressionTypes($expr->expr) as $type) {
150+
$this->addVarType($expr, $type);
151+
$result[] = $type;
152+
}
153+
154+
} elseif ($expr instanceof Node\Expr\Clone_) {
155+
$result = $this->getExpressionTypes($expr->expr);
156+
}
157+
158+
return $result;
159+
}
160+
161+
162+
/**
163+
* @param string $exprType
164+
* @return void
165+
*/
166+
private function addReturnType($exprType)
167+
{
168+
if ($exprType !== NULL && class_exists($exprType) && !in_array($exprType, $this->returnTypes)) {
169+
$this->returnTypes[] = $exprType;
170+
}
171+
}
172+
173+
174+
/**
175+
* @param Node\Expr\Assign $node
176+
* @param string $exprType
177+
* @return void
178+
*/
179+
private function addVarType($node, $exprType)
180+
{
181+
if ($node->var instanceof Node\Expr\Variable && is_string($node->var->name)
182+
&& (empty($this->varTypes[$node->var->name]) || !in_array($exprType, $this->varTypes[$node->var->name]))
183+
&& $exprType !== NULL && class_exists($exprType)
184+
) {
185+
$this->varTypes[$node->var->name][] = $exprType;
186+
}
187+
}
188+
}

src/RedirectChecker.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Nextras Secured Links library.
5+
* @license MIT
6+
* @link https://github.com/nextras/secured-links
7+
*/
8+
9+
namespace Nextras\SecuredLinks;
10+
11+
use Nette;
12+
use Nette\Application\Application;
13+
use Nette\Application\IResponse;
14+
use Nette\Application\Responses\RedirectResponse;
15+
16+
17+
class RedirectChecker
18+
{
19+
/**
20+
* @param Application $app
21+
* @param IResponse $response
22+
* @return void
23+
*/
24+
public function checkResponse(Application $app, IResponse $response)
25+
{
26+
$requests = $app->getRequests();
27+
$request = $requests[count($requests) - 1];
28+
29+
if ($request->hasFlag(SecuredRouter::SIGNED) && !$response instanceof RedirectResponse) {
30+
throw new \LogicException('Secured request did not redirect. Possible CSRF-token reveal by HTTP referer header.');
31+
}
32+
}
33+
}

src/SecuredRouter.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
class SecuredRouter implements IRouter
1919
{
20+
/** signed flag, marks requests which has been signed */
21+
const SIGNED = 'signed';
22+
2023
/** length of secret token stored in session */
2124
const SECURITY_TOKEN_LENGTH = 16;
2225

@@ -57,8 +60,8 @@ public function __construct(IRouter $inner, IPresenterFactory $presenterFactory,
5760
public function match(Nette\Http\IRequest $httpRequest)
5861
{
5962
$appRequest = $this->inner->match($httpRequest);
60-
if ($appRequest !== NULL && !$this->isSignatureOk($appRequest)) {
61-
return NULL;
63+
if ($appRequest !== NULL && $this->isSignatureOk($appRequest)) {
64+
$appRequest->setFlag(self::SIGNED);
6265
}
6366

6467
return $appRequest;

0 commit comments

Comments
 (0)