Skip to content

Commit

Permalink
bug #52469 Check whether secrets are empty and mark them all as sensi…
Browse files Browse the repository at this point in the history
…tive (nicolas-grekas)

This PR was merged into the 6.4 branch.

Discussion
----------

Check whether secrets are empty and mark them all as sensitive

| Q             | A
| ------------- | ---
| Branch?       | 6.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Issues        | -
| License       | MIT

Some security-hardening.

I made this throw a deprecation (as was already the case in some places) but I'd be fine making them throw an exception right away since an empty secret is a bold mistake. WDYT? (Webhook was experimental in 6.3 so it's fine throwing there I think.)

Commits
-------

52ca699 Check whether secrets are empty and mark them all as sensitive
  • Loading branch information
nicolas-grekas committed Nov 7, 2023
2 parents f0fcc9f + 52ca699 commit cf5510d
Show file tree
Hide file tree
Showing 21 changed files with 74 additions and 34 deletions.
7 changes: 4 additions & 3 deletions src/Symfony/Component/HttpFoundation/UriSigner.php
Expand Up @@ -12,8 +12,6 @@
namespace Symfony\Component\HttpFoundation;

/**
* Signs URIs.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class UriSigner
Expand All @@ -22,11 +20,14 @@ class UriSigner
private string $parameter;

/**
* @param string $secret A secret
* @param string $parameter Query string parameter to use
*/
public function __construct(#[\SensitiveParameter] string $secret, string $parameter = '_hash')
{
if (!$secret) {
throw new \InvalidArgumentException('A non-empty secret is required.');
}

$this->secret = $secret;
$this->parameter = $parameter;
}
Expand Down
Expand Up @@ -41,7 +41,7 @@ protected function getRequestMatcher(): RequestMatcherInterface
]);
}

protected function doParse(Request $request, string $secret): ?AbstractMailerEvent
protected function doParse(Request $request, #[\SensitiveParameter] string $secret): ?AbstractMailerEvent
{
$content = $request->toArray();
if (
Expand Down
Expand Up @@ -17,6 +17,7 @@
use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\Mailer\Bridge\Mailgun\RemoteEvent\MailgunPayloadConverter;
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
use Symfony\Component\RemoteEvent\Event\Mailer\AbstractMailerEvent;
use Symfony\Component\RemoteEvent\Exception\ParseException;
use Symfony\Component\Webhook\Client\AbstractRequestParser;
Expand All @@ -37,8 +38,12 @@ protected function getRequestMatcher(): RequestMatcherInterface
]);
}

