Merge pull request from GHSA-p62j-hrxm-xcxf

This checks the following things:
- Validity of UTF-8 encoding of title, author, and page content
- Maximum soft and hard lengths of title, author, and page content (soft
  limits may be bypassed by uncancelling PlayerEditBookEvent; hard
  limits may not be bypassed)
- Maximum number of pages. Books with more than 50 pages may still be
  edited, but may not have new pages added.
This commit is contained in:
Dylan T 2022-01-04 20:39:02 +00:00 committed by GitHub
parent aeab19a616
commit 3ed57ce49a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 75 additions and 7 deletions

View File

@ -23,17 +23,32 @@ declare(strict_types=1);
namespace pocketmine\item; namespace pocketmine\item;
use pocketmine\utils\Limits;
use pocketmine\utils\Utils; use pocketmine\utils\Utils;
use function sprintf;
use function strlen;
class WritableBookPage{ class WritableBookPage{
public const PAGE_LENGTH_HARD_LIMIT_BYTES = Limits::INT16_MAX;
public const PHOTO_NAME_LENGTH_HARD_LIMIT_BYTES = Limits::INT16_MAX;
/** @var string */ /** @var string */
private $text; private $text;
/** @var string */ /** @var string */
private $photoName; private $photoName;
/**
* @throws \InvalidArgumentException
*/
private static function checkLength(string $string, string $name, int $maxLength) : void{
if(strlen($string) > $maxLength){
throw new \InvalidArgumentException(sprintf("$name must be at most %d bytes, but have %d bytes", $maxLength, strlen($string)));
}
}
public function __construct(string $text, string $photoName = ""){ public function __construct(string $text, string $photoName = ""){
//TODO: data validation self::checkLength($text, "Text", self::PAGE_LENGTH_HARD_LIMIT_BYTES);
self::checkLength($photoName, "Photo name", self::PHOTO_NAME_LENGTH_HARD_LIMIT_BYTES);
Utils::checkUTF8($text); Utils::checkUTF8($text);
$this->text = $text; $this->text = $text;
$this->photoName = $photoName; $this->photoName = $photoName;

View File

@ -24,7 +24,10 @@ declare(strict_types=1);
namespace pocketmine\item; namespace pocketmine\item;
use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\CompoundTag;
use pocketmine\utils\Limits;
use pocketmine\utils\Utils; use pocketmine\utils\Utils;
use function sprintf;
use function strlen;
class WrittenBook extends WritableBookBase{ class WrittenBook extends WritableBookBase{
@ -85,6 +88,9 @@ class WrittenBook extends WritableBookBase{
* @return $this * @return $this
*/ */
public function setAuthor(string $authorName) : self{ public function setAuthor(string $authorName) : self{
if(strlen($authorName) > Limits::INT16_MAX){
throw new \InvalidArgumentException(sprintf("Author must be at most %d bytes, but have %d bytes", Limits::INT16_MAX, strlen($authorName)));
}
Utils::checkUTF8($authorName); Utils::checkUTF8($authorName);
$this->author = $authorName; $this->author = $authorName;
return $this; return $this;
@ -103,6 +109,9 @@ class WrittenBook extends WritableBookBase{
* @return $this * @return $this
*/ */
public function setTitle(string $title) : self{ public function setTitle(string $title) : self{
if(strlen($title) > Limits::INT16_MAX){
throw new \InvalidArgumentException(sprintf("Title must be at most %d bytes, but have %d bytes", Limits::INT16_MAX, strlen($title)));
}
Utils::checkUTF8($title); Utils::checkUTF8($title);
$this->title = $title; $this->title = $title;
return $this; return $this;

View File

@ -37,6 +37,7 @@ use pocketmine\inventory\transaction\TransactionException;
use pocketmine\inventory\transaction\TransactionValidationException; use pocketmine\inventory\transaction\TransactionValidationException;
use pocketmine\item\VanillaItems; use pocketmine\item\VanillaItems;
use pocketmine\item\WritableBook; use pocketmine\item\WritableBook;
use pocketmine\item\WritableBookPage;
use pocketmine\item\WrittenBook; use pocketmine\item\WrittenBook;
use pocketmine\math\Vector3; use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\CompoundTag;
@ -96,6 +97,8 @@ use pocketmine\network\mcpe\protocol\types\PlayerAction;
use pocketmine\network\PacketHandlingException; use pocketmine\network\PacketHandlingException;
use pocketmine\player\Player; use pocketmine\player\Player;
use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Limits;
use pocketmine\utils\TextFormat;
use function array_push; use function array_push;
use function base64_encode; use function base64_encode;
use function count; use function count;
@ -107,8 +110,10 @@ use function json_decode;
use function json_encode; use function json_encode;
use function json_last_error_msg; use function json_last_error_msg;
use function max; use function max;
use function mb_strlen;
use function microtime; use function microtime;
use function preg_match; use function preg_match;
use function sprintf;
use function strlen; use function strlen;
use function strpos; use function strpos;
use function substr; use function substr;
@ -711,6 +716,24 @@ class InGamePacketHandler extends PacketHandler{
return false; //TODO return false; //TODO
} }
/**
* @throws PacketHandlingException
*/
private function checkBookText(string $string, string $fieldName, int $softLimit, int $hardLimit, bool &$cancel) : string{
if(strlen($string) > $hardLimit){
throw new PacketHandlingException(sprintf("Book %s must be at most %d bytes, but have %d bytes", $fieldName, $hardLimit, strlen($string)));
}
$result = TextFormat::clean($string, false);
//strlen() is O(1), mb_strlen() is O(n)
if(strlen($result) > $softLimit * 4 || mb_strlen($result, 'UTF-8') > $softLimit){
$cancel = true;
$this->session->getLogger()->debug("Cancelled book edit due to $fieldName exceeded soft limit of $softLimit chars");
}
return $result;
}
public function handleBookEdit(BookEditPacket $packet) : bool{ public function handleBookEdit(BookEditPacket $packet) : bool{
//TODO: break this up into book API things //TODO: break this up into book API things
$oldBook = $this->player->getInventory()->getItem($packet->inventorySlot); $oldBook = $this->player->getInventory()->getItem($packet->inventorySlot);
@ -720,10 +743,11 @@ class InGamePacketHandler extends PacketHandler{
$newBook = clone $oldBook; $newBook = clone $oldBook;
$modifiedPages = []; $modifiedPages = [];
$cancel = false;
switch($packet->type){ switch($packet->type){
case BookEditPacket::TYPE_REPLACE_PAGE: case BookEditPacket::TYPE_REPLACE_PAGE:
$newBook->setPageText($packet->pageNumber, $packet->text); $text = self::checkBookText($packet->text, "page text", 256, WritableBookPage::PAGE_LENGTH_HARD_LIMIT_BYTES, $cancel);
$newBook->setPageText($packet->pageNumber, $text);
$modifiedPages[] = $packet->pageNumber; $modifiedPages[] = $packet->pageNumber;
break; break;
case BookEditPacket::TYPE_ADD_PAGE: case BookEditPacket::TYPE_ADD_PAGE:
@ -732,7 +756,8 @@ class InGamePacketHandler extends PacketHandler{
//TODO: the client can send insert-before actions on trailing client-side pages which cause odd behaviour on the server //TODO: the client can send insert-before actions on trailing client-side pages which cause odd behaviour on the server
return false; return false;
} }
$newBook->insertPage($packet->pageNumber, $packet->text); $text = self::checkBookText($packet->text, "page text", 256, WritableBookPage::PAGE_LENGTH_HARD_LIMIT_BYTES, $cancel);
$newBook->insertPage($packet->pageNumber, $text);
$modifiedPages[] = $packet->pageNumber; $modifiedPages[] = $packet->pageNumber;
break; break;
case BookEditPacket::TYPE_DELETE_PAGE: case BookEditPacket::TYPE_DELETE_PAGE:
@ -751,11 +776,14 @@ class InGamePacketHandler extends PacketHandler{
$modifiedPages = [$packet->pageNumber, $packet->secondaryPageNumber]; $modifiedPages = [$packet->pageNumber, $packet->secondaryPageNumber];
break; break;
case BookEditPacket::TYPE_SIGN_BOOK: case BookEditPacket::TYPE_SIGN_BOOK:
/** @var WrittenBook $newBook */ $title = self::checkBookText($packet->title, "title", 16, Limits::INT16_MAX, $cancel);
//this one doesn't have a limit in vanilla, so we have to improvise
$author = self::checkBookText($packet->author, "author", 256, Limits::INT16_MAX, $cancel);
$newBook = VanillaItems::WRITTEN_BOOK() $newBook = VanillaItems::WRITTEN_BOOK()
->setPages($oldBook->getPages()) ->setPages($oldBook->getPages())
->setAuthor($packet->author) ->setAuthor($author)
->setTitle($packet->title) ->setTitle($title)
->setGeneration(WrittenBook::GENERATION_ORIGINAL); ->setGeneration(WrittenBook::GENERATION_ORIGINAL);
break; break;
default: default:
@ -771,7 +799,23 @@ class InGamePacketHandler extends PacketHandler{
BookEditPacket::TYPE_SIGN_BOOK => PlayerEditBookEvent::ACTION_SIGN_BOOK, BookEditPacket::TYPE_SIGN_BOOK => PlayerEditBookEvent::ACTION_SIGN_BOOK,
default => throw new AssumptionFailedError("We already filtered unknown types in the switch above") default => throw new AssumptionFailedError("We already filtered unknown types in the switch above")
}; };
/*
* Plugins may have created books with more than 50 pages; we allow plugins to do this, but not players.
* Don't allow the page count to grow past 50, but allow deleting, swapping or altering text of existing pages.
*/
$oldPageCount = count($oldBook->getPages());
$newPageCount = count($newBook->getPages());
if(($newPageCount > $oldPageCount && $newPageCount > 50)){
$this->session->getLogger()->debug("Cancelled book edit due to adding too many pages (new page count would be $newPageCount)");
$cancel = true;
}
$event = new PlayerEditBookEvent($this->player, $oldBook, $newBook, $action, $modifiedPages); $event = new PlayerEditBookEvent($this->player, $oldBook, $newBook, $action, $modifiedPages);
if($cancel){
$event->cancel();
}
$event->call(); $event->call();
if($event->isCancelled()){ if($event->isCancelled()){
return true; return true;