Merge branch '5.2' into 5.x

* 5.2:
  Dont allow unserializing classes with a destructor
  Dont allow unserializing classes with a destructor - 4.4
  [Cache] fix possible collision when writing tmp file in filesystem adapter
  a colon followed by spaces exclusively separates mapping keys and values
  Contracts: Remove ellipsis
  fix handling float-like key attribute values
  Fix transient test with HttpClient jitter
  Fix missing BCC recipients in SES bridge
  Move AuthenticationSuccessEvent outside try/catch block
  Dont allow unserializing classes with a destructor - 5.2
  Dont allow unserializing classes with a destructor - 5.1
This commit is contained in:
Nicolas Grekas 2021-01-12 15:29:08 +01:00
commit a176f8767c
39 changed files with 352 additions and 47 deletions

View File

@ -129,6 +129,16 @@ class ElasticsearchLogstashHandler extends AbstractHandler
$this->wait(false);
}
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->wait(true);

View File

@ -89,6 +89,12 @@ class AppKernel extends Kernel implements ExtensionInterface, ConfigurationInter
public function __wakeup()
{
foreach ($this as $k => $v) {
if (\is_object($v)) {
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
}
$this->__construct($this->varDir, $this->testCase, $this->rootConfig, $this->environment, $this->debug);
}

View File

@ -93,9 +93,20 @@ trait FilesystemCommonTrait
set_error_handler(__CLASS__.'::throwError');
try {
if (null === $this->tmp) {
$this->tmp = $this->directory.uniqid('', true);
$this->tmp = $this->directory.bin2hex(random_bytes(6));
}
file_put_contents($this->tmp, $data);
try {
$h = fopen($this->tmp, 'x');
} catch (\ErrorException $e) {
if (false === strpos($e->getMessage(), 'File exists')) {
throw $e;
}
$this->tmp = $this->directory.bin2hex(random_bytes(6));
$h = fopen($this->tmp, 'x');
}
fwrite($h, $data);
fclose($h);
if (null !== $expiresAt) {
touch($this->tmp, $expiresAt);

View File

@ -227,6 +227,10 @@ class PrototypedArrayNode extends ArrayNode
} elseif (isset($v[$this->keyAttribute])) {
$k = $v[$this->keyAttribute];
if (\is_float($k)) {
$k = var_export($k, true);
}
// remove the key attribute when required
if ($this->removeKeyAttribute) {
unset($v[$this->keyAttribute]);

View File

@ -201,6 +201,32 @@ class NormalizationTest extends TestCase
$this->assertNormalized($tree, $data, $data);
}
public function testFloatLikeValueAsMapKeyAttribute()
{
$tree = (new TreeBuilder('root'))
->getRootNode()
->useAttributeAsKey('number')
->arrayPrototype()
->children()
->scalarNode('foo')->end()
->end()
->end()
->end()
->buildTree()
;
$this->assertNormalized($tree, [
[
'number' => 3.0,
'foo' => 'bar',
],
], [
'3.0' => [
'foo' => 'bar',
],
]);
}
public static function assertNormalized(NodeInterface $tree, $denormalized, $normalized)
{
self::assertSame($normalized, $tree->normalize($denormalized));

View File

@ -40,6 +40,16 @@ abstract class AbstractConfigurator
throw new \BadMethodCallException(sprintf('Call to undefined method "%s::%s()".', static::class, $method));
}
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
/**
* Checks that a value is valid, optionally replacing Definition and Reference configurators by their configure value.
*

View File

@ -35,6 +35,16 @@ class BufferingLogger extends AbstractLogger
return $logs;
}
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
foreach ($this->logs as [$level, $message, $context]) {

View File

@ -76,6 +76,16 @@ class OrderedHashMapIterator implements \Iterator
$this->managedCursors[$this->cursorId] = &$this->cursor;
}
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
/**
* Removes the iterator's cursors from the managed cursors of the
* corresponding {@link OrderedHashMap} instance.

View File

@ -120,6 +120,16 @@ class ErrorChunk implements ChunkInterface
return $this->didThrow;
}
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
if (!$this->didThrow) {

View File

@ -367,6 +367,16 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
}
}
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->reset();

View File

@ -218,6 +218,16 @@ final class HttplugClient implements HttplugInterface, HttpAsyncClient, RequestF
throw new \LogicException(sprintf('You cannot use "%s()" as the "nyholm/psr7" package is not installed. Try running "composer require nyholm/psr7".', __METHOD__));
}
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->wait();

View File

@ -142,6 +142,16 @@ final class AmpResponse implements ResponseInterface, StreamableInterface
return null !== $type ? $this->info[$type] ?? null : $this->info;
}
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
try {

View File

@ -127,6 +127,16 @@ trait CommonResponseTrait
return $stream;
}
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
/**
* Closes the response and all its network handles.
*/

View File

@ -44,6 +44,16 @@ class TraceableResponse implements ResponseInterface, StreamableInterface
$this->event = $event;
}
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
try {

View File

@ -93,19 +93,16 @@ class GenericRetryStrategyTest extends TestCase
public function testJitter()
{
$strategy = new GenericRetryStrategy([], 1000, 1, 0, 1);
$belowHalf = 0;
$aboveHalf = 0;
for ($i = 0; $i < 20; ++$i) {
$min = 2000;
$max = 0;
for ($i = 0; $i < 50; ++$i) {
$delay = $strategy->getDelay($this->getContext(0, 'GET', 'http://example.com/', 200), null, null);
if ($delay < 500) {
++$belowHalf;
} elseif ($delay > 1500) {
++$aboveHalf;
}
$min = min($min, $delay);
$max = max($max, $delay);
}
$this->assertGreaterThanOrEqual(1, $belowHalf);
$this->assertGreaterThanOrEqual(1, $aboveHalf);
$this->assertGreaterThanOrEqual(1000, $max - $min);
$this->assertGreaterThanOrEqual(1000, $max);
$this->assertLessThanOrEqual(1000, $min);
}
private function getContext($retryCount, $method, $url, $statusCode): AsyncContext

View File

@ -179,7 +179,7 @@ class DumpDataCollector extends DataCollector implements DataDumperInterface
$fileLinkFormat = array_pop($this->data);
$this->dataCount = \count($this->data);
self::__construct($this->stopwatch, $fileLinkFormat, $charset);
self::__construct($this->stopwatch, \is_string($fileLinkFormat) || $fileLinkFormat instanceof FileLinkFormatter ? $fileLinkFormat : null, \is_string($charset) ? $charset : null);
}
public function getDumpsCount(): int

View File

@ -865,6 +865,10 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl
public function __wakeup()
{
if (\is_object($this->environment) || \is_object($this->debug)) {
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
$this->__construct($this->environment, $this->debug);
}
}

View File

@ -35,6 +35,16 @@ class Connection extends AbstractConnection
/** @var resource */
private $connection;
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->disconnect();

View File

@ -38,6 +38,16 @@ class Query extends AbstractQuery
parent::__construct($connection, $dn, $query, $options);
}
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$con = $this->connection->getResource();

View File

@ -49,6 +49,16 @@ final class Lock implements SharedLockInterface, LoggerAwareInterface
$this->logger = new NullLogger();
}
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
/**
* Automatically releases the underlying lock when the object is destructed.
*/

View File

@ -63,6 +63,12 @@ class SesHttpTransportTest extends TestCase
$this->assertStringContainsString('AWS3-HTTPS AWSAccessKeyId=ACCESS_KEY,Algorithm=HmacSHA256,Signature=', $options['headers'][0] ?? $options['request_headers'][0]);
parse_str($options['body'], $body);
$this->assertArrayHasKey('Destinations_member_1', $body);
$this->assertSame('saif.gmati@symfony.com', $body['Destinations_member_1']);
$this->assertArrayHasKey('Destinations_member_2', $body);
$this->assertSame('jeremy@derusse.com', $body['Destinations_member_2']);
$content = base64_decode($body['RawMessage_Data']);
$this->assertStringContainsString('Hello!', $content);
@ -88,6 +94,7 @@ class SesHttpTransportTest extends TestCase
$mail = new Email();
$mail->subject('Hello!')
->to(new Address('saif.gmati@symfony.com', 'Saif Eddin'))
->bcc(new Address('jeremy@derusse.com', 'Jérémy Derussé'))
->from(new Address('fabpot@symfony.com', 'Fabien'))
->text('Hello There!');

View File

@ -66,8 +66,14 @@ class SesHttpTransport extends AbstractHttpTransport
],
];
if (($message->getOriginalMessage() instanceof Message)
&& $configurationSetHeader = $message->getOriginalMessage()->getHeaders()->get('X-SES-CONFIGURATION-SET')) {
$index = 1;
foreach ($message->getEnvelope()->getRecipients() as $recipient) {
$request['body']['Destinations.member.'.$index++] = $recipient->getAddress();
}
if ($message->getOriginalMessage() instanceof Message
&& $configurationSetHeader = $message->getOriginalMessage()->getHeaders()->get('X-SES-CONFIGURATION-SET')
) {
$request['body']['ConfigurationSetName'] = $configurationSetHeader->getBodyAsString();
}

View File

@ -340,6 +340,16 @@ class SmtpTransport extends AbstractTransport
$this->restartCounter = 0;
}
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->stop();