protected function doParse(Request $request, string $secret): ?AbstractMailerEvent
protected function doParse(Request $request, #[\SensitiveParameter] string $secret): ?AbstractMailerEvent
{
if (!$secret) {
throw new InvalidArgumentException('A non-empty secret is required.');
}

$content = $request->toArray();
if (
!isset($content['signature']['timestamp'])
Expand All @@ -60,7 +65,7 @@ protected function doParse(Request $request, string $secret): ?AbstractMailerEve
}
}

private function validateSignature(array $signature, string $secret): void
private function validateSignature(array $signature, #[\SensitiveParameter] string $secret): void
{
// see https://documentation.mailgun.com/en/latest/user_manual.html#webhooks-1
if (!hash_equals($signature['signature'], hash_hmac('sha256', $signature['timestamp'].$signature['token'], $secret))) {
Expand Down
Expand Up @@ -37,7 +37,7 @@ protected function getRequestMatcher(): RequestMatcherInterface
]);
}

protected function doParse(Request $request, string $secret): ?AbstractMailerEvent
protected function doParse(Request $request, #[\SensitiveParameter] string $secret): ?AbstractMailerEvent
{
try {
return $this->converter->convert($request->toArray());
Expand Down
Expand Up @@ -41,7 +41,7 @@ protected function getRequestMatcher(): RequestMatcherInterface
]);
}

protected function doParse(Request $request, string $secret): ?AbstractMailerEvent
protected function doParse(Request $request, #[\SensitiveParameter] string $secret): ?AbstractMailerEvent
{
$payload = $request->toArray();
if (
Expand Down
Expand Up @@ -17,6 +17,7 @@
use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\Mailer\Bridge\Sendgrid\RemoteEvent\SendgridPayloadConverter;
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
use Symfony\Component\RemoteEvent\Event\Mailer\AbstractMailerEvent;
use Symfony\Component\RemoteEvent\Exception\ParseException;
use Symfony\Component\Webhook\Client\AbstractRequestParser;
Expand Down Expand Up @@ -86,12 +87,12 @@ protected function doParse(Request $request, string $secret): ?AbstractMailerEve
*
* @see https://docs.sendgrid.com/for-developers/tracking-events/getting-started-event-webhook-security-features
*/
private function validateSignature(
string $signature,
string $timestamp,
string $payload,
string $secret,
): void {
private function validateSignature(string $signature, string $timestamp, string $payload, #[\SensitiveParameter] string $secret): void
{
if (!$secret) {
throw new InvalidArgumentException('A non-empty secret is required.');
}

$timestampedPayload = $timestamp.$payload;

// Sendgrid provides the verification key as base64-encoded DER data. Openssl wants a PEM format, which is a multiline version of the base64 data.
Expand Down
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\Mailer\Transport\Smtp\Auth;

use Symfony\Component\Mailer\Exception\InvalidArgumentException;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;

/**
Expand Down Expand Up @@ -41,6 +42,10 @@ public function authenticate(EsmtpTransport $client): void
*/
private function getResponse(#[\SensitiveParameter] string $secret, string $challenge): string
{
if (!$secret) {
throw new InvalidArgumentException('A non-empty secret is required.');
}

if (\strlen($secret) > 64) {
$secret = pack('H32', md5($secret));
}
Expand Down
Expand Up @@ -25,7 +25,7 @@ protected function getRequestMatcher(): RequestMatcherInterface
return new MethodRequestMatcher('POST');
}

protected function doParse(Request $request, string $secret): ?SmsEvent
protected function doParse(Request $request, #[\SensitiveParameter] string $secret): ?SmsEvent
{
// Statuses: https://www.twilio.com/docs/sms/api/message-resource#message-status-values
// Payload examples: https://www.twilio.com/docs/sms/outbound-message-logging
Expand Down
Expand Up @@ -16,6 +16,7 @@
use Symfony\Component\HttpFoundation\RequestMatcher\IsJsonRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\Notifier\Exception\InvalidArgumentException;
use Symfony\Component\RemoteEvent\Event\Sms\SmsEvent;
use Symfony\Component\Webhook\Client\AbstractRequestParser;
use Symfony\Component\Webhook\Exception\RejectWebhookException;
Expand All @@ -30,8 +31,12 @@ protected function getRequestMatcher(): RequestMatcherInterface
]);
}

protected function doParse(Request $request, string $secret): ?SmsEvent
protected function doParse(Request $request, #[\SensitiveParameter] string $secret): ?SmsEvent
{
if (!$secret) {
throw new InvalidArgumentException('A non-empty secret is required.');
}

// Signed webhooks: https://developer.vonage.com/en/getting-started/concepts/webhooks#validating-signed-webhooks
if (!$request->headers->has('Authorization')) {
throw new RejectWebhookException(406, 'Missing "Authorization" header.');
Expand Down Expand Up @@ -70,7 +75,7 @@ protected function doParse(Request $request, string $secret): ?SmsEvent
return $event;
}

private function validateSignature(string $jwt, string $secret): void
private function validateSignature(string $jwt, #[\SensitiveParameter] string $secret): void
{
$tokenParts = explode('.', $jwt);
if (3 !== \count($tokenParts)) {
Expand Down
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\Security\Core\Authentication\Token;

use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
use Symfony\Component\Security\Core\User\UserInterface;

/**
Expand All @@ -32,12 +33,12 @@ public function __construct(UserInterface $user, string $firewallName, #[\Sensit
{
parent::__construct($user->getRoles());

if (empty($secret)) {
throw new \InvalidArgumentException('$secret must not be empty.');
if (!$secret) {
throw new InvalidArgumentException('A non-empty secret is required.');
}

if ('' === $firewallName) {
throw new \InvalidArgumentException('$firewallName must not be empty.');
if (!$firewallName) {
throw new InvalidArgumentException('$firewallName must not be empty.');
}

$this->firewallName = $firewallName;
Expand Down
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\Security\Core\Signature;

use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
use Symfony\Component\Security\Core\Signature\Exception\ExpiredSignatureException;
use Symfony\Component\Security\Core\Signature\Exception\InvalidSignatureException;
use Symfony\Component\Security\Core\User\UserInterface;
Expand All @@ -37,6 +38,10 @@ class SignatureHasher
*/
public function __construct(PropertyAccessorInterface $propertyAccessor, array $signatureProperties, #[\SensitiveParameter] string $secret, ExpiredSignatureStorage $expiredSignaturesStorage = null, int $maxUses = null)
{
if (!$secret) {
throw new InvalidArgumentException('A non-empty secret is required.');
}

$this->propertyAccessor = $propertyAccessor;
$this->signatureProperties = $signatureProperties;
$this->secret = $secret;
Expand Down
Expand Up @@ -19,6 +19,7 @@
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CookieTheftException;
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
Expand Down Expand Up @@ -51,6 +52,10 @@ class RememberMeAuthenticator implements InteractiveAuthenticatorInterface

public function __construct(RememberMeHandlerInterface $rememberMeHandler, #[\SensitiveParameter] string $secret, TokenStorageInterface $tokenStorage, string $cookieName, LoggerInterface $logger = null)
{
if (!$secret) {
throw new InvalidArgumentException('A non-empty secret is required.');
}

$this->rememberMeHandler = $rememberMeHandler;
$this->secret = $secret;
$this->tokenStorage = $tokenStorage;
Expand Down
Expand Up @@ -14,6 +14,7 @@
use Symfony\Component\HttpFoundation\RateLimiter\AbstractRequestRateLimiter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\RateLimiter\RateLimiterFactory;
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
use Symfony\Component\Security\Http\SecurityRequestAttributes;

/**
Expand All @@ -35,10 +36,10 @@ final class DefaultLoginRateLimiter extends AbstractRequestRateLimiter
*/
public function __construct(RateLimiterFactory $globalFactory, RateLimiterFactory $localFactory, #[\SensitiveParameter] string $secret = '')
{
if ('' === $secret) {
trigger_deprecation('symfony/security-http', '6.4', 'Calling "%s()" with an empty secret is deprecated. A non-empty secret will be mandatory in version 7.0.', __METHOD__);
// throw new \Symfony\Component\Security\Core\Exception\InvalidArgumentException('A non-empty secret is required.');
if (!$secret) {
throw new InvalidArgumentException('A non-empty secret is required.');
}

$this->globalFactory = $globalFactory;
$this->localFactory = $localFactory;
$this->secret = $secret;
Expand Down
Expand Up @@ -22,7 +22,7 @@
*/
abstract class AbstractRequestParser implements RequestParserInterface
{
public function parse(Request $request, string $secret): ?RemoteEvent
public function parse(Request $request, #[\SensitiveParameter] string $secret): ?RemoteEvent
{
$this->validate($request);

Expand All @@ -41,7 +41,7 @@ public function createRejectedResponse(string $reason): Response

abstract protected function getRequestMatcher(): RequestMatcherInterface;

abstract protected function doParse(Request $request, string $secret): ?RemoteEvent;
abstract protected function doParse(Request $request, #[\SensitiveParameter] string $secret): ?RemoteEvent;

protected function validate(Request $request): void
{
Expand Down
4 changes: 2 additions & 2 deletions src/Symfony/Component/Webhook/Client/RequestParser.php
Expand Up @@ -41,7 +41,7 @@ protected function getRequestMatcher(): RequestMatcherInterface
]);
}

protected function doParse(Request $request, string $secret): RemoteEvent
protected function doParse(Request $request, #[\SensitiveParameter] string $secret): RemoteEvent
{
$body = $request->toArray();

Expand All @@ -60,7 +60,7 @@ protected function doParse(Request $request, string $secret): RemoteEvent
);
}

private function validateSignature(HeaderBag $headers, string $body, $secret): void
private function validateSignature(HeaderBag $headers, string $body, #[\SensitiveParameter] string $secret): void
{
$signature = $headers->get($this->signatureHeaderName);
$event = $headers->get($this->eventHeaderName);
Expand Down
Expand Up @@ -28,7 +28,7 @@ interface RequestParserInterface
*
* @throws RejectWebhookException When the payload is rejected (signature issue, parse issue, ...)
*/
public function parse(Request $request, string $secret): ?RemoteEvent;
public function parse(Request $request, #[\SensitiveParameter] string $secret): ?RemoteEvent;

public function createSuccessfulResponse(): Response;

Expand Down
Expand Up @@ -13,6 +13,7 @@

use Symfony\Component\HttpClient\HttpOptions;
use Symfony\Component\RemoteEvent\RemoteEvent;
use Symfony\Component\Webhook\Exception\InvalidArgumentException;
use Symfony\Component\Webhook\Exception\LogicException;

/**
Expand All @@ -26,8 +27,12 @@ public function __construct(
) {
}

public function configure(RemoteEvent $event, string $secret, HttpOptions $options): void
public function configure(RemoteEvent $event, #[\SensitiveParameter] string $secret, HttpOptions $options): void
{
if (!$secret) {
throw new InvalidArgumentException('A non-empty secret is required.');
}

$opts = $options->toArray();
$headers = $opts['headers'];
if (!isset($opts['body'])) {
Expand Down
Expand Up @@ -25,7 +25,7 @@ public function __construct(
) {
}

public function configure(RemoteEvent $event, string $secret, HttpOptions $options): void
public function configure(RemoteEvent $event, #[\SensitiveParameter] string $secret, HttpOptions $options): void
{
$options->setHeaders([
$this->eventHeaderName => $event->getName(),
Expand Down
Expand Up @@ -25,7 +25,7 @@ public function __construct(
) {
}

public function configure(RemoteEvent $event, string $secret, HttpOptions $options): void
public function configure(RemoteEvent $event, #[\SensitiveParameter] string $secret, HttpOptions $options): void
{
$body = $this->serializer->serialize($event->getPayload(), 'json');
$options->setBody($body);
Expand Down
Expand Up @@ -19,5 +19,5 @@
*/
interface RequestConfiguratorInterface
{
public function configure(RemoteEvent $event, string $secret, HttpOptions $options): void;
public function configure(RemoteEvent $event, #[\SensitiveParameter] string $secret, HttpOptions $options): void;
}
8 changes: 7 additions & 1 deletion src/Symfony/Component/Webhook/Subscriber.php
Expand Up @@ -11,12 +11,18 @@

namespace Symfony\Component\Webhook;

use Symfony\Component\Webhook\Exception\InvalidArgumentException;

class Subscriber
{
public function __construct(
private readonly string $url,
#[\SensitiveParameter] private readonly string $secret,
#[\SensitiveParameter]
private readonly string $secret,
) {
if (!$secret) {
throw new InvalidArgumentException('A non-empty secret is required.');
}
}

public function getUrl(): string
Expand Down

0 comments on commit cf5510d

Please sign in to comment.