diff --git a/.env.example.complete b/.env.example.complete index 18e7bd00d9c..ebebaf9e3e8 100644 --- a/.env.example.complete +++ b/.env.example.complete @@ -351,10 +351,25 @@ EXPORT_PDF_COMMAND_TIMEOUT=15 # Only used if 'ALLOW_UNTRUSTED_SERVER_FETCHING=true' which disables security protections. WKHTMLTOPDF=false -# Allow abc123'; + $page->html = "escape {$script}"; + $page->save(); + + $pageView = $this->get($page->getUrl()); + $pageView->assertStatus(200); + $pageView->assertDontSee($script, false); + $pageView->assertSee('abc123abc123'); + } + + public function test_more_complex_content_script_escaping_scenarios() + { + config()->set('app.content_filtering', 'j'); + + $checks = [ + "

Some script

", + "

Some script

", + "

Some script

", + "

Some script

", + "

Some script

", + "

Some script

", + ]; + + $this->asEditor(); + $page = $this->entities->page(); + + foreach ($checks as $check) { + $page->html = $check; + $page->save(); + + $pageView = $this->get($page->getUrl()); + $pageView->assertStatus(200); + $this->withHtml($pageView)->assertElementNotContains('.page-content', ''); + } + } + + public function test_js_and_base64_src_urls_are_removed() + { + config()->set('app.content_filtering', 'j'); + + $checks = [ + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + ]; + + $this->asEditor(); + $page = $this->entities->page(); + + foreach ($checks as $check) { + $page->html = $check; + $page->save(); + + $pageView = $this->get($page->getUrl()); + $pageView->assertStatus(200); + $html = $this->withHtml($pageView); + $html->assertElementNotContains('.page-content', 'assertElementNotContains('.page-content', 'data='); + $html->assertElementNotContains('.page-content', ''); + $html->assertElementNotContains('.page-content', 'src='); + $html->assertElementNotContains('.page-content', 'javascript:'); + $html->assertElementNotContains('.page-content', 'data:'); + $html->assertElementNotContains('.page-content', 'base64'); + } + } + + public function test_javascript_uri_links_are_removed() + { + config()->set('app.content_filtering', 'j'); + + $checks = [ + 'withHtml($pageView)->assertElementNotContains('.page-content', 'href=javascript:'); + } + } + + public function test_form_filtering_is_controlled_by_config() + { + config()->set('app.content_filtering', ''); + $page = $this->entities->page(); + $page->html = '
'; + $page->save(); + + $this->asEditor()->get($page->getUrl())->assertSee('dont-see-this', false); + + config()->set('app.content_filtering', 'f'); + $this->get($page->getUrl())->assertDontSee('dont-see-this', false); + } + + public function test_form_actions_with_javascript_are_removed() + { + config()->set('app.content_filtering', 'j'); + + $checks = [ + '', + 'Click me', + 'Click me', + '', + '', + ]; + + $this->asEditor(); + $page = $this->entities->page(); + + foreach ($checks as $check) { + $page->html = $check; + $page->save(); + + $pageView = $this->get($page->getUrl()); + $pageView->assertStatus(200); + $pageView->assertDontSee('id="xss"', false); + $pageView->assertDontSee('action=javascript:', false); + $pageView->assertDontSee('action=JaVaScRiPt:', false); + $pageView->assertDontSee('formaction=javascript:', false); + $pageView->assertDontSee('formaction=JaVaScRiPt:', false); + } + } + + public function test_form_elements_are_removed() + { + config()->set('app.content_filtering', 'f'); + + $checks = [ + '

thisisacattofind

thisdogshouldnotbefound
', + '

thisisacattofind

', + '

thisisacattofind

', + '

thisisacattofind

', + '

thisisacattofind

thisdogshouldnotbefound
', + '

thisisacattofind

', + '

thisisacattofind

', + <<<'TESTCASE' + + + + +

thisisacattofind

+
+

thisdogshouldnotbefound

