36 private LoggerInterface $log;
41 private const MAX_POST_SIZE = 8192;
48 $logname = $reportOnly ?
'csp-report-only' :
'csp';
49 $this->log = LoggerFactory::getInstance( $logname );
50 $userAgent = $this->
getRequest()->getHeader(
'user-agent' );
52 $this->verifyPostBodyOk();
53 $report = $this->getReport();
54 $flags = $this->getFlags( $report, $userAgent );
56 $warningText = $this->generateLogLine( $flags, $report );
57 $this->logReport( $flags, $warningText, [
59 'csp-report' => $report,
60 'method' => __METHOD__,
61 'user_id' => $this->
getUser()->getId() ?:
'logged-out',
62 'user-agent' => $userAgent,
74 private function logReport( $flags, $logLine, $context ) {
75 if ( in_array(
'false-positive', $flags ) ) {
77 $this->log->debug( $logLine, $context );
80 $this->log->warning( $logLine, $context );
91 private function getFlags( $report, $userAgent ) {
94 $falsePositives = $this->
getConfig()->get( MainConfigNames::CSPFalsePositiveUrls );
101 $flags[] =
'report-only';
106 ContentSecurityPolicy::falsePositiveBrowser( $userAgent ) &&
107 $report[
'blocked-uri'] ===
"self"
110 isset( $report[
'blocked-uri'] ) &&
111 $this->matchUrlPattern( $report[
'blocked-uri'], $falsePositives )
114 isset( $report[
'source-file'] ) &&
115 $this->matchUrlPattern( $report[
'source-file'], $falsePositives )
121 $flags[] =
'false-positive';
131 private function matchUrlPattern( $url, array $patterns ) {
132 if ( isset( $patterns[ $url ] ) ) {
137 unset( $bits[
'user'], $bits[
'pass'], $bits[
'query'], $bits[
'fragment'] );
140 if ( isset( $patterns[$serverUrl] ) ) {
145 foreach ( $patterns as $pattern => $val ) {
149 if ( str_ends_with( $pattern,
'/' ) && str_starts_with( $url, $pattern ) ) {
162 private function verifyPostBodyOk() {
164 $contentType = $req->getHeader(
'content-type' );
165 if ( $contentType !==
'application/json'
166 && $contentType !==
'application/csp-report'
168 $this->error(
'wrongformat', __METHOD__ );
170 if ( $req->getHeader(
'content-length' ) > self::MAX_POST_SIZE ) {
171 $this->error(
'toobig', __METHOD__ );
180 private function getReport() {
181 $postBody = $this->
getRequest()->getRawInput();
182 if ( strlen( $postBody ) > self::MAX_POST_SIZE ) {
184 $this->error(
'toobig', __METHOD__ );
187 if ( !$status->isGood() ) {
188 $msg = $status->getErrors()[0][
'message'];
189 if ( $msg instanceof
Message ) {
190 $msg = $msg->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'];
210 private function generateLogLine( $flags, $report ) {
213 $flagText =
'[' . implode(
', ', $flags ) .
']';
216 $blockedOrigin = isset( $report[
'blocked-uri'] )
217 ? $this->originFromUrl( $report[
'blocked-uri'] )
219 $page = $report[
'document-uri'] ??
'n/a';
220 $line = isset( $report[
'line-number'] )
221 ?
':' . $report[
'line-number']
224 ' Received CSP report: <' . $blockedOrigin .
'>' .
225 ' blocked from being loaded on <' . $page .
'>' . $line;
232 private function originFromUrl( $url ) {
234 unset( $bits[
'user'], $bits[
'pass'], $bits[
'query'], $bits[
'fragment'] );
247 private function error( $code, $method ) {
248 $this->log->info(
'Error reading CSP report: ' . $code, [
250 'user-agent' => $this->
getRequest()->getHeader(
'user-agent' )
254 [
'apierror-csp-report',
wfEscapeWikiText( $code ) ],
'cspreport-' . $code, [], 400
261 ParamValidator::PARAM_TYPE =>
'boolean',
262 ParamValidator::PARAM_DEFAULT => false
265 ParamValidator::PARAM_TYPE =>
'string',
266 ParamValidator::PARAM_DEFAULT =>
'internal',
267 ParamValidator::PARAM_REQUIRED => false