16use Psr\Log\LoggerInterface;
26 private LoggerInterface $log;
31 private const MAX_POST_SIZE = 8192;
37 parent::__construct( $main, $action );
45 $logname = $reportOnly ?
'csp-report-only' :
'csp';
46 $this->log = LoggerFactory::getInstance( $logname );
47 $userAgent = $this->
getRequest()->getHeader(
'user-agent' );
49 $this->verifyPostBodyOk();
50 $report = $this->getReport();
51 $flags = $this->getFlags( $report, $userAgent );
54 '[{flags}] Received CSP report: <{blockedOrigin}> blocked from being loaded on <{documentUri}>:{line}',
56 'flags' => implode(
', ', $flags ),
57 'blockedOrigin' => isset( $report[
'blocked-uri'] ) ?
58 $this->originFromUrl( $report[
'blocked-uri'] ) :
'n/a',
59 'documentUri' => $report[
'document-uri'] ??
'n/a',
60 'line' => $report[
'line-number'] ??
'',
62 'csp-report' => $report,
63 'method' => __METHOD__,
64 'user_id' => $this->
getUser()->getId() ?:
'logged-out',
65 'user-agent' => $userAgent,
79 private function logReport( $logLine, $context, $flags ) {
80 if ( in_array(
'false-positive', $flags ) ) {
82 $this->log->debug( $logLine, $context );
85 $this->log->warning( $logLine, $context );
96 private function getFlags( $report, $userAgent ) {
102 if (
$source !==
'internal' ) {
103 $flags[] =
'source=' .
$source;
106 $flags[] =
'report-only';
111 ContentSecurityPolicy::falsePositiveBrowser( $userAgent ) &&
112 $report[
'blocked-uri'] ===
"self"
115 isset( $report[
'blocked-uri'] ) &&
116 $this->matchUrlPattern( $report[
'blocked-uri'], $falsePositives )
119 isset( $report[
'source-file'] ) &&
120 $this->matchUrlPattern( $report[
'source-file'], $falsePositives )
126 $flags[] =
'false-positive';
136 private function matchUrlPattern(
$url, array $patterns ) {
137 if ( isset( $patterns[
$url ] ) ) {
141 $serverUrl = $this->originFromUrl(
$url );
143 if ( isset( $patterns[$serverUrl] ) ) {
148 foreach ( $patterns as $pattern => $val ) {
152 if ( str_ends_with( $pattern,
'/' ) && str_starts_with(
$url, $pattern ) ) {
165 private function verifyPostBodyOk() {
167 $contentType = $req->getHeader(
'content-type' );
168 if ( $contentType !==
'application/json'
169 && $contentType !==
'application/csp-report'
171 $this->error(
'wrongformat', __METHOD__ );
173 if ( $req->getHeader(
'content-length' ) > self::MAX_POST_SIZE ) {
174 $this->error(
'toobig', __METHOD__ );
183 private function getReport() {
184 $postBody = $this->
getRequest()->getRawInput();
185 if ( strlen( $postBody ) > self::MAX_POST_SIZE ) {
187 $this->error(
'toobig', __METHOD__ );
189 $status = FormatJson::parse( $postBody, FormatJson::FORCE_ASSOC );
190 if ( !$status->isGood() ) {
191 $msg = $status->getMessages()[0]->getKey();
192 $this->error( $msg, __METHOD__ );
195 $report = $status->getValue();
197 if ( !isset( $report[
'csp-report'] ) ) {
198 $this->error(
'missingkey', __METHOD__ );
200 return $report[
'csp-report'];
213 private function originFromUrl(
$url ) {
214 $bits = parse_url(
$url );
216 if ( !$bits || !isset( $bits[
'scheme'] ) ) {
220 unset( $bits[
'user'], $bits[
'pass'], $bits[
'query'], $bits[
'fragment'] );
223 if ( !isset( $bits[
'delimiter'] ) ) {
224 $bits[
'delimiter'] =
'://';
228 return UrlUtils::assemble( $bits );
237 private function error( $code, $method ): never {
238 $this->log->info(
'Error reading CSP report: ' . $code, [
240 'user-agent' => $this->
getRequest()->getHeader(
'user-agent' )
244 [
'apierror-csp-report',
wfEscapeWikiText( $code ) ],
'cspreport-' . $code, [], 400
252 ParamValidator::PARAM_TYPE =>
'boolean',
253 ParamValidator::PARAM_DEFAULT => false
256 ParamValidator::PARAM_TYPE =>
'string',
257 ParamValidator::PARAM_DEFAULT =>
'internal',
258 ParamValidator::PARAM_REQUIRED => false
296class_alias( ApiCSPReport::class,
'ApiCSPReport' );
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
This is the main API class, used for both external and internal processing.
A class containing constants representing the names of configuration variables.
const CSPFalsePositiveUrls
Name constant for the CSPFalsePositiveUrls setting, for use with Config::get()
Handle sending Content-Security-Policy headers.