-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Security: Added new SSR allow list and validator
Included unit tests to cover validator functionality. Added to webhooks. Still need to do testing specifically for webhooks.
- Loading branch information
1 parent
9100a82
commit c324ad9
Showing
5 changed files
with
137 additions
and
0 deletions.
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
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,64 @@ | ||
<?php | ||
|
||
namespace BookStack\Util; | ||
|
||
use BookStack\Exceptions\HttpFetchException; | ||
|
||
class SsrUrlValidator | ||
{ | ||
protected string $config; | ||
|
||
public function __construct(string $config = null) | ||
{ | ||
$this->config = $config ?? config('app.ssr_hosts') ?? ''; | ||
} | ||
|
||
/** | ||
* @throws HttpFetchException | ||
*/ | ||
public function ensureAllowed(string $url): void | ||
{ | ||
if (!$this->allowed($url)) { | ||
throw new HttpFetchException(trans('errors.http_ssr_url_no_match')); | ||
} | ||
} | ||
|
||
/** | ||
* Check if the given URL is allowed by the configured SSR host values. | ||
*/ | ||
public function allowed(string $url): bool | ||
{ | ||
$allowed = $this->getHostPatterns(); | ||
|
||
foreach ($allowed as $pattern) { | ||
if ($this->urlMatchesPattern($url, $pattern)) { | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
protected function urlMatchesPattern($url, $pattern): bool | ||
{ | ||
$pattern = trim($pattern); | ||
$url = trim($url); | ||
|
||
if (empty($pattern) || empty($url)) { | ||
return false; | ||
} | ||
|
||
$quoted = preg_quote($pattern, '/'); | ||
$regexPattern = str_replace('\*', '.*', $quoted); | ||
|
||
return preg_match('/^' . $regexPattern . '.*$/i', $url); | ||
} | ||
|
||
/** | ||
* @return string[] | ||
*/ | ||
protected function getHostPatterns(): array | ||
{ | ||
return explode(' ', strtolower($this->config)); | ||
} | ||
} |
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,59 @@ | ||
<?php | ||
|
||
namespace Tests\Unit; | ||
|
||
use BookStack\Exceptions\HttpFetchException; | ||
use BookStack\Util\SsrUrlValidator; | ||
use Tests\TestCase; | ||
|
||
class SsrUrlValidatorTest extends TestCase | ||
{ | ||
public function test_allowed() | ||
{ | ||
$testMap = [ | ||
// Single values | ||
['config' => '', 'url' => '', 'result' => false], | ||
['config' => '', 'url' => 'https://example.com', 'result' => false], | ||
['config' => ' ', 'url' => 'https://example.com', 'result' => false], | ||
['config' => '*', 'url' => '', 'result' => false], | ||
['config' => '*', 'url' => 'https://example.com', 'result' => true], | ||
['config' => 'https://*', 'url' => 'https://example.com', 'result' => true], | ||
['config' => 'http://*', 'url' => 'https://example.com', 'result' => false], | ||
['config' => 'https://*example.com', 'url' => 'https://example.com', 'result' => true], | ||
['config' => 'https://*ample.com', 'url' => 'https://example.com', 'result' => true], | ||
['config' => 'https://*.example.com', 'url' => 'https://example.com', 'result' => false], | ||
['config' => 'https://*.example.com', 'url' => 'https://test.example.com', 'result' => true], | ||
['config' => '*//example.com', 'url' => 'https://example.com', 'result' => true], | ||
['config' => '*//example.com', 'url' => 'http://example.com', 'result' => true], | ||
['config' => 'https://example.com', 'url' => 'https://example.com/a/b/c?test=cat', 'result' => true], | ||
['config' => 'https://example.com', 'url' => 'https://example.co.uk', 'result' => false], | ||
|
||
// Escapes | ||
['config' => 'https://(.*?).com', 'url' => 'https://example.com', 'result' => false], | ||
['config' => 'https://example.com', 'url' => 'https://example.co.uk#https://example.com', 'result' => false], | ||
|
||
// Multi values | ||
['config' => '*//example.org *//example.com', 'url' => 'https://example.com', 'result' => true], | ||
['config' => '*//example.org *//example.com', 'url' => 'https://example.com/a/b/c?test=cat#hello', 'result' => true], | ||
['config' => '*.example.org *.example.com', 'url' => 'https://example.co.uk', 'result' => false], | ||
['config' => ' *.example.org *.example.com ', 'url' => 'https://example.co.uk', 'result' => false], | ||
['config' => '* *.example.com', 'url' => 'https://example.co.uk', 'result' => true], | ||
['config' => '*//example.org *//example.com *//example.co.uk', 'url' => 'https://example.co.uk', 'result' => true], | ||
['config' => '*//example.org *//example.com *//example.co.uk', 'url' => 'https://example.net', 'result' => false], | ||
]; | ||
|
||
foreach ($testMap as $test) { | ||
$result = (new SsrUrlValidator($test['config']))->allowed($test['url']); | ||
$this->assertEquals($test['result'], $result, "Failed asserting url '{$test['url']}' with config '{$test['config']}' results " . ($test['result'] ? 'true' : 'false')); | ||
} | ||
} | ||
|
||
public function test_enssure_allowed() | ||
{ | ||
$result = (new SsrUrlValidator('https://example.com'))->ensureAllowed('https://example.com'); | ||
$this->assertNull($result); | ||
|
||
$this->expectException(HttpFetchException::class); | ||
(new SsrUrlValidator('https://example.com'))->ensureAllowed('https://test.example.com'); | ||
} | ||
} |