]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/api/ApiCSPReport.php
MediaWiki 1.30.2-scripts2
[autoinstalls/mediawiki.git] / includes / api / ApiCSPReport.php
1 <?php
2 /**
3  * Copyright © 2015 Brian Wolff
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  * http://www.gnu.org/copyleft/gpl.html
19  *
20  * @file
21  */
22
23 use MediaWiki\Logger\LoggerFactory;
24
25 /**
26  * Api module to receive and log CSP violation reports
27  *
28  * @ingroup API
29  */
30 class ApiCSPReport extends ApiBase {
31
32         private $log;
33
34         /**
35          * These reports should be small. Ignore super big reports out of paranoia
36          */
37         const MAX_POST_SIZE = 8192;
38
39         /**
40          * Logs a content-security-policy violation report from web browser.
41          */
42         public function execute() {
43                 $reportOnly = $this->getParameter( 'reportonly' );
44                 $logname = $reportOnly ? 'csp-report-only' : 'csp';
45                 $this->log = LoggerFactory::getInstance( $logname );
46                 $userAgent = $this->getRequest()->getHeader( 'user-agent' );
47
48                 $this->verifyPostBodyOk();
49                 $report = $this->getReport();
50                 $flags = $this->getFlags( $report );
51
52                 $warningText = $this->generateLogLine( $flags, $report );
53                 $this->logReport( $flags, $warningText, [
54                         // XXX Is it ok to put untrusted data into log??
55                         'csp-report' => $report,
56                         'method' => __METHOD__,
57                         'user' => $this->getUser()->getName(),
58                         'user-agent' => $userAgent,
59                         'source' => $this->getParameter( 'source' ),
60                 ] );
61                 $this->getResult()->addValue( null, $this->getModuleName(), 'success' );
62         }
63
64         /**
65          * Log CSP report, with a different severity depending on $flags
66          * @param array $flags Flags for this report
67          * @param string $logLine text of log entry
68          * @param array $context logging context
69          */
70         private function logReport( $flags, $logLine, $context ) {
71                 if ( in_array( 'false-positive', $flags ) ) {
72                         // These reports probably don't matter much
73                         $this->log->debug( $logLine, $context );
74                 } else {
75                         // Normal report.
76                         $this->log->warning( $logLine, $context );
77                 }
78         }
79
80         /**
81          * Get extra notes about the report.
82          *
83          * @param array $report The CSP report
84          * @return array
85          */
86         private function getFlags( $report ) {
87                 $reportOnly = $this->getParameter( 'reportonly' );
88                 $source = $this->getParameter( 'source' );
89                 $falsePositives = $this->getConfig()->get( 'CSPFalsePositiveUrls' );
90
91                 $flags = [];
92                 if ( $source !== 'internal' ) {
93                         $flags[] = 'source=' . $source;
94                 }
95                 if ( $reportOnly ) {
96                         $flags[] = 'report-only';
97                 }
98
99                 if (
100                         ( isset( $report['blocked-uri'] ) &&
101                         isset( $falsePositives[$report['blocked-uri']] ) )
102                         || ( isset( $report['source-file'] ) &&
103                         isset( $falsePositives[$report['source-file']] ) )
104                 ) {
105                         // Report caused by Ad-Ware
106                         $flags[] = 'false-positive';
107                 }
108                 return $flags;
109         }
110
111         /**
112          * Output an api error if post body is obviously not OK.
113          */
114         private function verifyPostBodyOk() {
115                 $req = $this->getRequest();
116                 $contentType = $req->getHeader( 'content-type' );
117                 if ( $contentType !== 'application/json'
118                         && $contentType !== 'application/csp-report'
119                 ) {
120                         $this->error( 'wrongformat', __METHOD__ );
121                 }
122                 if ( $req->getHeader( 'content-length' ) > self::MAX_POST_SIZE ) {
123                         $this->error( 'toobig', __METHOD__ );
124                 }
125         }
126
127         /**
128          * Get the report from post body and turn into associative array.
129          *
130          * @return Array
131          */
132         private function getReport() {
133                 $postBody = $this->getRequest()->getRawInput();
134                 if ( strlen( $postBody ) > self::MAX_POST_SIZE ) {
135                         // paranoia, already checked content-length earlier.
136                         $this->error( 'toobig', __METHOD__ );
137                 }
138                 $status = FormatJson::parse( $postBody, FormatJson::FORCE_ASSOC );
139                 if ( !$status->isGood() ) {
140                         $msg = $status->getErrors()[0]['message'];
141                         if ( $msg instanceof Message ) {
142                                 $msg = $msg->getKey();
143                         }
144                         $this->error( $msg, __METHOD__ );
145                 }
146
147                 $report = $status->getValue();
148
149                 if ( !isset( $report['csp-report'] ) ) {
150                         $this->error( 'missingkey', __METHOD__ );
151                 }
152                 return $report['csp-report'];
153         }
154
155         /**
156          * Get text of log line.
157          *
158          * @param array $flags of additional markers for this report
159          * @param array $report the csp report
160          * @return string Text to put in log
161          */
162         private function generateLogLine( $flags, $report ) {
163                 $flagText = '';
164                 if ( $flags ) {
165                         $flagText = '[' . implode( $flags, ', ' ) . ']';
166                 }
167
168                 $blockedFile = isset( $report['blocked-uri'] ) ? $report['blocked-uri'] : 'n/a';
169                 $page = isset( $report['document-uri'] ) ? $report['document-uri'] : 'n/a';
170                 $line = isset( $report['line-number'] ) ? ':' . $report['line-number'] : '';
171                 $warningText = $flagText .
172                         ' Received CSP report: <' . $blockedFile .
173                         '> blocked from being loaded on <' . $page . '>' . $line;
174                 return $warningText;
175         }
176
177         /**
178          * Stop processing the request, and output/log an error
179          *
180          * @param string $code error code
181          * @param string $method method that made error
182          * @throws ApiUsageException Always
183          */
184         private function error( $code, $method ) {
185                 $this->log->info( 'Error reading CSP report: ' . $code, [
186                         'method' => $method,
187                         'user-agent' => $this->getRequest()->getHeader( 'user-agent' )
188                 ] );
189                 // Return 400 on error for user agents to display, e.g. to the console.
190                 $this->dieWithError(
191                         [ 'apierror-csp-report', wfEscapeWikiText( $code ) ], 'cspreport-' . $code, [], 400
192                 );
193         }
194
195         public function getAllowedParams() {
196                 return [
197                         'reportonly' => [
198                                 ApiBase::PARAM_TYPE => 'boolean',
199                                 ApiBase::PARAM_DFLT => false
200                         ],
201                         'source' => [
202                                 ApiBase::PARAM_TYPE => 'string',
203                                 ApiBase::PARAM_DFLT => 'internal',
204                                 ApiBase::PARAM_REQUIRED => false
205                         ]
206                 ];
207         }
208
209         public function mustBePosted() {
210                 return true;
211         }
212
213         public function isWriteMode() {
214                 return false;
215         }
216
217         /**
218          * Mark as internal. This isn't meant to be used by normal api users
219          * @return bool
220          */
221         public function isInternal() {
222                 return true;
223         }
224
225         /**
226          * Even if you don't have read rights, we still want your report.
227          * @return bool
228          */
229         public function isReadMode() {
230                 return false;
231         }
232
233         /**
234          * Doesn't touch db, so max lag should be rather irrelavent.
235          *
236          * Also, this makes sure that reports aren't lost during lag events.
237          * @return bool
238          */
239         public function shouldCheckMaxLag() {
240                 return false;
241         }
242 }