MediaWiki REL1_37
ApiCSPReport.php
Go to the documentation of this file.
1<?php
24use Psr\Log\LoggerInterface;
25
31class ApiCSPReport extends ApiBase {
32
34 private $log;
35
39 private const MAX_POST_SIZE = 8192;
40
44 public function execute() {
45 $reportOnly = $this->getParameter( 'reportonly' );
46 $logname = $reportOnly ? 'csp-report-only' : 'csp';
47 $this->log = LoggerFactory::getInstance( $logname );
48 $userAgent = $this->getRequest()->getHeader( 'user-agent' );
49
50 $this->verifyPostBodyOk();
51 $report = $this->getReport();
52 $flags = $this->getFlags( $report, $userAgent );
53
54 $warningText = $this->generateLogLine( $flags, $report );
55 $this->logReport( $flags, $warningText, [
56 // XXX Is it ok to put untrusted data into log??
57 'csp-report' => $report,
58 'method' => __METHOD__,
59 'user_id' => $this->getUser()->getId() ?: 'logged-out',
60 'user-agent' => $userAgent,
61 'source' => $this->getParameter( 'source' ),
62 ] );
63 $this->getResult()->addValue( null, $this->getModuleName(), 'success' );
64 }
65
72 private function logReport( $flags, $logLine, $context ) {
73 if ( in_array( 'false-positive', $flags ) ) {
74 // These reports probably don't matter much
75 $this->log->debug( $logLine, $context );
76 } else {
77 // Normal report.
78 $this->log->warning( $logLine, $context );
79 }
80 }
81
89 private function getFlags( $report, $userAgent ) {
90 $reportOnly = $this->getParameter( 'reportonly' );
91 $source = $this->getParameter( 'source' );
92 $falsePositives = $this->getConfig()->get( 'CSPFalsePositiveUrls' );
93
94 $flags = [];
95 if ( $source !== 'internal' ) {
96 $flags[] = 'source=' . $source;
97 }
98 if ( $reportOnly ) {
99 $flags[] = 'report-only';
100 }
101
102 if (
103 (
105 $report['blocked-uri'] === "self"
106 ) ||
107 (
108 isset( $report['blocked-uri'] ) &&
109 $this->matchUrlPattern( $report['blocked-uri'], $falsePositives )
110 ) ||
111 (
112 isset( $report['source-file'] ) &&
113 $this->matchUrlPattern( $report['source-file'], $falsePositives )
114 )
115 ) {
116 // False positive due to:
117 // https://bugzilla.mozilla.org/show_bug.cgi?id=1026520
118
119 $flags[] = 'false-positive';
120 }
121 return $flags;
122 }
123
129 private function matchUrlPattern( $url, array $patterns ) {
130 if ( isset( $patterns[ $url ] ) ) {
131 return true;
132 }
133
134 $bits = wfParseUrl( $url );
135 unset( $bits['user'], $bits['pass'], $bits['query'], $bits['fragment'] );
136 $bits['path'] = '';
137 $serverUrl = wfAssembleUrl( $bits );
138 if ( isset( $patterns[$serverUrl] ) ) {
139 // The origin of the url matches a pattern,
140 // e.g. "https://example.org" matches "https://example.org/foo/b?a#r"
141 return true;
142 }
143 foreach ( $patterns as $pattern => $val ) {
144 // We only use this pattern if it ends in a slash, this prevents
145 // "/foos" from matching "/foo", and "https://good.combo.bad" matching
146 // "https://good.com".
147 if ( substr( $pattern, -1 ) === '/' && strpos( $url, $pattern ) === 0 ) {
148 // The pattern starts with the same as the url
149 // e.g. "https://example.org/foo/" matches "https://example.org/foo/b?a#r"
150 return true;
151 }
152 }
153
154 return false;
155 }
156
160 private function verifyPostBodyOk() {
161 $req = $this->getRequest();
162 $contentType = $req->getHeader( 'content-type' );
163 if ( $contentType !== 'application/json'
164 && $contentType !== 'application/csp-report'
165 ) {
166 $this->error( 'wrongformat', __METHOD__ );
167 }
168 if ( $req->getHeader( 'content-length' ) > self::MAX_POST_SIZE ) {
169 $this->error( 'toobig', __METHOD__ );
170 }
171 }
172
178 private function getReport() {
179 $postBody = $this->getRequest()->getRawInput();
180 if ( strlen( $postBody ) > self::MAX_POST_SIZE ) {
181 // paranoia, already checked content-length earlier.
182 $this->error( 'toobig', __METHOD__ );
183 }
184 $status = FormatJson::parse( $postBody, FormatJson::FORCE_ASSOC );
185 if ( !$status->isGood() ) {
186 $msg = $status->getErrors()[0]['message'];
187 if ( $msg instanceof Message ) {
188 $msg = $msg->getKey();
189 }
190 $this->error( $msg, __METHOD__ );
191 }
192
193 $report = $status->getValue();
194
195 if ( !isset( $report['csp-report'] ) ) {
196 $this->error( 'missingkey', __METHOD__ );
197 }
198 return $report['csp-report'];
199 }
200
208 private function generateLogLine( $flags, $report ) {
209 $flagText = '';
210 if ( $flags ) {
211 $flagText = '[' . implode( ', ', $flags ) . ']';
212 }
213
214 $blockedOrigin = isset( $report['blocked-uri'] )
215 ? $this->originFromUrl( $report['blocked-uri'] )
216 : 'n/a';
217 $page = $report['document-uri'] ?? 'n/a';
218 $line = isset( $report['line-number'] )
219 ? ':' . $report['line-number']
220 : '';
221 $warningText = $flagText .
222 ' Received CSP report: <' . $blockedOrigin . '>' .
223 ' blocked from being loaded on <' . $page . '>' . $line;
224 return $warningText;
225 }
226
231 private function originFromUrl( $url ) {
232 $bits = wfParseUrl( $url );
233 unset( $bits['user'], $bits['pass'], $bits['query'], $bits['fragment'] );
234 $bits['path'] = '';
235 $serverUrl = wfAssembleUrl( $bits );
236 // e.g. "https://example.org" from "https://example.org/foo/b?a#r"
237 return $serverUrl;
238 }
239
247 private function error( $code, $method ) {
248 $this->log->info( 'Error reading CSP report: ' . $code, [
249 'method' => $method,
250 'user-agent' => $this->getRequest()->getHeader( 'user-agent' )
251 ] );
252 // Return 400 on error for user agents to display, e.g. to the console.
253 $this->dieWithError(
254 [ 'apierror-csp-report', wfEscapeWikiText( $code ) ], 'cspreport-' . $code, [], 400
255 );
256 }
257
258 public function getAllowedParams() {
259 return [
260 'reportonly' => [
261 ApiBase::PARAM_TYPE => 'boolean',
262 ApiBase::PARAM_DFLT => false
263 ],
264 'source' => [
265 ApiBase::PARAM_TYPE => 'string',
266 ApiBase::PARAM_DFLT => 'internal',
268 ]
269 ];
270 }
271
272 public function mustBePosted() {
273 return true;
274 }
275
280 public function isInternal() {
281 return true;
282 }
283
288 public function isReadMode() {
289 return false;
290 }
291
298 public function shouldCheckMaxLag() {
299 return false;
300 }
301}
wfParseUrl( $url)
parse_url() work-alike, but non-broken.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfAssembleUrl( $urlParts)
This function will reassemble a URL parsed with wfParseURL.
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:55
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1436
const PARAM_REQUIRED
Definition ApiBase.php:105
getParameter( $paramName, $parseLimit=true)
Get a value for the given parameter.
Definition ApiBase.php:884
const PARAM_TYPE
Definition ApiBase.php:81
const PARAM_DFLT
Definition ApiBase.php:73
getResult()
Get the result object.
Definition ApiBase.php:628
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:497
Api module to receive and log CSP violation reports.
getReport()
Get the report from post body and turn into associative array.
matchUrlPattern( $url, array $patterns)
error( $code, $method)
Stop processing the request, and output/log an error.
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
execute()
Logs a content-security-policy violation report from web browser.
LoggerInterface $log
shouldCheckMaxLag()
Doesn't touch db, so max lag should be rather irrelavent.
isReadMode()
Even if you don't have read rights, we still want your report.
logReport( $flags, $logLine, $context)
Log CSP report, with a different severity depending on $flags.
originFromUrl( $url)
getFlags( $report, $userAgent)
Get extra notes about the report.
const MAX_POST_SIZE
These reports should be small.
mustBePosted()
Indicates whether this module must be called with a POST request.
isInternal()
Mark as internal.
generateLogLine( $flags, $report)
Get text of log line.
verifyPostBodyOk()
Output an api error if post body is obviously not OK.
static falsePositiveBrowser( $ua)
Does this browser give false positive reports?
IContextSource $context
PSR-3 logger instance factory.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:138
$line
Definition mcc.php:119
$source