diff --git a/docs/1-getting-started.md b/docs/1-getting-started.md index 3dda2d7..5e72f53 100644 --- a/docs/1-getting-started.md +++ b/docs/1-getting-started.md @@ -93,6 +93,11 @@ $sanitizer = HtmlSanitizer\Sanitizer::create([ * If true, mailto links will be accepted. */ 'allow_mailto' => false, + + /* + * If true, relative links will be accepted. + */ + 'allow_relative_links' => false, ], 'img' => [ @@ -111,6 +116,11 @@ $sanitizer = HtmlSanitizer\Sanitizer::create([ * If true, images data-uri URLs will be accepted. */ 'allow_data_uri' => false, + + /* + * If true, relative links will be accepted. + */ + 'allow_relative_links' => false, ], 'iframe' => [ @@ -124,6 +134,11 @@ $sanitizer = HtmlSanitizer\Sanitizer::create([ * 'allowed_hosts' => ['trusted1.com', 'google.com'], */ 'allowed_hosts' => null, + + /* + * If true, relative links will be accepted. + */ + 'allow_relative_links' => false, ], ], ]); diff --git a/src/Extension/Basic/NodeVisitor/ANodeVisitor.php b/src/Extension/Basic/NodeVisitor/ANodeVisitor.php index d5b94f4..d6ca838 100644 --- a/src/Extension/Basic/NodeVisitor/ANodeVisitor.php +++ b/src/Extension/Basic/NodeVisitor/ANodeVisitor.php @@ -41,6 +41,7 @@ public function __construct(array $config = []) $this->config['allowed_schemes'], $this->config['allowed_hosts'], $this->config['allow_mailto'], + $this->config['allow_relative_links'], $this->config['force_https'] ); } @@ -61,6 +62,7 @@ public function getDefaultConfiguration(): array 'allowed_schemes' => ['http', 'https'], 'allowed_hosts' => null, 'allow_mailto' => true, + 'allow_relative_links' => false, 'force_https' => false, 'rel' => null, ]; diff --git a/src/Extension/Basic/Sanitizer/AHrefSanitizer.php b/src/Extension/Basic/Sanitizer/AHrefSanitizer.php index 3dc6d9b..962fbc1 100644 --- a/src/Extension/Basic/Sanitizer/AHrefSanitizer.php +++ b/src/Extension/Basic/Sanitizer/AHrefSanitizer.php @@ -23,13 +23,15 @@ class AHrefSanitizer private $allowedSchemes; private $allowedHosts; private $allowMailTo; + private $allowRelativeLinks; private $forceHttps; - public function __construct(array $allowedSchemes, ?array $allowedHosts, bool $allowMailTo, bool $forceHttps) + public function __construct(array $allowedSchemes, ?array $allowedHosts, bool $allowMailTo, bool $allowRelativeLinks, bool $forceHttps) { $this->allowedSchemes = $allowedSchemes; $this->allowedHosts = $allowedHosts; $this->allowMailTo = $allowMailTo; + $this->allowRelativeLinks = $allowRelativeLinks; $this->forceHttps = $forceHttps; } @@ -46,6 +48,14 @@ public function sanitize(?string $input): ?string } } + if ($this->allowRelativeLinks) { + $allowedSchemes[] = null; + + if (\is_array($this->allowedHosts)) { + $allowedHosts[] = null; + } + } + if (!$sanitized = $this->sanitizeUrl($input, $allowedSchemes, $allowedHosts, $this->forceHttps)) { return null; } diff --git a/src/Extension/Iframe/NodeVisitor/IframeNodeVisitor.php b/src/Extension/Iframe/NodeVisitor/IframeNodeVisitor.php index f4604f4..15c31d5 100644 --- a/src/Extension/Iframe/NodeVisitor/IframeNodeVisitor.php +++ b/src/Extension/Iframe/NodeVisitor/IframeNodeVisitor.php @@ -40,6 +40,7 @@ public function __construct(array $config = []) $this->sanitizer = new IframeSrcSanitizer( $this->config['allowed_schemes'], $this->config['allowed_hosts'], + $this->config['allow_relative_links'], $this->config['force_https'] ); } @@ -64,6 +65,7 @@ public function getDefaultConfiguration(): array return [ 'allowed_schemes' => ['http', 'https'], 'allowed_hosts' => null, + 'allow_relative_links' => false, 'force_https' => false, ]; } diff --git a/src/Extension/Iframe/Sanitizer/IframeSrcSanitizer.php b/src/Extension/Iframe/Sanitizer/IframeSrcSanitizer.php index e32365c..37ba927 100644 --- a/src/Extension/Iframe/Sanitizer/IframeSrcSanitizer.php +++ b/src/Extension/Iframe/Sanitizer/IframeSrcSanitizer.php @@ -22,17 +22,30 @@ class IframeSrcSanitizer private $allowedSchemes; private $allowedHosts; + private $allowRelativeLinks; private $forceHttps; - public function __construct(array $allowedSchemes, ?array $allowedHosts, bool $forceHttps) + public function __construct(array $allowedSchemes, ?array $allowedHosts, bool $allowRelativeLinks, bool $forceHttps) { $this->allowedSchemes = $allowedSchemes; $this->allowedHosts = $allowedHosts; + $this->allowRelativeLinks = $allowRelativeLinks; $this->forceHttps = $forceHttps; } public function sanitize(?string $input): ?string { - return $this->sanitizeUrl($input, $this->allowedSchemes, $this->allowedHosts, $this->forceHttps); + $allowedSchemes = $this->allowedSchemes; + $allowedHosts = $this->allowedHosts; + + if ($this->allowRelativeLinks) { + $allowedSchemes[] = null; + + if (\is_array($this->allowedHosts)) { + $allowedHosts[] = null; + } + } + + return $this->sanitizeUrl($input, $allowedSchemes, $allowedHosts, $this->forceHttps); } } diff --git a/src/Extension/Image/NodeVisitor/ImgNodeVisitor.php b/src/Extension/Image/NodeVisitor/ImgNodeVisitor.php index 62ecf71..603bb99 100644 --- a/src/Extension/Image/NodeVisitor/ImgNodeVisitor.php +++ b/src/Extension/Image/NodeVisitor/ImgNodeVisitor.php @@ -41,6 +41,7 @@ public function __construct(array $config = []) $this->config['allowed_schemes'], $this->config['allowed_hosts'], $this->config['allow_data_uri'], + $this->config['allow_relative_links'], $this->config['force_https'] ); } @@ -61,6 +62,7 @@ public function getDefaultConfiguration(): array 'allowed_schemes' => ['http', 'https'], 'allowed_hosts' => null, 'allow_data_uri' => false, + 'allow_relative_links' => false, 'force_https' => false, ]; } diff --git a/src/Extension/Image/Sanitizer/ImgSrcSanitizer.php b/src/Extension/Image/Sanitizer/ImgSrcSanitizer.php index fb9158a..413ebbf 100644 --- a/src/Extension/Image/Sanitizer/ImgSrcSanitizer.php +++ b/src/Extension/Image/Sanitizer/ImgSrcSanitizer.php @@ -23,13 +23,15 @@ class ImgSrcSanitizer private $allowedSchemes; private $allowedHosts; private $allowDataUri; + private $allowRelativeLinks; private $forceHttps; - public function __construct(array $allowedSchemes, ?array $allowedHosts, bool $allowDataUri, bool $forceHttps) + public function __construct(array $allowedSchemes, ?array $allowedHosts, bool $allowDataUri, bool $allowRelativeLinks, bool $forceHttps) { $this->allowedSchemes = $allowedSchemes; $this->allowedHosts = $allowedHosts; $this->allowDataUri = $allowDataUri; + $this->allowRelativeLinks = $allowRelativeLinks; $this->forceHttps = $forceHttps; } @@ -38,13 +40,20 @@ public function sanitize(?string $input): ?string $allowedSchemes = $this->allowedSchemes; $allowedHosts = $this->allowedHosts; - if ($this->allowDataUri) { + if ($this->allowDataUri && !$this->allowRelativeLinks) { $allowedSchemes[] = 'data'; if (null !== $allowedHosts) { $allowedHosts[] = null; } } + if ($this->allowRelativeLinks) { + $allowedSchemes[] = null; + if (null !== $allowedHosts) { + $allowedHosts[] = null; + } + } + if (!$sanitized = $this->sanitizeUrl($input, $allowedSchemes, $allowedHosts, $this->forceHttps)) { return null; } diff --git a/tests/Sanitizer/AHrefSanitizerTest.php b/tests/Sanitizer/AHrefSanitizerTest.php index 14ab55e..25acd66 100644 --- a/tests/Sanitizer/AHrefSanitizerTest.php +++ b/tests/Sanitizer/AHrefSanitizerTest.php @@ -23,6 +23,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => null, 'allowMailTo' => false, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'https://trusted.com/link.php', 'output' => 'https://trusted.com/link.php', @@ -32,6 +33,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => ['trusted.com'], 'allowMailTo' => false, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'https://trusted.com/link.php', 'output' => 'https://trusted.com/link.php', @@ -41,6 +43,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => ['trusted.com'], 'allowMailTo' => false, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'https://untrusted.com/link.php', 'output' => null, @@ -50,6 +53,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => null, 'allowMailTo' => false, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => '/link.php', 'output' => null, @@ -59,16 +63,28 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => null, 'allowMailTo' => true, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => '/link.php', 'output' => null, ]; + yield [ + 'allowedSchemes' => ['http', 'https'], + 'allowedHosts' => null, + 'allowMailTo' => true, + 'allowRelativeLinks' => true, + 'forceHttps' => false, + 'input' => '/link.php', + 'output' => '/link.php', + ]; + // Force HTTPS yield [ 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => ['trusted.com'], 'allowMailTo' => false, + 'allowRelativeLinks' => false, 'forceHttps' => true, 'input' => 'http://trusted.com/link.php', 'output' => 'https://trusted.com/link.php', @@ -79,6 +95,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => null, 'allowMailTo' => true, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'output' => null, @@ -88,6 +105,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => null, 'allowMailTo' => true, + 'allowRelativeLinks' => false, 'forceHttps' => true, 'input' => 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'output' => null, @@ -98,6 +116,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => null, 'allowMailTo' => false, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'mailto:test@gmail.com', 'output' => null, @@ -107,6 +126,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => null, 'allowMailTo' => true, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'mailto:test@gmail.com', 'output' => 'mailto:test@gmail.com', @@ -116,6 +136,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => ['trusted.com'], 'allowMailTo' => true, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'mailto:test@gmail.com', 'output' => 'mailto:test@gmail.com', @@ -125,6 +146,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => ['trusted.com'], 'allowMailTo' => true, + 'allowRelativeLinks' => false, 'forceHttps' => true, 'input' => 'mailto:test@gmail.com', 'output' => 'mailto:test@gmail.com', @@ -134,6 +156,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => null, 'allowMailTo' => true, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'mailto:invalid', 'output' => null, @@ -143,6 +166,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => null, 'allowMailTo' => true, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'mailto:', 'output' => null, @@ -152,6 +176,7 @@ public function provideUrls() 'allowedSchemes' => ['https'], 'allowedHosts' => null, 'allowMailTo' => true, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'http://trusted.com/link.php', 'output' => null, @@ -161,8 +186,8 @@ public function provideUrls() /** * @dataProvider provideUrls */ - public function testSanitize($allowedSchemes, $allowedHosts, $allowMailTo, $forceHttps, $input, $expected) + public function testSanitize($allowedSchemes, $allowedHosts, $allowMailTo, $allowRelativeLinks, $forceHttps, $input, $expected) { - $this->assertSame($expected, (new AHrefSanitizer($allowedSchemes, $allowedHosts, $allowMailTo, $forceHttps))->sanitize($input)); + $this->assertSame($expected, (new AHrefSanitizer($allowedSchemes, $allowedHosts, $allowMailTo, $allowRelativeLinks, $forceHttps))->sanitize($input)); } } diff --git a/tests/Sanitizer/IframeSrcSanitizerTest.php b/tests/Sanitizer/IframeSrcSanitizerTest.php index 2cd6cf6..fc97494 100644 --- a/tests/Sanitizer/IframeSrcSanitizerTest.php +++ b/tests/Sanitizer/IframeSrcSanitizerTest.php @@ -22,6 +22,7 @@ public function provideUrls() yield [ 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => null, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'https://trusted.com/iframe.php', 'output' => 'https://trusted.com/iframe.php', @@ -30,6 +31,7 @@ public function provideUrls() yield [ 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => ['trusted.com'], + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'https://trusted.com/iframe.php', 'output' => 'https://trusted.com/iframe.php', @@ -38,6 +40,7 @@ public function provideUrls() yield [ 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => ['trusted.com'], + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'https://untrusted.com/iframe.php', 'output' => null, @@ -46,15 +49,26 @@ public function provideUrls() yield [ 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => null, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => '/iframe.php', 'output' => null, ]; + yield [ + 'allowedSchemes' => ['http', 'https'], + 'allowedHosts' => null, + 'allowRelativeLinks' => true, + 'forceHttps' => false, + 'input' => '/iframe.php', + 'output' => '/iframe.php', + ]; + // Force HTTPS yield [ 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => ['trusted.com'], + 'allowRelativeLinks' => false, 'forceHttps' => true, 'input' => 'http://trusted.com/iframe.php', 'output' => 'https://trusted.com/iframe.php', @@ -64,6 +78,7 @@ public function provideUrls() yield [ 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => null, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'output' => null, @@ -72,6 +87,7 @@ public function provideUrls() yield [ 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => null, + 'allowRelativeLinks' => false, 'forceHttps' => true, 'input' => 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'output' => null, @@ -80,6 +96,7 @@ public function provideUrls() yield [ 'allowedSchemes' => ['https'], 'allowedHosts' => null, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'http://trusted.com/iframe.php', 'output' => null, @@ -89,8 +106,8 @@ public function provideUrls() /** * @dataProvider provideUrls */ - public function testSanitize($allowedSchemes, $allowedHosts, $forceHttps, $input, $expected) + public function testSanitize($allowedSchemes, $allowedHosts, $allowRelativeLinks, $forceHttps, $input, $expected) { - $this->assertSame($expected, (new IframeSrcSanitizer($allowedSchemes, $allowedHosts, $forceHttps))->sanitize($input)); + $this->assertSame($expected, (new IframeSrcSanitizer($allowedSchemes, $allowedHosts, $allowRelativeLinks, $forceHttps))->sanitize($input)); } } diff --git a/tests/Sanitizer/ImgSrcSanitizerTest.php b/tests/Sanitizer/ImgSrcSanitizerTest.php index 3b711dd..6e61c12 100644 --- a/tests/Sanitizer/ImgSrcSanitizerTest.php +++ b/tests/Sanitizer/ImgSrcSanitizerTest.php @@ -23,6 +23,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => null, 'allowDataUri' => false, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'https://trusted.com/image.php', 'output' => 'https://trusted.com/image.php', @@ -32,6 +33,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => ['trusted.com'], 'allowDataUri' => false, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'https://trusted.com/image.php', 'output' => 'https://trusted.com/image.php', @@ -41,6 +43,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => ['trusted.com'], 'allowDataUri' => false, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'https://untrusted.com/image.php', 'output' => null, @@ -50,6 +53,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => null, 'allowDataUri' => false, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => '/image.php', 'output' => null, @@ -59,16 +63,28 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => null, 'allowDataUri' => true, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => '/image.php', 'output' => null, ]; + yield [ + 'allowedSchemes' => ['http', 'https'], + 'allowedHosts' => null, + 'allowDataUri' => false, + 'allowRelativeLinks' => true, + 'forceHttps' => false, + 'input' => '/image.php', + 'output' => '/image.php', + ]; + // Force HTTPS yield [ 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => ['trusted.com'], 'allowDataUri' => false, + 'allowRelativeLinks' => false, 'forceHttps' => true, 'input' => 'http://trusted.com/image.php', 'output' => 'https://trusted.com/image.php', @@ -79,6 +95,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => null, 'allowDataUri' => false, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'output' => null, @@ -88,6 +105,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => null, 'allowDataUri' => true, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'output' => 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', @@ -97,6 +115,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => ['trusted.com'], 'allowDataUri' => true, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'output' => 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', @@ -106,6 +125,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => null, 'allowDataUri' => true, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'data://image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'output' => null, @@ -115,6 +135,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => null, 'allowDataUri' => true, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'data:', 'output' => null, @@ -124,6 +145,7 @@ public function provideUrls() 'allowedSchemes' => ['http', 'https'], 'allowedHosts' => null, 'allowDataUri' => true, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'data:text/plain;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'output' => null, @@ -133,6 +155,7 @@ public function provideUrls() 'allowedSchemes' => ['https'], 'allowedHosts' => null, 'allowDataUri' => false, + 'allowRelativeLinks' => false, 'forceHttps' => false, 'input' => 'http://trusted.com/image.php', 'output' => null, @@ -142,8 +165,8 @@ public function provideUrls() /** * @dataProvider provideUrls */ - public function testSanitize($allowedSchemes, $allowedHosts, $allowDataUri, $forceHttps, $input, $expected) + public function testSanitize($allowedSchemes, $allowedHosts, $allowDataUri, $allowRelativeLinks, $forceHttps, $input, $expected) { - $this->assertSame($expected, (new ImgSrcSanitizer($allowedSchemes, $allowedHosts, $allowDataUri, $forceHttps))->sanitize($input)); + $this->assertSame($expected, (new ImgSrcSanitizer($allowedSchemes, $allowedHosts, $allowDataUri, $allowRelativeLinks, $forceHttps))->sanitize($input)); } }