37 private LoggerInterface $log;
42 private const MAX_POST_SIZE = 8192;
49 $logname = $reportOnly ?
'csp-report-only' :
'csp';
50 $this->log = LoggerFactory::getInstance( $logname );
51 $userAgent = $this->
getRequest()->getHeader(
'user-agent' );
53 $this->verifyPostBodyOk();
54 $report = $this->getReport();
55 $flags = $this->getFlags( $report, $userAgent );
57 $warningText = $this->generateLogLine( $flags, $report );
58 $this->logReport( $flags, $warningText, [
60 'csp-report' => $report,
61 'method' => __METHOD__,
62 'user_id' => $this->
getUser()->getId() ?:
'logged-out',
63 'user-agent' => $userAgent,
75 private function logReport( $flags, $logLine, $context ) {
76 if ( in_array(
'false-positive', $flags ) ) {
78 $this->log->debug( $logLine, $context );
81 $this->log->warning( $logLine, $context );
92 private function getFlags( $report, $userAgent ) {
95 $falsePositives = $this->
getConfig()->get( MainConfigNames::CSPFalsePositiveUrls );
102 $flags[] =
'report-only';
107 ContentSecurityPolicy::falsePositiveBrowser( $userAgent ) &&
108 $report[
'blocked-uri'] ===
"self"
111 isset( $report[
'blocked-uri'] ) &&
112 $this->matchUrlPattern( $report[
'blocked-uri'], $falsePositives )
115 isset( $report[
'source-file'] ) &&
116 $this->matchUrlPattern( $report[
'source-file'], $falsePositives )
122 $flags[] =
'false-positive';
132 private function matchUrlPattern( $url, array $patterns ) {
133 if ( isset( $patterns[ $url ] ) ) {
138 unset( $bits[
'user'], $bits[
'pass'], $bits[
'query'], $bits[
'fragment'] );
141 if ( isset( $patterns[$serverUrl] ) ) {
146 foreach ( $patterns as $pattern => $val ) {
150 if ( str_ends_with( $pattern,
'/' ) && str_starts_with( $url, $pattern ) ) {
163 private function verifyPostBodyOk() {
165 $contentType = $req->getHeader(
'content-type' );
166 if ( $contentType !==
'application/json'
167 && $contentType !==
'application/csp-report'
169 $this->error(
'wrongformat', __METHOD__ );
171 if ( $req->getHeader(
'content-length' ) > self::MAX_POST_SIZE ) {
172 $this->error(
'toobig', __METHOD__ );
181 private function getReport() {
182 $postBody = $this->
getRequest()->getRawInput();
183 if ( strlen( $postBody ) > self::MAX_POST_SIZE ) {
185 $this->error(
'toobig', __METHOD__ );
187 $status = FormatJson::parse( $postBody, FormatJson::FORCE_ASSOC );
188 if ( !$status->isGood() ) {
189 $msg = $status->getErrors()[0][
'message'];
190 if ( $msg instanceof Message ) {
191 $msg = $msg->getKey();
193 $this->error( $msg, __METHOD__ );
196 $report = $status->getValue();
198 if ( !isset( $report[
'csp-report'] ) ) {
199 $this->error(
'missingkey', __METHOD__ );
201 return $report[
'csp-report'];
211 private function generateLogLine( $flags, $report ) {
214 $flagText =
'[' . implode(
', ', $flags ) .
']';
217 $blockedOrigin = isset( $report[
'blocked-uri'] )
218 ? $this->originFromUrl( $report[
'blocked-uri'] )
220 $page = $report[
'document-uri'] ??
'n/a';
221 $line = isset( $report[
'line-number'] )
222 ?
':' . $report[
'line-number']
225 ' Received CSP report: <' . $blockedOrigin .
'>' .
226 ' blocked from being loaded on <' . $page .
'>' . $line;
233 private function originFromUrl( $url ) {
235 unset( $bits[
'user'], $bits[
'pass'], $bits[
'query'], $bits[
'fragment'] );
248 private function error( $code, $method ) {
249 $this->log->info(
'Error reading CSP report: ' . $code, [
251 'user-agent' => $this->
getRequest()->getHeader(
'user-agent' )
255 [
'apierror-csp-report',
wfEscapeWikiText( $code ) ],
'cspreport-' . $code, [], 400
262 ParamValidator::PARAM_TYPE =>
'boolean',
263 ParamValidator::PARAM_DEFAULT => false
266 ParamValidator::PARAM_TYPE =>
'string',
267 ParamValidator::PARAM_DEFAULT =>
'internal',
268 ParamValidator::PARAM_REQUIRED => false