Limit a story to 64 KB before HTML Purifier runs.

A preview purified any posted story, even without a token or a captcha. A few megabytes took seconds and hundreds of megabytes.
This commit is contained in:
tim
2026-09-26 15:27:51 +02:00
parent bf36ec25fd
commit ed6f98e0ac
4 changed files with 31 additions and 0 deletions
+12
View File
@@ -3,6 +3,13 @@ require_once('open_db.lib.php');
require_once(__DIR__ . '/lib/purify.inc.php'); require_once(__DIR__ . '/lib/purify.inc.php');
require_once(__DIR__ . '/html.lib.php'); require_once(__DIR__ . '/html.lib.php');
// The longest story HTML, in bytes. HTML Purifier needs seconds for a few megabytes.
define('MAX_STORY_BYTES', 65536);
function story_too_long($story) {
return strlen((string)$story) > MAX_STORY_BYTES;
}
function display_stories($id = NULL) { function display_stories($id = NULL) {
global $db, $is_admin, $editable, $action; global $db, $is_admin, $editable, $action;
if (is_null($id)) if (is_null($id))
@@ -10,17 +17,20 @@ function display_stories($id = NULL) {
$search = get_search_args(); $search = get_search_args();
$page = ''; $page = '';
$nolink = NULL; $nolink = NULL;
$posted = FALSE;
if ($action == 'preview') { if ($action == 'preview') {
global $file_src; global $file_src;
$date = 'Awaiting posting'; $date = 'Awaiting posting';
$result = get_post_data(TRUE); $result = get_post_data(TRUE);
$nolink = TRUE; $nolink = TRUE;
$posted = TRUE;
} else if ($action == 'submit') { } else if ($action == 'submit') {
$date = 'Posted '.gmdate("Y-m-d H:i"); $date = 'Posted '.gmdate("Y-m-d H:i");
if (!$is_admin) { if (!$is_admin) {
$result = get_post_data(TRUE); $result = get_post_data(TRUE);
$nolink = TRUE; $nolink = TRUE;
$posted = TRUE;
} else { } else {
$result = get_query($id); $result = get_query($id);
} }
@@ -90,6 +100,8 @@ date("Y-m-d H:i", strtotime($entry['date'])) . '</time>';
} else { } else {
$page .= '<img src="/display?id='.$entry_id.'" alt="'.$title.'" />'; $page .= '<img src="/display?id='.$entry_id.'" alt="'.$title.'" />';
} }
} else if ($posted && story_too_long($entry['story'])) {
$page .= '<p>The story is too long to show.</p>'."\n";
} else { } else {
$page .= purify_story($entry['story'])."\n"; $page .= purify_story($entry['story'])."\n";
} }
+4
View File
@@ -206,6 +206,10 @@ function validate_data() {
$validate['title'] = "You have not submitted a title."; $validate['title'] = "You have not submitted a title.";
} }
if (story_too_long($story)) {
$validate['story'] = 'The story is too long. Please shorten it.';
}
if ($remove_img && !$story) { if ($remove_img && !$story) {
$validate['story'] = "You've tried to remove an image without replacing it with text."; $validate['story'] = "You've tried to remove an image without replacing it with text.";
} }
+9
View File
@@ -111,6 +111,15 @@ class DisplayStoriesTest extends TestCase {
$this->assertStringNotContainsString('x()', $page); $this->assertStringNotContainsString('x()', $page);
} }
// HTML Purifier needs seconds for a few megabytes, so a posted story over the limit is not shown
public function testPreviewDoesNotShowATooLongStory(): void {
$GLOBALS['action'] = 'preview';
$this->request(array(), array('title' => 'Long', 'author' => 'Me', 'story' => '<p><b>x</b></p>' . str_repeat('a', 65536)));
$page = display_stories();
$this->assertStringContainsString('<p>The story is too long to show.</p>', $page);
$this->assertStringNotContainsString('<b>x</b>', $page);
}
public function testPreviewShowsAPendingImage(): void { public function testPreviewShowsAPendingImage(): void {
$GLOBALS['action'] = 'preview'; $GLOBALS['action'] = 'preview';
$GLOBALS['file_src'] = '/display?pending="><script>'; $GLOBALS['file_src'] = '/display?pending="><script>';
+6
View File
@@ -35,6 +35,12 @@ class ValidateDataTest extends TestCase {
$this->assertSame(array(), $this->validate(array('remove_img' => '1'))); $this->assertSame(array(), $this->validate(array('remove_img' => '1')));
} }
public function testATooLongStoryIsRefused(): void {
$this->assertSame(array(), $this->validate(array('story' => str_repeat('a', 65536))));
$this->assertSame(array('story' => 'The story is too long. Please shorten it.'),
$this->validate(array('story' => str_repeat('a', 65537))));
}
public function testVisitorsNeedTheCaptcha(): void { public function testVisitorsNeedTheCaptcha(): void {
FakeRecaptcha::install('{"tokenProperties":{"valid":false}}'); FakeRecaptcha::install('{"tokenProperties":{"valid":false}}');
$this->assertSame(array('captcha' => 'Please complete the captcha.'), $this->validate(array('g-recaptcha-response' => 'r'), FALSE)); $this->assertSame(array('captcha' => 'Please complete the captcha.'), $this->validate(array('g-recaptcha-response' => 'r'), FALSE));