CVE-2020-26211 (PHP, published 2020-11-03T21:15Z)
Description: value: In BookStack before version 0.30.4, a user with permissions to edit a page could insert JavaScript code through the use of `javascript:` URIs within a link or form which would run, within the context of the current page, when clicked or submitted. Additionally, a user with permissions to edit a page could insert a particular meta tag which could be used to silently redirect users to a alternative location upon visit of a page. Dangerous content may remain in the database but will be removed before being displayed on a page. If you think this could have been exploited the linked advisory provides a SQL query to test. As a workaround without upgrading, page edit permissions could be limited to only those that are trusted until you can upgrade although this will not address existing exploitation of this vulnerability. The issue is fixed in BookStack version 0.30.4.
Severity: LOW (CVSS v3 base score: 8.7)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/BookStackApp/BookStack
Files changed:
app/Entities/Managers/PageContent.php
tests/Entity/PageContentTest.php
Commit message: XSS and redirect fixes with test cases


Vulnerable code:

Fixed code:
        // Remove clickable links to JavaScript URI
        $badLinks = $xPath->query('//*[contains(@href, \'javascript:\')]');
        foreach ($badLinks as $badLink) {
            $badLink->parentNode->removeChild($badLink);
        }

        // Remove forms with calls to JavaScript URI
        $badForms = $xPath->query('//*[contains(@action, \'javascript:\')] | //*[contains(@formaction, \'javascript:\')]');
        foreach ($badForms as $badForm) {
            $badForm->parentNode->removeChild($badForm);
        }

        // Remove meta tag to prevent external redirects
        $metaTags = $xPath->query('//meta[contains(@content, \'url\')]');
        foreach ($metaTags as $metaTag) {
            $metaTag->parentNode->removeChild($metaTag);
        }

    public function test_javascript_uri_links_are_removed()
    {
        $checks = [
            '<a id="xss" href="javascript:alert(document.cookie)>Click me</a>',
            '<a id="xss" href="javascript: alert(document.cookie)>Click me</a>'
        ];

        $this->asEditor();
        $page = Page::first();

        foreach ($checks as $check) {
            $page->html = $check;
            $page->save();

            $pageView = $this->get($page->getUrl());
            $pageView->assertStatus(200);
            $pageView->assertElementNotContains('.page-content', '<a id="xss">');
            $pageView->assertElementNotContains('.page-content', 'href=javascript:');
        }
    }
    public function test_form_actions_with_javascript_are_removed()
    {
        $checks = [
            '<form><input id="xss" type=submit formaction=javascript:alert(document.domain) value=Submit><input></form>',
            '<form ><button id="xss" formaction=javascript:alert(document.domain)>Click me</button></form>',
            '<form id="xss" action=javascript:alert(document.domain)><input type=submit value=Submit></form>'
        ];

        $this->asEditor();
        $page = Page::first();

        foreach ($checks as $check) {
            $page->html = $check;
            $page->save();

            $pageView = $this->get($page->getUrl());
            $pageView->assertStatus(200);
            $pageView->assertElementNotContains('.page-content', '<button id="xss"');
            $pageView->assertElementNotContains('.page-content', '<input id="xss"');
            $pageView->assertElementNotContains('.page-content', '<form id="xss"');
            $pageView->assertElementNotContains('.page-content', 'action=javascript:');
            $pageView->assertElementNotContains('.page-content', 'formaction=javascript:');
        }
    }
    
    public function test_metadata_redirects_are_removed()
    {
        $checks = [
            '<meta http-equiv="refresh" content="0; url=//external_url">',
        ];

        $this->asEditor();
        $page = Page::first();

        foreach ($checks as $check) {
            $page->html = $check;
            $page->save();

            $pageView = $this->get($page->getUrl());
            $pageView->assertStatus(200);
            $pageView->assertElementNotContains('.page-content', '<meta>');
            $pageView->assertElementNotContains('.page-content', '</meta>');
            $pageView->assertElementNotContains('.page-content', 'content=');
            $pageView->assertElementNotContains('.page-content', 'external_url');
        }
    }