Merge branch '4.4'

* 4.4: (23 commits)
  [HttpFoundation] fix docblock
  [HttpKernel] Flatten "exception" controller argument if not typed
  Fix MySQL column type definition.
  Link the right file depending on the new version
  [Cache] Redis Tag Aware warn on wrong eviction policy
  [HttpClient] fix HttpClientDataCollector
  [HttpKernel] collect bundle classes, not paths
  [Config] fix id-generation for GlobResource
  [HttpKernel] dont check cache freshness more than once per process
  [Finder] Allow ssh2 stream wrapper for sftp
  [FrameworkBundle] fix wiring of httplug client
  add FrameworkBundle requirement
  [SecurityBundle] add tests with empty authenticator
  [Security] always check the token on non-lazy firewalls
  [DI] Use reproducible entropy to generate env placeholders
  [WebProfilerBundle] Require symfony/twig-bundle
  [Mailer] Add UPGRADE entry about the null transport DSN
  bumped Symfony version to 4.3.9
  updated VERSION for 4.3.8
  updated CHANGELOG for 4.3.8
  ...
This commit is contained in:
Nicolas Grekas 2019-11-16 16:24:47 +01:00
commit df63cc59f1
25 changed files with 230 additions and 29 deletions

View File

@ -7,6 +7,16 @@ in 4.3 minor versions.
To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash
To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v4.3.0...v4.3.1
* 4.3.8 (2019-11-13)
* bug #34344 [Console] Constant STDOUT might be undefined (nicolas-grekas)
* security #cve-2019-18886 [Security\Core] throw AccessDeniedException when switch user fails (nicolas-grekas)
* security #cve-2019-18888 [Mime] fix guessing mime-types of files with leading dash (nicolas-grekas)
* security #cve-2019-11325 [VarExporter] fix exporting some strings (nicolas-grekas)
* security #cve-2019-18889 [Cache] forbid serializing AbstractAdapter and TagAwareAdapter instances (nicolas-grekas)
* security #cve-2019-18888 [HttpFoundation] fix guessing mime-types of files with leading dash (nicolas-grekas)
* security #cve-2019-18887 [HttpKernel] Use constant time comparison in UriSigner (stof)
* 4.3.7 (2019-11-11)
* bug #34294 [Workflow] Fix error when we use ValueObject for the marking property (FabienSalles)

View File

@ -26,8 +26,8 @@ file and directory structure of your application:
Then, upgrade the contents of your console script and your front controller:
* `bin/console`: https://github.com/symfony/recipes/blob/master/symfony/console/3.3/bin/console
* `public/index.php`: https://github.com/symfony/recipes/blob/master/symfony/framework-bundle/3.3/public/index.php
* `bin/console`: https://github.com/symfony/recipes/blob/master/symfony/console/4.4/bin/console
* `public/index.php`: https://github.com/symfony/recipes/blob/master/symfony/framework-bundle/4.4/public/index.php
Lastly, read the following article to add Symfony Flex to your application and
upgrade the configuration files: https://symfony.com/doc/current/setup/flex.html

View File