View File

@ -66,6 +66,16 @@ class Connection
$this->queueUrl = $queueUrl;
}
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->reset();

View File

@ -155,7 +155,13 @@ class DataPart extends TextPart
$r->setValue($this, $this->_headers);
unset($this->_headers);
if (!\is_array($this->_parent)) {
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
foreach (['body', 'charset', 'subtype', 'disposition', 'name', 'encoding'] as $name) {
if (null !== $this->_parent[$name] && !\is_string($this->_parent[$name])) {
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
$r = new \ReflectionProperty(TextPart::class, $name);
$r->setAccessible(true);
$r->setValue($this, $this->_parent[$name]);

View File

@ -35,6 +35,16 @@ class UnixPipes extends AbstractPipes
parent::__construct($input);
}
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->close();

View File

@ -88,6 +88,16 @@ class WindowsPipes extends AbstractPipes
parent::__construct($input);
}
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->close();

View File

@ -195,6 +195,16 @@ class Process implements \IteratorAggregate
return $process;
}
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
if ($this->options['create_new_console'] ?? false) {

View File

@ -104,6 +104,10 @@ final class TokenBucket implements LimiterStateInterface
*/
public function __wakeup(): void
{
if (!\is_string($this->stringRate)) {
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
$this->rate = Rate::fromString($this->stringRate);
unset($this->stringRate);
}

View File

@ -38,6 +38,16 @@ class CollectionConfigurator
$this->parentPrefixes = $parentPrefixes;
}
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
if (null === $this->prefixes) {

View File

@ -30,6 +30,16 @@ class ImportConfigurator
$this->route = $route;
}
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->parent->addCollection($this->route);

View File

@ -185,18 +185,6 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent
if (null !== $this->logger) {
$this->logger->info('Authenticator successful!', ['token' => $authenticatedToken, 'authenticator' => \get_class($authenticator)]);
}
// success! (sets the token on the token storage, etc)
$response = $this->handleAuthenticationSuccess($authenticatedToken, $passport, $request, $authenticator);
if ($response instanceof Response) {
return $response;
}
if (null !== $this->logger) {
$this->logger->debug('Authenticator set no success response: request continues.', ['authenticator' => \get_class($authenticator)]);
}
return null;
} catch (AuthenticationException $e) {
// oh no! Authentication failed!
$response = $this->handleAuthenticationFailure($e, $request, $authenticator, $passport);
@ -206,6 +194,18 @@ class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthent
return null;
}
// success! (sets the token on the token storage, etc)
$response = $this->handleAuthenticationSuccess($authenticatedToken, $passport, $request, $authenticator);
if ($response instanceof Response) {
return $response;
}
if (null !== $this->logger) {
$this->logger->debug('Authenticator set no success response: request continues.', ['authenticator' => \get_class($authenticator)]);
}
return null;
}
private function handleAuthenticationSuccess(TokenInterface $authenticatedToken, PassportInterface $passport, Request $request, AuthenticatorInterface $authenticator): ?Response

