Merge remote-tracking branch 'origin/stable' into next-minor

This commit is contained in:
Dylan K. Taylor 2022-01-06 22:43:57 +00:00
commit fb29653ed7
No known key found for this signature in database
GPG Key ID: 8927471A91CAFD3D
10 changed files with 118 additions and 21 deletions

View File

@ -4,13 +4,13 @@
</p> </p>
<p align="center"> <p align="center">
<img src="https://github.com/pmmp/PocketMine-MP/workflows/CI/badge.svg" alt="CI" /> <a href="https://github.com/pmmp/PocketMine-MP/actions/workflows/main.yml"><img src="https://github.com/pmmp/PocketMine-MP/workflows/CI/badge.svg" alt="CI" /></a>
<img alt="GitHub release (latest SemVer)" src="https://img.shields.io/github/v/release/pmmp/PocketMine-MP?label=release&sort=semver"> <a href="https://github.com/pmmp/PocketMine-MP/releases/latest"><img alt="GitHub release (latest SemVer)" src="https://img.shields.io/github/v/release/pmmp/PocketMine-MP?label=release&sort=semver"></a>
<a href="https://hub.docker.com/r/pmmp/pocketmine-mp"><img src="https://img.shields.io/docker/v/pmmp/pocketmine-mp?logo=docker&label=image" alt="Docker image version (latest semver)" /></a> <a href="https://hub.docker.com/r/pmmp/pocketmine-mp"><img src="https://img.shields.io/docker/v/pmmp/pocketmine-mp?logo=docker&label=image" alt="Docker image version (latest semver)" /></a>
<a href="https://discord.gg/bmSAZBG"><img src="https://img.shields.io/discord/373199722573201408?label=discord&color=7289DA&logo=discord" alt="Discord" /></a> <a href="https://discord.gg/bmSAZBG"><img src="https://img.shields.io/discord/373199722573201408?label=discord&color=7289DA&logo=discord" alt="Discord" /></a>
<br> <br>
<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/pmmp/PocketMine-MP/total?label=downloads%40total"> <a href="https://github.com/pmmp/PocketMine-MP/releases"><img alt="GitHub all releases" src="https://img.shields.io/github/downloads/pmmp/PocketMine-MP/total?label=downloads%40total"></a>
<img alt="GitHub release (latest by SemVer)" src="https://img.shields.io/github/downloads/pmmp/PocketMine-MP/latest/total?sort=semver"> <a href="https://github.com/pmmp/PocketMine-MP/releases/latest"><img alt="GitHub release (latest by SemVer)" src="https://img.shields.io/github/downloads/pmmp/PocketMine-MP/latest/total?sort=semver"></a>
</p> </p>
## Getting started ## Getting started

View File

@ -26,3 +26,7 @@ Plugin developers should **only** update their required API to this version if y
- Fixed skins appearing black when using RTX resource packs. - Fixed skins appearing black when using RTX resource packs.
- Fixed chunks containing furnaces in old worlds (pre-2017) being discarded as corrupted. - Fixed chunks containing furnaces in old worlds (pre-2017) being discarded as corrupted.
- This was caused by a strict corruption check detecting bad data created by a bug in PocketMine-MP that was fixed in 2017. - This was caused by a strict corruption check detecting bad data created by a bug in PocketMine-MP that was fixed in 2017.
# 3.26.5
- Fixed several denial-of-service attack vectors related to writable book text length and encoding.
- Fixed several denial-of-service attack vectors related to skin data field lengths.

View File

@ -1591,3 +1591,12 @@ Released 1st January 2022.
- Fixed incorrect message being displayed when trying to sleep in a bed which is too far away. - Fixed incorrect message being displayed when trying to sleep in a bed which is too far away.
- Fixed missing space between `Kicked by admin.` and `Reason` when using `/kick` to kick a player. - Fixed missing space between `Kicked by admin.` and `Reason` when using `/kick` to kick a player.
- Fixed client-side performance issue of entities with very large scale. - Fixed client-side performance issue of entities with very large scale.
# 4.0.5
Released 4th January 2022.
## Fixes
- Fixed several denial-of-service attack vectors related to writable book text length and encoding.
- Fixed several denial-of-service attack vectors related to skin data field lengths.
- Fixed food bar desync when cancelling `PlayerItemConsumeEvent` in a plugin.
- Fixed compass needles not updating when the world spawn is changed.

View File

@ -53,7 +53,7 @@
"webmozart/path-util": "^2.3" "webmozart/path-util": "^2.3"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "1.2.0", "phpstan/phpstan": "1.3.1",
"phpstan/phpstan-phpunit": "^1.0.0", "phpstan/phpstan-phpunit": "^1.0.0",
"phpstan/phpstan-strict-rules": "^1.0.0", "phpstan/phpstan-strict-rules": "^1.0.0",
"phpunit/phpunit": "^9.2" "phpunit/phpunit": "^9.2"

16
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "5d3bf1dc144f66c995d677cae7de8b63", "content-hash": "8b6fac2b9bbfe685d654ed417713d894",
"packages": [ "packages": [
{ {
"name": "adhocore/json-comment", "name": "adhocore/json-comment",
@ -1900,16 +1900,16 @@
}, },
{ {
"name": "phpstan/phpstan", "name": "phpstan/phpstan",
"version": "1.2.0", "version": "1.3.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan.git", "url": "https://github.com/phpstan/phpstan.git",
"reference": "cbe085f9fdead5b6d62e4c022ca52dc9427a10ee" "reference": "c3e7a5837829b3cd5907b895da73a4da084a9f8f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/cbe085f9fdead5b6d62e4c022ca52dc9427a10ee", "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c3e7a5837829b3cd5907b895da73a4da084a9f8f",
"reference": "cbe085f9fdead5b6d62e4c022ca52dc9427a10ee", "reference": "c3e7a5837829b3cd5907b895da73a4da084a9f8f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1925,7 +1925,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.2-dev" "dev-master": "1.3-dev"
} }
}, },
"autoload": { "autoload": {
@ -1940,7 +1940,7 @@
"description": "PHPStan - PHP Static Analysis Tool", "description": "PHPStan - PHP Static Analysis Tool",
"support": { "support": {
"issues": "https://github.com/phpstan/phpstan/issues", "issues": "https://github.com/phpstan/phpstan/issues",
"source": "https://github.com/phpstan/phpstan/tree/1.2.0" "source": "https://github.com/phpstan/phpstan/tree/1.3.1"
}, },
"funding": [ "funding": [
{ {
@ -1960,7 +1960,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2021-11-18T14:09:01+00:00" "time": "2022-01-04T17:12:37+00:00"
}, },
{ {
"name": "phpstan/phpstan-phpunit", "name": "phpstan/phpstan-phpunit",

View File

@ -24,6 +24,7 @@ declare(strict_types=1);
namespace pocketmine\entity; namespace pocketmine\entity;
use Ahc\Json\Comment as CommentedJsonDecoder; use Ahc\Json\Comment as CommentedJsonDecoder;
use pocketmine\utils\Limits;
use function implode; use function implode;
use function in_array; use function in_array;
use function json_encode; use function json_encode;
@ -49,7 +50,17 @@ final class Skin{
/** @var string */ /** @var string */
private $geometryData; private $geometryData;
private static function checkLength(string $string, string $name, int $maxLength) : void{
if(strlen($string) > $maxLength){
throw new InvalidSkinException("$name must be at most $maxLength bytes, but have " . strlen($string) . " bytes");
}
}
public function __construct(string $skinId, string $skinData, string $capeData = "", string $geometryName = "", string $geometryData = ""){ public function __construct(string $skinId, string $skinData, string $capeData = "", string $geometryName = "", string $geometryData = ""){
self::checkLength($skinId, "Skin ID", Limits::INT16_MAX);
self::checkLength($geometryName, "Geometry name", Limits::INT16_MAX);
self::checkLength($geometryData, "Geometry data", Limits::INT32_MAX);
if($skinId === ""){ if($skinId === ""){
throw new InvalidSkinException("Skin ID must not be empty"); throw new InvalidSkinException("Skin ID must not be empty");
} }

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\Facing; use pocketmine\math\Facing;
use pocketmine\math\Vector3; use pocketmine\math\Vector3;
@ -101,6 +102,8 @@ use pocketmine\network\mcpe\protocol\types\PlayerBlockActionWithBlockInfo;
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;
@ -112,8 +115,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;
@ -741,6 +746,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);
@ -750,10 +773,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:
@ -762,7 +786,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:
@ -781,11 +806,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:
@ -801,7 +829,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;

View File

@ -2258,6 +2258,11 @@ class World implements ChunkManager{
$previousSpawn = $this->getSpawnLocation(); $previousSpawn = $this->getSpawnLocation();
$this->provider->getWorldData()->setSpawn($pos); $this->provider->getWorldData()->setSpawn($pos);
(new SpawnChangeEvent($this, $previousSpawn))->call(); (new SpawnChangeEvent($this, $previousSpawn))->call();
$location = Position::fromObject($pos, $this);
foreach($this->players as $player){
$player->getNetworkSession()->syncWorldSpawnPoint($location);
}
} }
/** /**