@ -164,6 +164,11 @@ Lock
* Deprecated services `lock.store.flock`, `lock.store.semaphore`, `lock.store.memcached.abstract` and `lock.store.redis.abstract`,
use `StoreFactory::createStore` instead.
Mailer
------
* [BC BREAK] Changed the DSN to use for disabling delivery (using the `NullTransport`) from `smtp://null` to `null://null` (host doesn't matter).
Messenger
---------

View File

@ -26,8 +26,8 @@
<service id="Http\Client\HttpClient" class="Symfony\Component\HttpClient\HttplugClient">
<argument type="service" id="http_client" />
<argument type="service" id="Http\Message\ResponseFactory" on-invalid="ignore" />
<argument type="service" id="Http\Message\StreamFactory" on-invalid="ignore" />
<argument type="service" id="Psr\Http\Message\ResponseFactoryInterface" on-invalid="ignore" />
<argument type="service" id="Psr\Http\Message\StreamFactoryInterface" on-invalid="ignore" />
</service>
</services>
</container>

View File

@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\SecurityBundle\Tests\Functional;
class AnonymousTest extends AbstractWebTestCase
{
public function testAnonymous()
{
$client = $this->createClient(['test_case' => 'Anonymous', 'root_config' => 'config.yml']);
$client->request('GET', '/');
$this->assertSame(401, $client->getResponse()->getStatusCode());
}
}

View File

@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AnonymousBundle;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
class AppCustomAuthenticator extends AbstractGuardAuthenticator
{
public function supports(Request $request)
{
return false;
}
public function getCredentials(Request $request)
{
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
}
public function checkCredentials($credentials, UserInterface $user)
{
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
}
public function start(Request $request, AuthenticationException $authException = null)
{
return new Response($authException->getMessage(), Response::HTTP_UNAUTHORIZED);
}
public function supportsRememberMe()
{
}
}

View File

@ -0,0 +1,15 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return [
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
];

View File

@ -0,0 +1,24 @@
framework:
secret: test
router: { resource: "%kernel.project_dir%/%kernel.test_case%/routing.yml" }
validation: { enabled: true, enable_annotations: true }
csrf_protection: true
form: true
test: ~
default_locale: en
session:
storage_id: session.storage.mock_file
profiler: { only_exceptions: false }
services:
Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AnonymousBundle\AppCustomAuthenticator: ~
security:
firewalls:
secure:
pattern: ^/
anonymous: false
stateless: true
guard:
authenticators:
- Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AnonymousBundle\AppCustomAuthenticator

View File

@ -0,0 +1,5 @@
main:
path: /
defaults:
_controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction
path: /app

View File

@ -210,7 +210,7 @@
<thead>
<tr>
<th class="key">Name</th>
<th>Path</th>
<th>Class</th>
</tr>
</thead>
<tbody>

View File

@ -18,6 +18,7 @@
"require": {
"php": "^7.2.9",
"symfony/config": "^4.4|^5.0",
"symfony/framework-bundle": "^4.4|^5.0",
"symfony/http-kernel": "^4.4|^5.0",
"symfony/routing": "^4.4|^5.0",
"symfony/twig-bundle": "^4.4|^5.0",

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\Cache\Adapter;
use Predis\Connection\Aggregate\ClusterInterface;
use Predis\Connection\Aggregate\PredisCluster;
use Predis\Response\Status;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DeflateMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
@ -58,6 +59,11 @@ class RedisTagAwareAdapter extends AbstractTagAwareAdapter
*/
private const DEFAULT_CACHE_TTL = 8640000;
/**
* @var string|null detected eviction policy used on Redis server
*/
private $redisEvictionPolicy;
/**
* @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface $redisClient The redis client
* @param string $namespace The default namespace
@ -87,6 +93,13 @@ class RedisTagAwareAdapter extends AbstractTagAwareAdapter
*/
protected function doSave(array $values, int $lifetime, array $addTagData = [], array $delTagData = []): array
{
$eviction = $this->getRedisEvictionPolicy();
if ('noeviction' !== $eviction && 0 !== strpos($eviction, 'volatile-')) {
CacheItem::log($this->logger, sprintf('Redis maxmemory-policy setting "%s" is *not* supported by RedisTagAwareAdapter, use "noeviction" or "volatile-*" eviction policies', $eviction));
return false;
}
// serialize values
if (!$serialized = $this->marshaller->marshall($values, $failed)) {
return $failed;
@ -260,4 +273,20 @@ EOLUA;
return $newIds;
}
private function getRedisEvictionPolicy(): string
{
if (null !== $this->redisEvictionPolicy) {
return $this->redisEvictionPolicy;
}
foreach ($this->getHosts() as $host) {
$info = $host->info('Memory');
$info = isset($info['Memory']) ? $info['Memory'] : $info;
return $this->redisEvictionPolicy = $info['maxmemory_policy'];
}
return $this->redisEvictionPolicy = '';
}
}

View File

@ -41,6 +41,7 @@ class GlobResource implements \IteratorAggregate, SelfCheckingResourceInterface
*/
public function __construct(string $prefix, string $pattern, bool $recursive, bool $forExclusion = false, array $excludedPrefixes = [])
{
ksort($excludedPrefixes);
$this->prefix = realpath($prefix) ?: (file_exists($prefix) ? $prefix : false);
$this->pattern = $pattern;
$this->recursive = $recursive;
@ -62,7 +63,7 @@ class GlobResource implements \IteratorAggregate, SelfCheckingResourceInterface
*/
public function __toString(): string
{
return 'glob.'.$this->prefix.$this->pattern.(int) $this->recursive;
return 'glob.'.$this->prefix.(int) $this->recursive.$this->pattern.(int) $this->forExclusion.implode("\0", $this->excludedPrefixes);
}
/**

View File

@ -24,6 +24,8 @@ class EnvPlaceholderParameterBag extends ParameterBag
private $unusedEnvPlaceholders = [];
private $providedTypes = [];
private static $counter = 0;
/**
* {@inheritdoc}
*/
@ -49,7 +51,7 @@ class EnvPlaceholderParameterBag extends ParameterBag
throw new RuntimeException(sprintf('The default value of an env() parameter must be a string or null, but "%s" given to "%s".', \gettype($defaultValue), $name));
}
$uniqueName = md5($name.uniqid(mt_rand(), true));
$uniqueName = md5($name.'_'.self::$counter++);
$placeholder = sprintf('%s_%s_%s', $this->getEnvPlaceholderUniquePrefix(), str_replace(':', '_', $env), $uniqueName);
$this->envPlaceholders[$env][$placeholder] = $placeholder;
@ -64,7 +66,13 @@ class EnvPlaceholderParameterBag extends ParameterBag
*/
public function getEnvPlaceholderUniquePrefix(): string
{
return $this->envPlaceholderUniquePrefix ?? $this->envPlaceholderUniquePrefix = 'env_'.bin2hex(random_bytes(8));
if (null === $this->envPlaceholderUniquePrefix) {
$reproducibleEntropy = unserialize(serialize($this->parameters));
array_walk_recursive($reproducibleEntropy, function (&$v) { $v = null; });
$this->envPlaceholderUniquePrefix = 'env_'.substr(md5(serialize($reproducibleEntropy)), -16);
}
return $this->envPlaceholderUniquePrefix;
}
/**

View File

@ -784,7 +784,7 @@ class Finder implements \IteratorAggregate, \Countable
{
$dir = rtrim($dir, '/'.\DIRECTORY_SEPARATOR);
if (preg_match('#^s?ftp://#', $dir)) {
if (preg_match('#^(ssh2\.)?s?ftp://#', $dir)) {
$dir .= '/';
}

View File

@ -133,6 +133,7 @@ final class HttpClientDataCollector extends DataCollector
$debugInfo = array_diff_key($info, $baseInfo);
$info = array_diff_key($info, $debugInfo) + ['debug_info' => $debugInfo];
unset($traces[$i]['info']); // break PHP reference used by TraceableHttpClient
$traces[$i]['info'] = $this->cloneVar($info);
$traces[$i]['options'] = $this->cloneVar($trace['options']);
}

View File

@ -219,7 +219,7 @@ class PdoSessionHandler extends AbstractSessionHandler
// - trailing space removal
// - case-insensitivity
// - language processing like é == e
$sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol MEDIUMINT NOT NULL, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8_bin, ENGINE = InnoDB";
$sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED NOT NULL, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8_bin, ENGINE = InnoDB";
break;
case 'sqlite':
$sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)";

View File

@ -34,7 +34,7 @@ class RedisSessionHandler extends AbstractSessionHandler
* List of available options:
* * prefix: The prefix to use for the keys in order to avoid collision on the Redis server.
*
* @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy $redis
* @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis
*
* @throws \InvalidArgumentException When unsupported client or options are passed
*/

View File

@ -15,7 +15,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\VarDumper\Caster\LinkStub;
use Symfony\Component\VarDumper\Caster\ClassStub;
/**
* @author Fabien Potencier <fabien@symfony.com>
@ -28,12 +28,6 @@ class ConfigDataCollector extends DataCollector implements LateDataCollectorInte
* @var KernelInterface
*/
private $kernel;
private $hasVarDumper;
public function __construct()
{
$this->hasVarDumper = class_exists(LinkStub::class);
}
/**
* Sets the Kernel associated with this Request.
@ -67,7 +61,7 @@ class ConfigDataCollector extends DataCollector implements LateDataCollectorInte
if (isset($this->kernel)) {
foreach ($this->kernel->getBundles() as $name => $bundle) {
$this->data['bundles'][$name] = $this->hasVarDumper ? new LinkStub($bundle->getPath()) : $bundle->getPath();
$this->data['bundles'][$name] = new ClassStub(\get_class($bundle));
}
$this->data['symfony_state'] = $this->determineSymfonyState();

View File

@ -99,7 +99,7 @@ class ErrorListener implements EventSubscriberInterface
$r = new \ReflectionFunction(\Closure::fromCallable($event->getController()));
$r = $r->getParameters()[$k] ?? null;
if ($r && $r->hasType() && FlattenException::class === $r->getType()->getName()) {
if ($r && (!$r->hasType() || FlattenException::class === $r->getType()->getName())) {
$arguments = $event->getArguments();
$arguments[$k] = FlattenException::createFromThrowable($e);
$event->setArguments($arguments);

View File

@ -66,6 +66,8 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl
private $requestStackSize = 0;
private $resetServices = false;
private static $freshCache = [];
const VERSION = '5.0.0-DEV';
const VERSION_ID = 50000;
const MAJOR_VERSION = 5;
@ -430,7 +432,9 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl
$errorLevel = error_reporting(\E_ALL ^ \E_WARNING);
try {
if (file_exists($cachePath) && \is_object($this->container = include $cachePath) && (!$this->debug || $cache->isFresh())) {
if (file_exists($cachePath) && \is_object($this->container = include $cachePath)
&& (!$this->debug || (self::$freshCache[$k = $cachePath.'.'.$this->environment] ?? self::$freshCache[$k] = $cache->isFresh()))
) {
$this->container->set('kernel', $this);
error_reporting($errorLevel);

View File

@ -13,8 +13,8 @@ namespace Symfony\Component\HttpKernel\Tests\EventListener;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
@ -158,12 +158,11 @@ class ErrorListenerTest extends TestCase
$this->assertFalse($dispatcher->hasListeners(KernelEvents::RESPONSE), 'CSP removal listener has been removed');
}
public function testOnControllerArguments()
/**
* @dataProvider controllerProvider
*/
public function testOnControllerArguments(callable $controller)
{
$controller = function (FlattenException $exception) {
return new Response('OK: '.$exception->getMessage());
};
$listener = new ErrorListener($controller, $this->createMock(LoggerInterface::class), true);
$kernel = $this->createMock(HttpKernelInterface::class);
@ -181,6 +180,23 @@ class ErrorListenerTest extends TestCase
$this->assertSame('OK: foo', $event->getResponse()->getContent());
}
public function controllerProvider()
{
yield [function (FlattenException $exception) {
return new Response('OK: '.$exception->getMessage());
}];
yield [function ($exception) {
$this->assertInstanceOf(FlattenException::class, $exception);
return new Response('OK: '.$exception->getMessage());
}];
yield [function (\Throwable $exception) {
return new Response('OK: '.$exception->getMessage());
}];
}
}
class TestLogger extends Logger implements DebugLoggerInterface

View File

@ -4,6 +4,7 @@ CHANGELOG
4.4.0
-----
* [BC BREAK] changed the `NullTransport` DSN from `smtp://null` to `null://null`
* [BC BREAK] renamed `SmtpEnvelope` to `Envelope`, renamed `DelayedSmtpEnvelope` to
`DelayedEnvelope`
* [BC BREAK] changed the syntax for failover and roundrobin DSNs

View File

@ -18,6 +18,7 @@ use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
use Symfony\Component\Security\Http\AccessMapInterface;
use Symfony\Component\Security\Http\Event\LazyResponseEvent;
/**
* AccessListener enforces access control rules.
@ -49,6 +50,10 @@ class AccessListener
*/
public function __invoke(RequestEvent $event)
{
if (!$event instanceof LazyResponseEvent && null === $token = $this->tokenStorage->getToken()) {
throw new AuthenticationCredentialsNotFoundException('A Token was not found in the TokenStorage.');
}
$request = $event->getRequest();
list($attributes) = $this->map->getPatterns($request);
@ -57,7 +62,7 @@ class AccessListener
return;
}
if (null === $token = $this->tokenStorage->getToken()) {
if ($event instanceof LazyResponseEvent && null === $token = $this->tokenStorage->getToken()) {
throw new AuthenticationCredentialsNotFoundException('A Token was not found in the TokenStorage.');
}

View File

@ -18,6 +18,7 @@ use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterfac
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Http\AccessMapInterface;
use Symfony\Component\Security\Http\Event\LazyResponseEvent;
use Symfony\Component\Security\Http\Firewall\AccessListener;
class AccessListenerTest extends TestCase
@ -219,7 +220,7 @@ class AccessListenerTest extends TestCase
->willReturn($request)
;
$listener($event);
$listener(new LazyResponseEvent($event));
}
public function testHandleWhenTheSecurityTokenStorageHasNoToken()