Created
October 8, 2019 05:39
-
-
Save maks-rafalko/ac9ccb36a68008be1e61b18e4d2ff9cc to your computer and use it in GitHub Desktop.
Symfony: do not log secrets (custom Monolog Formatter) to avoid secrets disclosure
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace Core\Monolog\Formatter; | |
use Doctrine\DBAL\Connection; | |
use Monolog\Formatter\LineFormatter; | |
/** | |
* This Monolog Formatter replaces secrets with '*****' characters to avoid secrets disclosure | |
*/ | |
final class MaskSecretsLineFormatter extends LineFormatter | |
{ | |
// ENV variables you want to hide in the logs | |
// DB username & pass will be added automatically below | |
public const SECRETS_TO_MASK = [ | |
'APP_SECRET', | |
'JWT_PASSPHRASE', | |
'JWT_API_PASS_HASH', | |
'AWS_S3_SECRET', | |
'AWS_S3_KEY', | |
]; | |
/** | |
* @var Connection | |
*/ | |
private $connection; | |
/** | |
* {@inheritdoc} | |
*/ | |
public function format(array $record) | |
{ | |
$logLine = parent::format($record); | |
$secretValues = []; | |
foreach (self::SECRETS_TO_MASK as $secretName) { | |
/** @var string|bool $secretValue */ | |
$secretValue = getenv($secretName); | |
if ($secretValue !== false) { | |
$secretValues[] = $secretValue; | |
} | |
} | |
$secretValues[] = $this->connection->getUsername(); | |
$secretValues[] = $this->connection->getPassword(); | |
$logLine = str_replace($secretValues, '*****', $logLine); | |
return $logLine; | |
} | |
public function setConnection(Connection $connection): void | |
{ | |
$this->connection = $connection; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace Core\Tests\Unit\Monolog\Formatter; | |
use Core\Monolog\Formatter\MaskSecretsLineFormatter; | |
use Doctrine\DBAL\Connection; | |
use PHPUnit\Framework\MockObject\MockObject; | |
use PHPUnit\Framework\TestCase; | |
final class MaskSecretsLineFormatterTest extends TestCase | |
{ | |
/** | |
* @var array | |
*/ | |
private static $env; | |
/** | |
* Saves the current environment | |
*/ | |
public static function setUpBeforeClass(): void | |
{ | |
self::$env = []; | |
foreach (MaskSecretsLineFormatter::SECRETS_TO_MASK as $name) { | |
self::$env[$name] = getenv($name); | |
} | |
} | |
public static function tearDownAfterClass(): void | |
{ | |
self::restorePathEnvironment(); | |
} | |
protected function setUp(): void | |
{ | |
self::restorePathEnvironment(); | |
} | |
public function test_it_replaces_secrets_with_stars(): void | |
{ | |
/** @var Connection|MockObject $connectionMock */ | |
$connectionMock = $this->createMock(Connection::class); | |
$connectionMock->expects(self::once())->method('getUsername')->willReturn('db_username'); | |
$connectionMock->expects(self::once())->method('getPassword')->willReturn('db_pass'); | |
putenv(sprintf('%s=%s', 'AWS_S3_KEY', 'aws_s3_key_secret')); | |
$formatter = new MaskSecretsLineFormatter(); | |
$formatter->setConnection($connectionMock); | |
$record = [ | |
'level_name' => 'ERROR', | |
'channel' => 'custom_channel', | |
'message' => 'DB secrets: db_username, db_pass. AWS S3 secret: aws_s3_key_secret', | |
'datetime' => '2019-10-07 12:56:19', | |
'extra' => [], | |
'context' => [ | |
'foo' => 'bar', | |
], | |
]; | |
$formattedString = $formatter->format($record); | |
self::assertContains('[2019-10-07 12:56:19] custom_channel.ERROR: DB secrets: *****, *****. AWS S3 secret: ***** {"foo":"bar"} []', $formattedString); | |
} | |
private static function restorePathEnvironment(): void | |
{ | |
foreach (self::$env as $name => $value) { | |
if (false !== $value) { | |
putenv($name . '=' . $value); | |
} else { | |
putenv($name); | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
monolog: | |
handlers: | |
main: | |
type: fingers_crossed | |
action_level: error | |
handler: nested | |
excluded_404s: | |
# regex: exclude all 404 errors from the logs | |
- ^/ | |
nested: | |
type: stream | |
path: "php://stderr" | |
include_stacktraces: true | |
level: error | |
+ formatter: 'Core\Monolog\Formatter\MaskSecretsLineFormatter' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
+ Core\Monolog\Formatter\MaskSecretsLineFormatter: | |
+ calls: | |
+ - [setConnection, ['@Doctrine\DBAL\Connection']] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment