25 use Psr\Log\LoggerInterface;
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';
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 ( substr( $pattern, -1 ) ===
'/' && strpos( $url, $pattern ) === 0 ) {
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']
223 $warningText = $flagText .
224 ' Received CSP report: <' . $blockedOrigin .
'>' .
225 ' blocked from being loaded on <' . $page .
'>' .
$line;
233 private function originFromUrl( $url ) {
235 unset( $bits[
'user'], $bits[
'pass'], $bits[
'query'], $bits[
'fragment'] );
249 private function error( $code, $method ) {
250 $this->log->info(
'Error reading CSP report: ' . $code, [
252 'user-agent' => $this->
getRequest()->getHeader(
'user-agent' )
256 [
'apierror-csp-report',
wfEscapeWikiText( $code ) ],
'cspreport-' . $code, [], 400
263 ParamValidator::PARAM_TYPE =>
'boolean',
264 ParamValidator::PARAM_DEFAULT => false
267 ParamValidator::PARAM_TYPE =>
'string',
268 ParamValidator::PARAM_DEFAULT =>
'internal',
269 ParamValidator::PARAM_REQUIRED => false
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.
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
getParameter( $paramName, $parseLimit=true)
Get a value for the given parameter.
getResult()
Get the result object.
getModuleName()
Get the name of the module being executed by this instance.
Api module to receive and log CSP violation reports.
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.
shouldCheckMaxLag()
Doesn't touch db, so max lag should be rather irrelevant.
isReadMode()
Even if you don't have read rights, we still want your report.
mustBePosted()
Indicates whether this module must be called with a POST request.
isInternal()
Mark as internal.
static falsePositiveBrowser( $ua)
Does this browser give false positive reports?
A class containing constants representing the names of configuration variables.
The Message class deals with fetching and processing of interface message into a variety of formats.