-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Detect CRLF URL attempts and fight back with maybe humor
URLs like `/%0DSet-Cookie:...` would throw "PHP Warning: Header may not contain more than a single header, new line detected in [...]Http/Response.php:98" because of the `IResponse::redirect()` call in `WebApplication::redirectToSecure()`. Let's detect such attempts and stop them in their tracks.
- Loading branch information
Showing
6 changed files
with
156 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
<?php | ||
declare(strict_types = 1); | ||
|
||
namespace MichalSpacekCz\EasterEgg; | ||
|
||
use DateTimeImmutable; | ||
use Nette\Http\IRequest; | ||
use Nette\Http\IResponse; | ||
use Nette\Utils\Strings; | ||
|
||
readonly class CrLfUrlInjections | ||
{ | ||
|
||
private const COOKIE_NAME = 'crlfinjection'; | ||
|
||
|
||
public function __construct( | ||
private IRequest $httpRequest, | ||
private IResponse $httpResponse, | ||
) { | ||
} | ||
|
||
|
||
public function detectAttempt(): bool | ||
{ | ||
$url = $this->httpRequest->getUrl()->getAbsoluteUrl(); | ||
if (!str_contains($url, "\r") && !str_contains($url, "\n")) { | ||
return false; | ||
} | ||
$matches = Strings::matchAll($url, sprintf('/Set\-Cookie:%s=([a-z0-9]+)/i', self::COOKIE_NAME)); | ||
foreach ($matches as $match) { | ||
// Don't use any cookie name from the request to avoid e.g. session fixation | ||
$this->httpResponse->setCookie( | ||
self::COOKIE_NAME, | ||
$match[1], | ||
new DateTimeImmutable('-3 years 1 month 3 days 3 hours 7 minutes'), | ||
'/expired=3years/1month/3days/3hours/7minutes/ago', | ||
); | ||
} | ||
$this->httpResponse->setCode(IResponse::S204_NoContent, 'U WOT M8'); | ||
$this->httpResponse->setHeader('Hack-the-Planet', 'https://youtu.be/u3CKgkyc7Qo?t=20'); | ||
return true; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
<?php | ||
declare(strict_types = 1); | ||
|
||
namespace MichalSpacekCz\EasterEgg; | ||
|
||
use MichalSpacekCz\Test\Http\Request; | ||
use MichalSpacekCz\Test\Http\Response; | ||
use MichalSpacekCz\Test\TestCaseRunner; | ||
use Nette\Http\IResponse; | ||
use Nette\Http\UrlScript; | ||
use Override; | ||
use Tester\Assert; | ||
use Tester\TestCase; | ||
|
||
require __DIR__ . '/../bootstrap.php'; | ||
|
||
/** @testCase */ | ||
class CrLfUrlInjectionsTest extends TestCase | ||
{ | ||
|
||
public function __construct( | ||
private readonly CrLfUrlInjections $crLfUrlInjections, | ||
private readonly Request $request, | ||
private readonly Response $response, | ||
) { | ||
} | ||
|
||
|
||
#[Override] | ||
protected function tearDown() | ||
{ | ||
$this->response->reset(); | ||
} | ||
|
||
|
||
/** | ||
* @return non-empty-list<array{0:string, 1:bool, 2:int}> | ||
*/ | ||
public function getUrls(): array | ||
{ | ||
return [ | ||
['/foo', false, 0], | ||
['/foo/Set-Cookie:crlfinjection=1337', false, 0], | ||
['/foo%0A', true, 0], | ||
['/foo%0ASetCookie:crlfinjection=1337', true, 0], | ||
['/foo%0ASet-Cookie:crlfinjection=1337', true, 1], | ||
['/foo%0D', true, 0], | ||
['/foo%0DSetCookie:crlfinjection=1337', true, 0], | ||
['/foo%0DSet-Cookie:crlfinjection=1337', true, 1], | ||
['/foo%0D%0A', true, 0], | ||
['/foo%0D%0ASetCookie:crlfinjection=1337', true, 0], | ||
['/foo%0D%0ASet-Cookie:crlfinjection=1337', true, 1], | ||
['/foo%0D%0ASet-Cookie:PHPSESSID=1338', true, 0], | ||
]; | ||
} | ||
|
||
|
||
/** @dataProvider getUrls */ | ||
public function testDetectAttempt(string $path, bool $attempt, int $cookies): void | ||
{ | ||
$this->request->setUrl((new UrlScript())->withPath(urldecode($path))); | ||
if ($attempt) { | ||
Assert::same($attempt, $this->crLfUrlInjections->detectAttempt()); | ||
Assert::same(IResponse::S204_NoContent, $this->response->getCode()); | ||
Assert::same('U WOT M8', $this->response->getReason()); | ||
Assert::count($cookies, $this->response->getCookie('crlfinjection')); | ||
if ($cookies > 1) { | ||
Assert::same('1337', $this->response->getCookie('crlfinjection')[0]->getValue()); | ||
} | ||
} else { | ||
Assert::false($this->crLfUrlInjections->detectAttempt()); | ||
Assert::same(IResponse::S200_OK, $this->response->getCode()); | ||
Assert::null($this->response->getReason()); | ||
Assert::same([], $this->response->getCookie('crlfinjection')); | ||
} | ||
} | ||
|
||
} | ||
|
||
TestCaseRunner::run(CrLfUrlInjectionsTest::class); |