+
+ + + + +
+
+TESTCASE + + ]; + + $this->asEditor(); + $page = $this->entities->page(); + + foreach ($checks as $check) { + $page->html = $check; + $page->save(); + + $pageView = $this->get($page->getUrl()); + $pageView->assertStatus(200); + $pageView->assertSee('thisisacattofind'); + $pageView->assertDontSee('thisdogshouldnotbefound'); + } + } + + public function test_form_attributes_are_removed() + { + config()->set('app.content_filtering', 'f'); + + $withinSvgSample = <<<'TESTCASE' + + + + +

thisisacattofind

+

thisisacattofind

+ + +
+
+TESTCASE; + + $checks = [ + 'formaction' => '

thisisacattofind

', + 'form' => '

thisisacattofind

', + 'formmethod' => '

thisisacattofind

', + 'formtarget' => '

thisisacattofind

', + 'FORMTARGET' => '

thisisacattofind

', + ]; + + $this->asEditor(); + $page = $this->entities->page(); + + foreach ($checks as $attribute => $check) { + $page->html = $check; + $page->save(); + + $pageView = $this->get($page->getUrl()); + $pageView->assertStatus(200); + $pageView->assertSee('thisisacattofind'); + $this->withHtml($pageView)->assertElementNotExists(".page-content [{$attribute}]"); + } + + $page->html = $withinSvgSample; + $page->save(); + $pageView = $this->get($page->getUrl()); + $pageView->assertStatus(200); + $html = $this->withHtml($pageView); + foreach ($checks as $attribute => $check) { + $pageView->assertSee('thisisacattofind'); + $html->assertElementNotExists(".page-content [{$attribute}]"); + } + } + + public function test_metadata_redirects_are_removed() + { + config()->set('app.content_filtering', 'h'); + + $checks = [ + '', + '', + '', + ]; + + $this->asEditor(); + $page = $this->entities->page(); + + foreach ($checks as $check) { + $page->html = $check; + $page->save(); + + $pageView = $this->get($page->getUrl()); + $pageView->assertStatus(200); + $this->withHtml($pageView)->assertElementNotContains('.page-content', ''); + $this->withHtml($pageView)->assertElementNotContains('.page-content', ''); + $this->withHtml($pageView)->assertElementNotContains('.page-content', 'content='); + $this->withHtml($pageView)->assertElementNotContains('.page-content', 'external_url'); + } + } + + public function test_page_inline_on_attributes_removed_by_default() + { + config()->set('app.content_filtering', 'j'); + + $this->asEditor(); + $page = $this->entities->page(); + $script = '

Hello

'; + $page->html = "escape {$script}"; + $page->save(); + + $pageView = $this->get($page->getUrl()); + $pageView->assertStatus(200); + $pageView->assertDontSee($script, false); + $pageView->assertSee('

Hello

', false); + } + + public function test_more_complex_inline_on_attributes_escaping_scenarios() + { + config()->set('app.content_filtering', 'j'); + + $checks = [ + '

Hello

', + '

Hello

', + '
Lorem ipsum dolor sit amet.

Hello

', + '
Lorem ipsum dolor sit amet.

Hello

', + '
Lorem ipsum dolor sit amet.

Hello

', + '
Lorem ipsum dolor sit amet.

Hello

', + '
xss link\', + ]; + + $this->asEditor(); + $page = $this->entities->page(); + + foreach ($checks as $check) { + $page->html = $check; + $page->save(); + + $pageView = $this->get($page->getUrl()); + $pageView->assertStatus(200); + $this->withHtml($pageView)->assertElementNotContains('.page-content', 'onclick'); + } + } + + public function test_page_content_scripts_show_with_filters_disabled() + { + $this->asEditor(); + $page = $this->entities->page(); + config()->set('app.content_filtering', ''); + + $script = 'abc123abc123'; + $page->html = "no escape {$script}"; + $page->save(); + + $pageView = $this->get($page->getUrl()); + $pageView->assertSee($script, false); + $pageView->assertDontSee('abc123abc123'); + } + + public function test_svg_script_usage_is_removed() + { + config()->set('app.content_filtering', 'j'); + + $checks = [ + '', + '', + '', + '', + '', + 'XSS', + 'XSS', + '', + ]; + + $this->asEditor(); + $page = $this->entities->page(); + + foreach ($checks as $check) { + $page->html = $check; + $page->save(); + + $pageView = $this->get($page->getUrl()); + $pageView->assertStatus(200); + $html = $this->withHtml($pageView); + $html->assertElementNotContains('.page-content', 'alert'); + $html->assertElementNotContains('.page-content', 'xlink:href'); + $html->assertElementNotContains('.page-content', 'application/xml'); + $html->assertElementNotContains('.page-content', 'javascript'); + } + } + + public function test_page_inline_on_attributes_show_with_filters_disabled() + { + $this->asEditor(); + $page = $this->entities->page(); + config()->set('app.content_filtering', ''); + + $script = '

Hello

'; + $page->html = "escape {$script}"; + $page->save(); + + $pageView = $this->get($page->getUrl()); + $pageView->assertSee($script, false); + $pageView->assertDontSee('

Hello

', false); + } + + public function test_non_content_filtering_is_controlled_by_config() + { + config()->set('app.content_filtering', ''); + $page = $this->entities->page(); + $html = <<<'HTML' + + +HTML; + $page->html = $html; + $page->save(); + + $resp = $this->asEditor()->get($page->getUrl()); + $resp->assertSee('superbeans', false); + + config()->set('app.content_filtering', 'h'); + + $resp = $this->asEditor()->get($page->getUrl()); + $resp->assertDontSee('superbeans', false); + } + + public function test_non_content_filtering() + { + config()->set('app.content_filtering', 'h'); + $page = $this->entities->page(); + $html = <<<'HTML' + +

inbetweenpsection

+ + +superbeans! + +HTML; + + $page->html = $html; + $page->save(); + + $resp = $this->asEditor()->get($page->getUrl()); + $resp->assertDontSee('superbeans', false); + $resp->assertSee('inbetweenpsection', false); + } + + public function test_allow_list_filtering_is_controlled_by_config() + { + config()->set('app.content_filtering', ''); + $page = $this->entities->page(); + $page->html = '
Hello!
'; + $page->save(); + + $resp = $this->asEditor()->get($page->getUrl()); + $resp->assertSee('style="position: absolute; left: 0;color:#00FFEE;"', false); + + config()->set('app.content_filtering', 'a'); + $resp = $this->get($page->getUrl()); + $resp->assertDontSee('style="position: absolute; left: 0;color:#00FFEE;"', false); + $resp->assertSee('style="color:#00FFEE;"', false); + } + + public function test_allow_list_style_filtering() + { + $testCasesExpectedByInput = [ + '
Hello!
' => '
Hello!
', + '
Hello!
' => '
Hello!
', + '
Hello!
' => '
Hello!
', + '
Hello!
' => '
Hello!
', + ]; + + config()->set('app.content_filtering', 'a'); + $page = $this->entities->page(); + $this->asEditor(); + + foreach ($testCasesExpectedByInput as $input => $expected) { + $page->html = $input; + $page->save(); + $resp = $this->get($page->getUrl()); + + $resp->assertSee($expected, false); + } + } +} diff --git a/tests/Entity/PageContentTest.php b/tests/Entity/PageContentTest.php index 77026113012..deae153e192 100644 --- a/tests/Entity/PageContentTest.php +++ b/tests/Entity/PageContentTest.php @@ -101,351 +101,6 @@ public function test_page_includes_to_nonexisting_pages_does_not_error() $pageResp->assertSee('Hello Barry'); } - public function test_page_content_scripts_removed_by_default() - { - $this->asEditor(); - $page = $this->entities->page(); - $script = 'abc123abc123'; - $page->html = "escape {$script}"; - $page->save(); - - $pageView = $this->get($page->getUrl()); - $pageView->assertStatus(200); - $pageView->assertDontSee($script, false); - $pageView->assertSee('abc123abc123'); - } - - public function test_more_complex_content_script_escaping_scenarios() - { - $checks = [ - "

Some script

", - "

Some script

", - "

Some script

", - "

Some script

", - "

Some script

", - "

Some script

", - ]; - - $this->asEditor(); - $page = $this->entities->page(); - - foreach ($checks as $check) { - $page->html = $check; - $page->save(); - - $pageView = $this->get($page->getUrl()); - $pageView->assertStatus(200); - $this->withHtml($pageView)->assertElementNotContains('.page-content', ''); - } - } - - public function test_js_and_base64_src_urls_are_removed() - { - $checks = [ - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - ]; - - $this->asEditor(); - $page = $this->entities->page(); - - foreach ($checks as $check) { - $page->html = $check; - $page->save(); - - $pageView = $this->get($page->getUrl()); - $pageView->assertStatus(200); - $html = $this->withHtml($pageView); - $html->assertElementNotContains('.page-content', ''); - $html->assertElementNotContains('.page-content', 'src='); - $html->assertElementNotContains('.page-content', 'javascript:'); - $html->assertElementNotContains('.page-content', 'data:'); - $html->assertElementNotContains('.page-content', 'base64'); - } - } - - public function test_javascript_uri_links_are_removed() - { - $checks = [ - '
withHtml($pageView)->assertElementNotContains('.page-content', 'href=javascript:'); - } - } - - public function test_form_actions_with_javascript_are_removed() - { - $checks = [ - '', - 'Click me', - 'Click me', - '', - '', - ]; - - $this->asEditor(); - $page = $this->entities->page(); - - foreach ($checks as $check) { - $page->html = $check; - $page->save(); - - $pageView = $this->get($page->getUrl()); - $pageView->assertStatus(200); - $pageView->assertDontSee('id="xss"', false); - $pageView->assertDontSee('action=javascript:', false); - $pageView->assertDontSee('action=JaVaScRiPt:', false); - $pageView->assertDontSee('formaction=javascript:', false); - $pageView->assertDontSee('formaction=JaVaScRiPt:', false); - } - } - - public function test_form_elements_are_removed() - { - $checks = [ - '

thisisacattofind

thisdogshouldnotbefound
', - '

thisisacattofind

', - '

thisisacattofind

', - '

thisisacattofind

', - '

thisisacattofind

thisdogshouldnotbefound
', - '

thisisacattofind

', - '

thisisacattofind

', - <<<'TESTCASE' - - - - -

thisisacattofind

-
-

thisdogshouldnotbefound

-
- - - - -
-
-TESTCASE - - ]; - - $this->asEditor(); - $page = $this->entities->page(); - - foreach ($checks as $check) { - $page->html = $check; - $page->save(); - - $pageView = $this->get($page->getUrl()); - $pageView->assertStatus(200); - $pageView->assertSee('thisisacattofind'); - $pageView->assertDontSee('thisdogshouldnotbefound'); - } - } - - public function test_form_attributes_are_removed() - { - $withinSvgSample = <<<'TESTCASE' - - - - -

thisisacattofind

-

thisisacattofind

- - -
-
-TESTCASE; - - $checks = [ - 'formaction' => '

thisisacattofind

', - 'form' => '

thisisacattofind

', - 'formmethod' => '

thisisacattofind

', - 'formtarget' => '

thisisacattofind

', - 'FORMTARGET' => '

thisisacattofind

', - ]; - - $this->asEditor(); - $page = $this->entities->page(); - - foreach ($checks as $attribute => $check) { - $page->html = $check; - $page->save(); - - $pageView = $this->get($page->getUrl()); - $pageView->assertStatus(200); - $pageView->assertSee('thisisacattofind'); - $this->withHtml($pageView)->assertElementNotExists(".page-content [{$attribute}]"); - } - - $page->html = $withinSvgSample; - $page->save(); - $pageView = $this->get($page->getUrl()); - $pageView->assertStatus(200); - $html = $this->withHtml($pageView); - foreach ($checks as $attribute => $check) { - $pageView->assertSee('thisisacattofind'); - $html->assertElementNotExists(".page-content [{$attribute}]"); - } - } - - public function test_metadata_redirects_are_removed() - { - $checks = [ - '', - '', - '', - ]; - - $this->asEditor(); - $page = $this->entities->page(); - - foreach ($checks as $check) { - $page->html = $check; - $page->save(); - - $pageView = $this->get($page->getUrl()); - $pageView->assertStatus(200); - $this->withHtml($pageView)->assertElementNotContains('.page-content', ''); - $this->withHtml($pageView)->assertElementNotContains('.page-content', ''); - $this->withHtml($pageView)->assertElementNotContains('.page-content', 'content='); - $this->withHtml($pageView)->assertElementNotContains('.page-content', 'external_url'); - } - } - - public function test_page_inline_on_attributes_removed_by_default() - { - $this->asEditor(); - $page = $this->entities->page(); - $script = '

Hello

'; - $page->html = "escape {$script}"; - $page->save(); - - $pageView = $this->get($page->getUrl()); - $pageView->assertStatus(200); - $pageView->assertDontSee($script, false); - $pageView->assertSee('

Hello

', false); - } - - public function test_more_complex_inline_on_attributes_escaping_scenarios() - { - $checks = [ - '

Hello

', - '

Hello

', - '
Lorem ipsum dolor sit amet.

Hello

', - '
Lorem ipsum dolor sit amet.

Hello

', - '
Lorem ipsum dolor sit amet.

Hello

', - '
Lorem ipsum dolor sit amet.

Hello

', - '
xss link\', - ]; - - $this->asEditor(); - $page = $this->entities->page(); - - foreach ($checks as $check) { - $page->html = $check; - $page->save(); - - $pageView = $this->get($page->getUrl()); - $pageView->assertStatus(200); - $this->withHtml($pageView)->assertElementNotContains('.page-content', 'onclick'); - } - } - - public function test_page_content_scripts_show_when_configured() - { - $this->asEditor(); - $page = $this->entities->page(); - config()->set('app.allow_content_scripts', 'true'); - - $script = 'abc123abc123'; - $page->html = "no escape {$script}"; - $page->save(); - - $pageView = $this->get($page->getUrl()); - $pageView->assertSee($script, false); - $pageView->assertDontSee('abc123abc123'); - } - - public function test_svg_script_usage_is_removed() - { - $checks = [ - '', - '', - '', - '', - '', - 'XSS', - 'XSS', - '', - ]; - - $this->asEditor(); - $page = $this->entities->page(); - - foreach ($checks as $check) { - $page->html = $check; - $page->save(); - - $pageView = $this->get($page->getUrl()); - $pageView->assertStatus(200); - $html = $this->withHtml($pageView); - $html->assertElementNotContains('.page-content', 'alert'); - $html->assertElementNotContains('.page-content', 'xlink:href'); - $html->assertElementNotContains('.page-content', 'application/xml'); - $html->assertElementNotContains('.page-content', 'javascript'); - } - } - - public function test_page_inline_on_attributes_show_if_configured() - { - $this->asEditor(); - $page = $this->entities->page(); - config()->set('app.allow_content_scripts', 'true'); - - $script = '

Hello

'; - $page->html = "escape {$script}"; - $page->save(); - - $pageView = $this->get($page->getUrl()); - $pageView->assertSee($script, false); - $pageView->assertDontSee('

Hello

', false); - } - public function test_duplicate_ids_does_not_break_page_render() { $this->asEditor(); @@ -649,6 +304,7 @@ public function test_page_markdown_strikethrough_rendering() public function test_page_markdown_single_html_comment_saving() { + config()->set('app.content_filtering', 'jfh'); $this->asEditor(); $page = $this->entities->page(); @@ -656,7 +312,7 @@ public function test_page_markdown_single_html_comment_saving() $this->put($page->getUrl(), [ 'name' => $page->name, 'markdown' => $content, 'html' => '', 'summary' => '', - ]); + ])->assertRedirect(); $page->refresh(); $this->assertStringMatchesFormat($content, $page->html); diff --git a/tests/Entity/PageDraftTest.php b/tests/Entity/PageDraftTest.php index 2623acd3f42..8a30a97503b 100644 --- a/tests/Entity/PageDraftTest.php +++ b/tests/Entity/PageDraftTest.php @@ -160,9 +160,11 @@ public function test_page_html_in_ajax_fetch_response() { $this->asAdmin(); $page = $this->entities->page(); + $page->html = '

test content

'; + $page->save(); $this->getJson('/ajax/page/' . $page->id)->assertJson([ - 'html' => $page->html, + 'html' => '

test content

', ]); } diff --git a/tests/Entity/PageEditorTest.php b/tests/Entity/PageEditorTest.php index d98b1f998df..67283f70420 100644 --- a/tests/Entity/PageEditorTest.php +++ b/tests/Entity/PageEditorTest.php @@ -265,4 +265,40 @@ public function test_editor_type_change_to_wysiwyg_infers_type_from_request_or_u $this->assertEquals($test['expected'], $page->refresh()->editor, "Failed asserting global editor {$test['setting']} with request editor {$test['request']} results in {$test['expected']} set for the page"); } } + + public function test_editor_html_content_is_filtered_if_loaded_by_a_different_user() + { + $editor = $this->users->editor(); + $page = $this->entities->page(); + $page->html = ''; + $page->updated_by = $editor->id; + $page->save(); + + $resp = $this->asAdmin()->get($page->getUrl('edit')); + $resp->assertOk(); + $resp->assertDontSee('hellotherethisisaturtlemonster', false); + + $resp = $this->asAdmin()->get("/ajax/page/{$page->id}"); + $resp->assertOk(); + $resp->assertDontSee('hellotherethisisaturtlemonster', false); + } + + public function test_editor_html_filtered_does_not_cause_error_if_empty() + { + $emptyExamples = ['', '

', '

 

', ' ', "\n"]; + $editor = $this->users->editor(); + $page = $this->entities->page(); + $page->updated_by = $editor->id; + + foreach ($emptyExamples as $emptyExample) { + $page->html = $emptyExample; + $page->save(); + + $resp = $this->asAdmin()->get($page->getUrl('edit')); + $resp->assertOk(); + + $resp = $this->asAdmin()->get("/ajax/page/{$page->id}"); + $resp->assertOk(); + } + } } diff --git a/tests/SecurityHeaderTest.php b/tests/SecurityHeaderTest.php index fe98e32080b..3f4b7d193ce 100644 --- a/tests/SecurityHeaderTest.php +++ b/tests/SecurityHeaderTest.php @@ -93,14 +93,14 @@ public function test_script_csp_nonce_changes_per_request() $this->assertNotEquals($firstHeader, $secondHeader); } - public function test_allow_content_scripts_settings_controls_csp_script_headers() + public function test_content_filtering_config_controls_csp_script_headers() { - config()->set('app.allow_content_scripts', true); + config()->set('app.content_filtering', ''); $resp = $this->get('/'); $scriptHeader = $this->getCspHeader($resp, 'script-src'); $this->assertEmpty($scriptHeader); - config()->set('app.allow_content_scripts', false); + config()->set('app.content_filtering', 'j'); $resp = $this->get('/'); $scriptHeader = $this->getCspHeader($resp, 'script-src'); $this->assertNotEmpty($scriptHeader); diff --git a/tests/Unit/ConfigTest.php b/tests/Unit/ConfigTest.php index 7795a861a0c..9ed68c8bdfc 100644 --- a/tests/Unit/ConfigTest.php +++ b/tests/Unit/ConfigTest.php @@ -170,6 +170,27 @@ public function test_mysql_host_parsed_as_expected() } } + public function test_content_filtering_defaults_to_enabled() + { + $this->runWithEnv(['APP_CONTENT_FILTERING' => null, 'ALLOW_CONTENT_SCRIPTS' => null], function () { + $this->assertEquals('jhfa', config('app.content_filtering')); + }); + } + + public function test_content_filtering_can_be_disabled() + { + $this->runWithEnv(['APP_CONTENT_FILTERING' => "", 'ALLOW_CONTENT_SCRIPTS' => null], function () { + $this->assertEquals('', config('app.content_filtering')); + }); + } + + public function test_allow_content_scripts_disables_content_filtering() + { + $this->runWithEnv(['APP_CONTENT_FILTERING' => null, 'ALLOW_CONTENT_SCRIPTS' => 'true'], function () { + $this->assertEquals('', config('app.content_filtering')); + }); + } + /** * Set an environment variable of the given name and value * then check the given config key to see if it matches the given result.