mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-05-04 18:08:46 +03:00
Responses: Added extra sanitization for download names
From testing, don't think this could exploited directly, as the response would error instead of allowing control characters, but this adds an extra layer of sanitization, and switches to encoded disposition filenames for better UTF8 support.
This commit is contained in:
@@ -102,12 +102,15 @@ class DownloadResponseFactory
|
||||
protected function getHeaders(string $fileName, int $fileSize, string $mime = 'application/octet-stream'): array
|
||||
{
|
||||
$disposition = ($mime === 'application/octet-stream') ? 'attachment' : 'inline';
|
||||
$downloadName = str_replace('"', '', $fileName);
|
||||
|
||||
$downloadName = str_replace(['"', '/', '\\', '$'], '', $fileName);
|
||||
$downloadName = preg_replace('/[\x00-\x1F\x7F]/', '', $downloadName);
|
||||
$encodedDownloadName = rawurlencode($downloadName);
|
||||
|
||||
return [
|
||||
'Content-Type' => $mime,
|
||||
'Content-Length' => $fileSize,
|
||||
'Content-Disposition' => "{$disposition}; filename=\"{$downloadName}\"",
|
||||
'Content-Disposition' => "{$disposition}; filename*=UTF-8''{$encodedDownloadName}",
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ class ExportsApiTest extends TestCase
|
||||
$resp = $this->get("/api/books/{$book->id}/export/html");
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSee($book->name);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.html"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.html');
|
||||
}
|
||||
|
||||
public function test_book_plain_text_endpoint()
|
||||
@@ -30,7 +30,7 @@ class ExportsApiTest extends TestCase
|
||||
$resp = $this->get("/api/books/{$book->id}/export/plaintext");
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSee($book->name);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.txt"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.txt');
|
||||
}
|
||||
|
||||
public function test_book_pdf_endpoint()
|
||||
@@ -40,7 +40,7 @@ class ExportsApiTest extends TestCase
|
||||
|
||||
$resp = $this->get("/api/books/{$book->id}/export/pdf");
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.pdf"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.pdf');
|
||||
}
|
||||
|
||||
public function test_book_markdown_endpoint()
|
||||
@@ -50,7 +50,7 @@ class ExportsApiTest extends TestCase
|
||||
|
||||
$resp = $this->get("/api/books/{$book->id}/export/markdown");
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.md"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.md');
|
||||
$resp->assertSee('# ' . $book->name);
|
||||
$resp->assertSee('# ' . $book->pages()->first()->name);
|
||||
$resp->assertSee('# ' . $book->chapters()->first()->name);
|
||||
@@ -63,7 +63,7 @@ class ExportsApiTest extends TestCase
|
||||
|
||||
$resp = $this->get("/api/books/{$book->id}/export/zip");
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.zip"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.zip');
|
||||
|
||||
$zip = ZipTestHelper::extractFromZipResponse($resp);
|
||||
$this->assertArrayHasKey('book', $zip->data);
|
||||
@@ -77,7 +77,7 @@ class ExportsApiTest extends TestCase
|
||||
$resp = $this->get("/api/chapters/{$chapter->id}/export/html");
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSee($chapter->name);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.html"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.html');
|
||||
}
|
||||
|
||||
public function test_chapter_plain_text_endpoint()
|
||||
@@ -88,7 +88,7 @@ class ExportsApiTest extends TestCase
|
||||
$resp = $this->get("/api/chapters/{$chapter->id}/export/plaintext");
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSee($chapter->name);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.txt"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.txt');
|
||||
}
|
||||
|
||||
public function test_chapter_pdf_endpoint()
|
||||
@@ -98,7 +98,7 @@ class ExportsApiTest extends TestCase
|
||||
|
||||
$resp = $this->get("/api/chapters/{$chapter->id}/export/pdf");
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.pdf"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.pdf');
|
||||
}
|
||||
|
||||
public function test_chapter_markdown_endpoint()
|
||||
@@ -108,7 +108,7 @@ class ExportsApiTest extends TestCase
|
||||
|
||||
$resp = $this->get("/api/chapters/{$chapter->id}/export/markdown");
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.md"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.md');
|
||||
$resp->assertSee('# ' . $chapter->name);
|
||||
$resp->assertSee('# ' . $chapter->pages()->first()->name);
|
||||
}
|
||||
@@ -120,7 +120,7 @@ class ExportsApiTest extends TestCase
|
||||
|
||||
$resp = $this->get("/api/chapters/{$chapter->id}/export/zip");
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.zip"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.zip');
|
||||
|
||||
$zip = ZipTestHelper::extractFromZipResponse($resp);
|
||||
$this->assertArrayHasKey('chapter', $zip->data);
|
||||
@@ -134,7 +134,7 @@ class ExportsApiTest extends TestCase
|
||||
$resp = $this->get("/api/pages/{$page->id}/export/html");
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSee($page->name);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.html"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.html');
|
||||
}
|
||||
|
||||
public function test_page_plain_text_endpoint()
|
||||
@@ -145,7 +145,7 @@ class ExportsApiTest extends TestCase
|
||||
$resp = $this->get("/api/pages/{$page->id}/export/plaintext");
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSee($page->name);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.txt"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.txt');
|
||||
}
|
||||
|
||||
public function test_page_pdf_endpoint()
|
||||
@@ -155,7 +155,7 @@ class ExportsApiTest extends TestCase
|
||||
|
||||
$resp = $this->get("/api/pages/{$page->id}/export/pdf");
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.pdf"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.pdf');
|
||||
}
|
||||
|
||||
public function test_page_markdown_endpoint()
|
||||
@@ -166,7 +166,7 @@ class ExportsApiTest extends TestCase
|
||||
$resp = $this->get("/api/pages/{$page->id}/export/markdown");
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSee('# ' . $page->name);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.md"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.md');
|
||||
}
|
||||
|
||||
public function test_page_zip_endpoint()
|
||||
@@ -176,7 +176,7 @@ class ExportsApiTest extends TestCase
|
||||
|
||||
$resp = $this->get("/api/pages/{$page->id}/export/zip");
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.zip"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.zip');
|
||||
|
||||
$zip = ZipTestHelper::extractFromZipResponse($resp);
|
||||
$this->assertArrayHasKey('page', $zip->data);
|
||||
|
||||
@@ -18,7 +18,7 @@ class HtmlExportTest extends TestCase
|
||||
$resp = $this->get($page->getUrl('/export/html'));
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSee($page->name);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.html"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.html');
|
||||
}
|
||||
|
||||
public function test_book_html_export()
|
||||
@@ -31,7 +31,7 @@ class HtmlExportTest extends TestCase
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSee($book->name);
|
||||
$resp->assertSee($page->name);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.html"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.html');
|
||||
}
|
||||
|
||||
public function test_book_html_export_shows_html_descriptions()
|
||||
@@ -58,7 +58,7 @@ class HtmlExportTest extends TestCase
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSee($chapter->name);
|
||||
$resp->assertSee($page->name);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.html"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.html');
|
||||
}
|
||||
|
||||
public function test_chapter_html_export_shows_html_descriptions()
|
||||
|
||||
@@ -14,7 +14,7 @@ class MarkdownExportTest extends TestCase
|
||||
$resp = $this->asEditor()->get($page->getUrl('/export/markdown'));
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSee($page->name);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.md"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.md');
|
||||
}
|
||||
|
||||
public function test_page_markdown_export_uses_existing_markdown_if_apparent()
|
||||
|
||||
@@ -17,7 +17,7 @@ class PdfExportTest extends TestCase
|
||||
|
||||
$resp = $this->get($page->getUrl('/export/pdf'));
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.pdf"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.pdf');
|
||||
}
|
||||
|
||||
public function test_book_pdf_export()
|
||||
@@ -28,7 +28,7 @@ class PdfExportTest extends TestCase
|
||||
|
||||
$resp = $this->get($book->getUrl('/export/pdf'));
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.pdf"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.pdf');
|
||||
}
|
||||
|
||||
public function test_chapter_pdf_export()
|
||||
@@ -38,7 +38,7 @@ class PdfExportTest extends TestCase
|
||||
|
||||
$resp = $this->get($chapter->getUrl('/export/pdf'));
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.pdf"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.pdf');
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class TextExportTest extends TestCase
|
||||
$resp = $this->get($page->getUrl('/export/plaintext'));
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSee($page->name);
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.txt"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.txt');
|
||||
}
|
||||
|
||||
public function test_book_text_export()
|
||||
@@ -35,7 +35,7 @@ class TextExportTest extends TestCase
|
||||
$resp->assertSee($directPage->name);
|
||||
$resp->assertSee('My awesome page');
|
||||
$resp->assertSee('My little nested page');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.txt"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.txt');
|
||||
}
|
||||
|
||||
public function test_book_text_export_format()
|
||||
@@ -68,7 +68,7 @@ class TextExportTest extends TestCase
|
||||
$resp->assertSee($chapter->name);
|
||||
$resp->assertSee($page->name);
|
||||
$resp->assertSee('This is content within the page!');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.txt"');
|
||||
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.txt');
|
||||
}
|
||||
|
||||
public function test_chapter_text_export_format()
|
||||
|
||||
@@ -324,7 +324,7 @@ class AttachmentTest extends TestCase
|
||||
$attachmentGet = $this->get($attachment->getUrl(true));
|
||||
// http-foundation/Response does some 'fixing' of responses to add charsets to text responses.
|
||||
$attachmentGet->assertHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
$attachmentGet->assertHeader('Content-Disposition', 'inline; filename="upload_test_file.txt"');
|
||||
$attachmentGet->assertHeader('Content-Disposition', 'inline; filename*=UTF-8\'\'upload_test_file.txt');
|
||||
$attachmentGet->assertHeader('X-Content-Type-Options', 'nosniff');
|
||||
|
||||
$this->files->deleteAllAttachmentFiles();
|
||||
@@ -340,7 +340,22 @@ class AttachmentTest extends TestCase
|
||||
$attachmentGet = $this->get($attachment->getUrl(true));
|
||||
// http-foundation/Response does some 'fixing' of responses to add charsets to text responses.
|
||||
$attachmentGet->assertHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
$attachmentGet->assertHeader('Content-Disposition', 'inline; filename="test_file.html"');
|
||||
$attachmentGet->assertHeader('Content-Disposition', 'inline; filename*=UTF-8\'\'test_file.html');
|
||||
|
||||
$this->files->deleteAllAttachmentFiles();
|
||||
}
|
||||
|
||||
public function test_file_access_name_in_content_disposition_header_is_sanitized()
|
||||
{
|
||||
$page = $this->entities->page();
|
||||
$this->asAdmin();
|
||||
|
||||
$attachment = $this->files->uploadAttachmentDataToPage($this, $page, 'test_file.html', '<html></html><p>testing</p>', 'text/html');
|
||||
$attachment->name = "my\\_/super\n_fu\$n_\tfile";
|
||||
$attachment->save();
|
||||
|
||||
$attachmentGet = $this->get($attachment->getUrl(true));
|
||||
$attachmentGet->assertHeader('Content-Disposition', 'inline; filename*=UTF-8\'\'my_super_fun_file.html');
|
||||
|
||||
$this->files->deleteAllAttachmentFiles();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user