Completed
Branch master (a553dc)
by
unknown
26:33
created

ApiCSPReport   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 195
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 195
rs 10
wmc 27
lcom 1
cbo 8

13 Methods

Rating   Name   Duplication   Size   Complexity  
A execute() 0 21 2
A logReport() 0 9 2
A getFlags() 0 14 3
A verifyPostBodyOk() 0 12 4
A getReport() 0 19 4
B generateLogLine() 0 14 5
A error() 0 8 1
A getAllowedParams() 0 13 1
A mustBePosted() 0 3 1
A isWriteMode() 0 3 1
A isInternal() 0 3 1
A isReadMode() 0 3 1
A shouldCheckMaxLag() 0 3 1
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 $flags Array Flags for this report
67
	 * @param $logLine String text of log entry
68
	 * @param $context Array 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 $report Array The CSP report
84
	 * @return Array
85
	 */
86
	private function getFlags( $report ) {
87
		$reportOnly = $this->getParameter( 'reportonly' );
88
		$userAgent = $this->getRequest()->getHeader( 'user-agent' );
0 ignored issues
show
Unused Code introduced by
$userAgent is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
89
		$source = $this->getParameter( 'source' );
90
91
		$flags = [];
92
		if ( $source !== 'internal' ) {
93
			$flags[] = 'source=' . $source;
94
		}
95
		if ( $reportOnly ) {
96
			$flags[] = 'report-only';
97
		}
98
		return $flags;
99
	}
100
101
	/**
102
	 * Output an api error if post body is obviously not OK.
103
	 */
104
	private function verifyPostBodyOk() {
105
		$req = $this->getRequest();
106
		$contentType = $req->getHeader( 'content-type' );
107
		if ( $contentType !== 'application/json'
108
			&& $contentType !=='application/csp-report'
109
		) {
110
			$this->error( 'wrongformat', __METHOD__ );
111
		}
112
		if ( $req->getHeader( 'content-length' ) > self::MAX_POST_SIZE ) {
113
			$this->error( 'toobig', __METHOD__ );
114
		}
115
	}
116
117
	/**
118
	 * Get the report from post body and turn into associative array.
119
	 *
120
	 * @return Array
121
	 */
122
	private function getReport() {
123
		$postBody = $this->getRequest()->getRawInput();
124
		if ( strlen( $postBody ) > self::MAX_POST_SIZE ) {
125
			// paranoia, already checked content-length earlier.
126
			$this->error( 'toobig', __METHOD__ );
127
		}
128
		$status = FormatJson::parse( $postBody, FormatJson::FORCE_ASSOC );
129
		if ( !$status->isGood() ) {
130
			list( $code, ) = $this->getErrorFromStatus( $status );
131
			$this->error( $code, __METHOD__ );
132
		}
133
134
		$report = $status->getValue();
135
136
		if ( !isset( $report['csp-report'] ) ) {
137
			$this->error( 'missingkey', __METHOD__ );
138
		}
139
		return $report['csp-report'];
140
	}
141
142
	/**
143
	 * Get text of log line.
144
	 *
145
	 * @param $flags Array of additional markers for this report
146
	 * @param $report Array the csp report
147
	 * @return String Text to put in log
148
	 */
149
	private function generateLogLine( $flags, $report ) {
150
		$flagText = '';
151
		if ( $flags ) {
152
			$flagText = '[' . implode( $flags, ', ' ) . ']';
153
		}
154
155
		$blockedFile = isset( $report['blocked-uri'] ) ? $report['blocked-uri'] : 'n/a';
156
		$page = isset( $report['document-uri'] ) ? $report['document-uri'] : 'n/a';
157
		$line = isset( $report['line-number'] ) ? ':' . $report['line-number'] : '';
158
		$warningText = $flagText .
159
			' Received CSP report: <' . $blockedFile .
160
			'> blocked from being loaded on <' . $page . '>' . $line;
161
		return $warningText;
162
	}
163
164
	/**
165
	 * Stop processing the request, and output/log an error
166
	 *
167
	 * @param $code String error code
168
	 * @param $method String method that made error
169
	 * @throws UsageException Always
170
	 */
171
	private function error( $code, $method ) {
172
		$this->log->info( 'Error reading CSP report: ' . $code, [
173
			'method' => $method,
174
			'user-agent' => $this->getRequest()->getHeader( 'user-agent' )
175
		] );
176
		// 500 so it shows up in browser's developer console.
177
		$this->dieUsage( "Error processing CSP report: $code", 'cspreport-' . $code, 500 );
178
	}
179
180
	public function getAllowedParams() {
181
		return [
182
			'reportonly' => [
183
				ApiBase::PARAM_TYPE => 'boolean',
184
				ApiBase::PARAM_DFLT => false
185
			],
186
			'source' => [
187
				ApiBase::PARAM_TYPE => 'string',
188
				ApiBase::PARAM_DFLT => 'internal',
189
				ApiBase::PARAM_REQUIRED => false
190
			]
191
		];
192
	}
193
194
	public function mustBePosted() {
195
		return true;
196
	}
197
198
	public function isWriteMode() {
199
		return false;
200
	}
201
202
	/**
203
	 * Mark as internal. This isn't meant to be used by normal api users
204
	 */
205
	public function isInternal() {
206
		return true;
207
	}
208
209
	/**
210
	 * Even if you don't have read rights, we still want your report.
211
	 */
212
	public function isReadMode() {
213
		return false;
214
	}
215
216
	/**
217
	 * Doesn't touch db, so max lag should be rather irrelavent.
218
	 *
219
	 * Also, this makes sure that reports aren't lost during lag events.
220
	 */
221
	public function shouldCheckMaxLag() {
222
		return false;
223
	}
224
}
225