getBody(); if (!$this->isJson($payload)) { return; } $normalizedParams = $request->getAttribute('normalizedParams'); $meta = [ 'addr' => IpAnonymizationUtility::anonymizeIp($normalizedParams->getRemoteAddress()), 'agent' => $normalizedParams->getHttpUserAgent(), ]; // skip potential externally injected violation reports $requestTime = $this->getRequestQueryParam($request, 'requestTime'); $requestHash = $this->getRequestQueryParam($request, 'requestHash'); if ($requestTime === null || $requestHash === null || !$this->hashService->validateHmac($requestTime, self::class, $requestHash) ) { return; } $originalDetails = json_decode($payload, true)['csp-report'] ?? []; $originalDetails = $this->anonymizeDetails($originalDetails); $details = new ReportDetails($originalDetails); $summary = $this->generateReportSummary($scope, $details); $report = new Report( $scope, ReportStatus::New, (int)$requestTime, $meta, $details, $summary ); $event = $this->eventDispatcher->dispatch( new BeforePersistingReportEvent($report, $request) ); if ($event->report !== null) { $this->reportRepository->add($event->report); } } protected function generateReportSummary(Scope $scope, ReportDetails $details): string { return $this->hashService->hmac( json_encode([ $scope, $details['effective-directive'] ?? null, $details['blocked-uri'] ?? null, $details['script-sample'] ?? null, ]), self::class, ); } protected function anonymizeDetails(array $details): array { foreach (self::URI_KEYS as $uriKey) { if (!isset($details[$uriKey])) { continue; } $details[$uriKey] = $this->anonymizeUri($details[$uriKey]); } return $details; } protected function anonymizeUri(string $value): string { try { $uri = Uri::fromAnyScheme($value); } catch (\Throwable) { return ''; } if ($uri->getQuery() === '') { return $value; } parse_str($uri->getQuery(), $query); // strip CSRF token (might be a different usage as well) unset($query['token']); return (string)$uri->withQuery(http_build_query($query, '', '&', PHP_QUERY_RFC3986)); } /** * Determines, whether current request URI starts with local reporting URI, * (e.g. `https://ip12.anyhost.it:8443/en/@http-reporting?csp=report`). */ protected function targetsCspReportUri(Scope $scope, ServerRequestInterface $request): bool { $normalizedParams = $request->getAttribute('normalizedParams'); $reportingUriBase = $this->policyProvider->getDefaultReportingUriBase($scope, $request, false); return str_starts_with($normalizedParams?->getRequestUri() ?? '', (string)$reportingUriBase); } /** * Determines, whether the request is eligible to be handled by the local reporting URI * (`targetsCspReportUri()` must have been called before). */ protected function isCspReport( Scope $scope, ServerRequestInterface $request, ?DispositionConfiguration $dispositionConfiguration = null, ): bool { // @todo // + verify current session // + invoke rate limiter // + check additional scope (snippet enrichment) $reportingUrl = $this->policyProvider->getReportingUrlFor($scope, $request, $dispositionConfiguration); $reportingUriBase = $this->policyProvider->getDefaultReportingUriBase($scope, $request); $contentTypeHeader = $request->getHeaderLine('content-type'); return // stop, if reporting was explicitly disabled in `contentSecurityPolicyReportingUrl` $reportingUrl !== null // stop, if different reporting URI was configured in `contentSecurityPolicyReportingUrl` && str_starts_with((string)$reportingUrl, (string)$reportingUriBase) // stop, if request is not `POST` or `Content-Type` is not `application/csp-report` && $request->getMethod() === 'POST' && $contentTypeHeader === 'application/csp-report'; } protected function getRequestQueryParam(ServerRequestInterface $request, string $name): ?string { $value = $request->getQueryParams()[$name] ?? null; return is_string($value) ? $value : null; } protected function isJson(string $value): bool { try { json_decode($value, false, 16, JSON_THROW_ON_ERROR); return true; } catch (\JsonException) { return false; } } }