View File

@ -12,11 +12,12 @@
namespace Symfony\Component\Security\Http\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PreAuthenticatedUserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
@ -43,21 +44,21 @@ class UserCheckerListener implements EventSubscriberInterface
$this->userChecker->checkPreAuth($passport->getUser());
}
public function postCheckCredentials(LoginSuccessEvent $event): void
public function postCheckCredentials(AuthenticationSuccessEvent $event): void
{
$passport = $event->getPassport();
if (!$passport instanceof UserPassportInterface || null === $passport->getUser()) {
$user = $event->getAuthenticationToken()->getUser();
if (!$user instanceof UserInterface) {
return;
}
$this->userChecker->checkPostAuth($passport->getUser());
$this->userChecker->checkPostAuth($user);
}
public static function getSubscribedEvents(): array
{
return [
CheckPassportEvent::class => ['preCheckCredentials', 256],
LoginSuccessEvent::class => ['postCheckCredentials', 256],
AuthenticationSuccessEvent::class => ['postCheckCredentials', 256],
];
}
}

View File

@ -12,8 +12,8 @@
namespace Symfony\Component\Security\Http\Tests\EventListener;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
@ -21,8 +21,8 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PreAuthenticate
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
use Symfony\Component\Security\Http\EventListener\UserCheckerListener;
class UserCheckerListenerTest extends TestCase
@ -63,14 +63,14 @@ class UserCheckerListenerTest extends TestCase
{
$this->userChecker->expects($this->once())->method('checkPostAuth')->with($this->user);
$this->listener->postCheckCredentials($this->createLoginSuccessEvent());
$this->listener->postCheckCredentials(new AuthenticationSuccessEvent(new PostAuthenticationToken($this->user, 'main', [])));
}
public function testPostAuthNoUser()
{
$this->userChecker->expects($this->never())->method('checkPostAuth');
$this->listener->postCheckCredentials($this->createLoginSuccessEvent($this->createMock(PassportInterface::class)));
$this->listener->postCheckCredentials(new AuthenticationSuccessEvent(new PreAuthenticatedToken('nobody', null, 'main')));
}
private function createCheckPassportEvent($passport = null)
@ -82,12 +82,8 @@ class UserCheckerListenerTest extends TestCase
return new CheckPassportEvent($this->createMock(AuthenticatorInterface::class), $passport);
}
private function createLoginSuccessEvent($passport = null)
private function createAuthenticationSuccessEvent()
{
if (null === $passport) {
$passport = new SelfValidatingPassport(new UserBadge('test', function () { return $this->user; }));
}
return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $passport, $this->createMock(TokenInterface::class), new Request(), null, 'main');
return new AuthenticationSuccessEvent(new PostAuthenticationToken($this->user, 'main', []));
}
}

