Skip to content

Commit

Permalink
[BUGFIX] Consider eventual consistency due to concurrent requests
Browse files Browse the repository at this point in the history
It might happen, the that default reports has not been persisted
yet - thus, additional details cannot rely on their existence.

That's why persisting to database has been dropped for additional
details. Instead those details are logged with the DEBUG severity.
  • Loading branch information
ohader committed May 23, 2023
1 parent 24189ed commit 230113d
Show file tree
Hide file tree
Showing 7 changed files with 50 additions and 83 deletions.
74 changes: 22 additions & 52 deletions Classes/ContentSecurityPolicyDetailsReporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,20 @@
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Core\Http\NullResponse;
use TYPO3\CMS\Core\Middleware\AbstractContentSecurityPolicyReporter;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\PolicyProvider;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\Report;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\ReportRepository;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\ReportDemand;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class ContentSecurityPolicyDetailsReporter extends AbstractContentSecurityPolicyReporter
class ContentSecurityPolicyDetailsReporter extends AbstractContentSecurityPolicyReporter implements LoggerAwareInterface
{
use LoggerAwareTrait;
use ExtensionConfigurationTrait;

// @todo `AbstractContentSecurityPolicyReporter` should become a trait in the TYPO3 core
Expand Down Expand Up @@ -60,63 +59,34 @@ protected function persistCspReport(Scope $scope, ServerRequestInterface $reques
$demand->scope = $scope;
$existingReports = $this->reportRepository->findAllSummarized($demand);

$uuid = $existingReports[0]?->uuid;
if ($uuid === null) {
return;
}
// note: it might happen, that the default report was not persisted yet (concurrent requests)
$existingReport = $existingReports[0] ?? null;

$cspDetails = array_filter(
$data,
static fn (string $key) => $key === 'document' || $key === 'navigator',
ARRAY_FILTER_USE_KEY
);

switch ($this->getPersistence($this->extensionConfiguration)) {
case 'database':
$this->persistToDatabase($existingReports[0], $cspDetails);
break;
case 'file':
default:
$this->persistToFileSystem($existingReports[0], $cspDetails);
}
}

protected function persistToFileSystem(Report $existingReport, array $cspDetails): void
{
$path = Environment::getProjectPath() . '/var/log/csp-details/';
if (!file_exists($path)) {
GeneralUtility::mkdir_deep($path);
}
$filePath = $path . $existingReport->uuid . '.md';
file_put_contents($filePath, implode("\n", [
'## UUID',
'`' . $existingReport->uuid . '`',
'',
'## Meta',
'`' . json_encode($existingReport->meta, JSON_UNESCAPED_SLASHES) . '`',
'',
'## Report',
'`' . json_encode($existingReport->details, JSON_UNESCAPED_SLASHES) . '`',
'',
'## Navigator',
'`' . json_encode($cspDetails['navigator'] ?? null, JSON_UNESCAPED_SLASHES) . '`',
'',
'## Document',
'```',
($cspDetails['document']['html'] ?? ''),
'```',
]));
$this->logger->debug(
sprintf(
"Document:\n%s",
$this->indent(($cspDetails['document']['html'] ?? '') . "\n")
),
[
'summary' => $summary,
'navigator' => $cspDetails['navigator'] ?? null,
'uuid' => $existingReport?->uuid,
'meta' => $existingReport?->meta,
'report' => $existingReport?->details,
]
);
}

protected function persistToDatabase(Report $existingReport, array $cspDetails): void
protected function indent(string $value, string $indention = "\t"): string
{
$connection = GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable('sys_http_report');
$connection->update(
'sys_http_report',
['csp_details' => json_encode($cspDetails)],
['uuid' => (string)$existingReport->uuid]
);
$lines = preg_split('#\v#', $value);
return implode("\n", array_map(static fn (string $line) => $indention . $line, $lines));
}

protected function isCspReport(Scope $scope, ServerRequestInterface $request): bool
Expand Down
5 changes: 0 additions & 5 deletions Classes/ExtensionConfigurationTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,4 @@ public function isEnabled(ExtensionConfiguration $extensionConfiguration): bool
{
return $extensionConfiguration->get('csp_details', 'enabled');
}

public function getPersistence(ExtensionConfiguration $extensionConfiguration): string
{
return $extensionConfiguration->get('csp_details', 'persistence') ?: 'file';
}
}
19 changes: 6 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,12 @@ take care of rotating/compressing these files or just have it enabled only while

## Configuration

ℹ️ This package needs to be enabled explicity!
ℹ️ This package needs to be enabled explicity via Admin Tools, Extension Settings!

Extension Configurations settings (via Admin Tools, Extension Settings)
can be used to adjust the behavior of this package, as shown below:
Once enabled, details are logged to `var/log/typo3_csp-details_*.log`.

⚠️ Logged details might contain tokens or other private details of the current user sessions.
Please make sure to frequently clean up these files, and just enable the debug functionality of
this package just for a very limited time.

```patch
+ 'csp_details' => [
+ 'enabled' => '0',
+ 'persistence' => 'file',
+ ],
```

* `enabled (bool, default '0')` : Whether to capture additional CSP details at all
* `persistence ('file'|'db', default 'file')`: Where to persist the details
+ `file`: create markdown files at `var/log/csp-details/{uuid}.md`
+ `db`: store to additional database field `sys_http_report.csp_details`
14 changes: 9 additions & 5 deletions Resources/Public/JavaScript/violation-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,15 @@ class CspDetailsViolationHandler {
webdriver: navigator.webdriver,
}
};
fetch(reportUriMatches.groups.reportUri, {
method: 'POST',
headers: { 'Content-Type': 'application/csp-report+csp-details' },
body: JSON.stringify(details),
});
// defer additional requests a bit, to have regular report entity persisted
setTimeout(() => {
fetch(reportUriMatches.groups.reportUri, {
method: 'POST',
cache: 'no-cache',
headers: { 'Content-Type': 'application/csp-report+csp-details' },
body: JSON.stringify(details),
});
}, 500)
});
}

Expand Down
2 changes: 0 additions & 2 deletions ext_conf_template.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
# cat=basic; type=boolean; label=Whether resolving details is enabled
enabled=0
# cat=basic; type=options[filesystem var/log/csp-details/{uuid}.md=file,database field sys_http_report.csp_details=db]; label=Where to persist details
persistence=file
13 changes: 13 additions & 0 deletions ext_localconf.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,18 @@
defined('TYPO3') or die('Access denied.');

use H4ck3r31\CspDetails\PageRendererHook;
use Psr\Log\LogLevel;
use TYPO3\CMS\Core\Log\Writer\FileWriter;

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_pagerenderer.php']['render-preProcess']['csp-details'] = PageRendererHook::class . '->importModule';

// add default logger in case it was not defined yet individually
if (empty($GLOBALS['TYPO3_CONF_VARS']['LOG']['H4ck3r31']['CspDetails'])) {
$GLOBALS['TYPO3_CONF_VARS']['LOG']['H4ck3r31']['CspDetails']['writerConfiguration'] = [
LogLevel::DEBUG => [
FileWriter::class => [
'logFileInfix' => 'csp-details',
],
]
];
}
6 changes: 0 additions & 6 deletions ext_tables.sql

This file was deleted.

0 comments on commit 230113d

Please sign in to comment.