CVE-2020-26210 (PHP, published 2020-11-03T19:15Z)
Description: value: In BookStack before version 0.30.4, a user with permissions to edit a page could add an attached link which would execute untrusted JavaScript code when clicked by a viewer of the page. Dangerous content may remain in the database after this update. If you think this could have been exploited the linked advisory provides a SQL query to test. As a workaround, 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 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/Http/Controllers/AttachmentController.php
app/Providers/AppServiceProvider.php
app/Uploads/AttachmentService.php
resources/lang/en/validation.php
tests/Uploads/AttachmentTest.php
Commit message: Prevented possible XSS via link attachments

This filters out potentially malicious javascript: or data: uri's coming
through to be attached to attachments.
Added tests to cover.

Thanks to Yassine ABOUKIR (@yassineaboukir on twitter) for reporting this
vulnerability.


Vulnerable code:
                'attachment_edit_url' =>  'string|min:1|max:255'
                'attachment_link_url' =>  'required|string|min:1|max:255'
        $attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, $pageId);
     * @param string $name
     * @param string $link
     * @param int $page_id
     * @return Attachment
    public function saveNewFromLink($name, $link, $page_id)
     * @param Attachment $attachment
     * @param $requestData
     * @return Attachment
    public function updateFile(Attachment $attachment, $requestData)
     * @param $fileName
     * @return \Illuminate\Http\UploadedFile
    protected function getTestFile($fileName)
        return new \Illuminate\Http\UploadedFile(base_path('tests/test-data/test-file.txt'), $fileName, 'text/plain', 55, null, true);
     * @param $name
     * @param int $uploadedTo
     * @return \Illuminate\Foundation\Testing\TestResponse
    protected function uploadFile($name, $uploadedTo = 0)
        $fileService = $this->app->make(\BookStack\Uploads\AttachmentService::class);
        $this->call('POST', 'attachments/link', [
            'attachment_link_url' => 'https://example.com',
            'attachment_link_name' => 'Example Attachment Link',
            'attachment_link_uploaded_to' => $page->id,
        ]);

        $attachmentId = Attachment::first()->id;

        $update = $this->call('PUT', 'attachments/' . $attachmentId, [
            'id' => $attachmentId,

Fixed code:
                'attachment_edit_url' =>  'string|min:1|max:255|safe_url'
                'attachment_link_url' =>  'required|string|min:1|max:255|safe_url'
        $attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId));
        Validator::extend('safe_url', function ($attribute, $value, $parameters, $validator) {
            $cleanLinkName = strtolower(trim($value));
            $isJs = strpos($cleanLinkName, 'javascript:') === 0;
            $isData = strpos($cleanLinkName, 'data:') === 0;
            return !$isJs && !$isData;
        });

    public function saveNewFromLink(string $name, string $link, int $page_id): Attachment
    public function updateFile(Attachment $attachment, array $requestData): Attachment


    'safe_url'             => 'The provided link may not be safe.',
use BookStack\Uploads\AttachmentService;
use Illuminate\Http\UploadedFile;
use Tests\TestResponse;
    protected function getTestFile(string $fileName): UploadedFile
        return new UploadedFile(base_path('tests/test-data/test-file.txt'), $fileName, 'text/plain', 55, null, true);
    protected function uploadFile(string $name, int $uploadedTo = 0): \Illuminate\Foundation\Testing\TestResponse
    /**
     * Create a new attachment
     */
    protected function createAttachment(Page $page): Attachment
    {
        $this->post('attachments/link', [
            'attachment_link_url' => 'https://example.com',
            'attachment_link_name' => 'Example Attachment Link',
            'attachment_link_uploaded_to' => $page->id,
        ]);

        return Attachment::query()->latest()->first();
    }

        $fileService = $this->app->make(AttachmentService::class);
        $attachment = $this->createAttachment($page);
        $update = $this->call('PUT', 'attachments/' . $attachment->id, [
            'id' => $attachment->id,

    public function test_data_and_js_links_cannot_be_attached_to_a_page()
    {
        $page = Page::first();
        $this->asAdmin();

        $badLinks = [
            'javascript:alert("bunny")',
            ' javascript:alert("bunny")',
            'JavaScript:alert("bunny")',
            "\t\n\t\nJavaScript:alert(\"bunny\")",
            "data:text/html;<a></a>",
            "Data:text/html;<a></a>",
            "Data:text/html;<a></a>",
        ];

        foreach ($badLinks as $badLink) {
            $linkReq = $this->post('attachments/link', [
                'attachment_link_url' => $badLink,
                'attachment_link_name' => 'Example Attachment Link',
                'attachment_link_uploaded_to' => $page->id,
            ]);
            $linkReq->assertStatus(422);
            $this->assertDatabaseMissing('attachments', [
                'path' => $badLink,
            ]);
        }

        $attachment = $this->createAttachment($page);

        foreach ($badLinks as $badLink) {
            $linkReq = $this->put('attachments/' . $attachment->id, [
                'attachment_edit_url' => $badLink,
                'attachment_edit_name' => 'Example Attachment Link',
            ]);
            $linkReq->assertStatus(422);
            $this->assertDatabaseMissing('attachments', [
                'path' => $badLink,
            ]);
        }
    }