View File

@ -359,6 +359,10 @@ class UnicodeString extends AbstractUnicodeString
public function __wakeup()
{
if (!\is_string($this->string)) {
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
normalizer_is_normalized($this->string) ?: $this->string = normalizer_normalize($this->string);
}

View File

@ -203,7 +203,7 @@ class Parser
array_pop($this->refsBeingParsed);
}
} elseif (
self::preg_match('#^(?P<key>(?:![^\s]++\s++)?(?:'.Inline::REGEX_QUOTED_STRING.'|(?:!?!php/const:)?[^ \'"\[\{!].*?)) *\:(\s++(?P<value>.+))?$#u', rtrim($this->currentLine), $values)
self::preg_match('#^(?P<key>(?:![^\s]++\s++)?(?:'.Inline::REGEX_QUOTED_STRING.'|(?:!?!php/const:)?[^ \'"\[\{!].*?)) *\:( ++(?P<value>.+))?$#u', rtrim($this->currentLine), $values)
&& (false === strpos($values['key'], ' #') || \in_array($values['key'][0], ['"', "'"]))
) {
if ($context && 'sequence' == $context) {

View File

@ -915,4 +915,21 @@ class InlineTest extends TestCase
[['!'], '! ["!"]'],
];
}
/**
* @dataProvider ideographicSpaceProvider
*/
public function testParseIdeographicSpace(string $yaml, string $expected)
{
$this->assertSame($expected, Inline::parse($yaml));
}
public function ideographicSpaceProvider(): array
{
return [
["\u{3000}", ' '],
["'\u{3000}'", ' '],
["'a b'", 'a b'],
];
}
}

View File

@ -2732,6 +2732,22 @@ YAML;
// (before, there was no \n after row2)
$this->assertSame(['a' => ['b' => "row\nrow2\n"], 'c' => 'd'], $this->parser->parse($longDocument));
}
public function testParseIdeographicSpaces()
{
$expected = <<<YAML
unquoted: \u{3000}
quoted: '\u{3000}'
within_string: 'a b'
regular_space: 'a b'
YAML;
$this->assertSame([
'unquoted' => ' ',
'quoted' => ' ',
'within_string' => 'a b',
'regular_space' => 'a b',
], $this->parser->parse($expected));
}
}
class B

View File

@ -11,7 +11,7 @@ Design Principles
* contracts are split by domain, each into their own sub-namespaces;
* contracts are small and consistent sets of PHP interfaces, traits, normative
docblocks and reference test suites when applicable, ...;
docblocks and reference test suites when applicable;
* all contracts must have a proven implementation to enter this repository;
* they must be backward compatible with existing Symfony components.