Brian Wolff has uploaded a new change for review. https://gerrit.wikimedia.org/r/274022
Change subject: Add API module to receive CSP reports. ...................................................................... Add API module to receive CSP reports. There are two expected usecases for this: * The proposed builtin CSP support at I80f6f4 * Setting CSP headers on media served from upload.wikimedia.org For details on CSP, see http://www.w3.org/TR/CSP2/ See also <FIXME: url to CSP RFC> Bug: FIXME Add bug number here Change-Id: Id92126ca7707186757e77fe50cd336ff1acb8b3f --- M autoload.php A includes/api/ApiCSPReport.php M includes/api/ApiMain.php M includes/api/i18n/en.json M includes/api/i18n/qqq.json 5 files changed, 238 insertions(+), 1 deletion(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core refs/changes/22/274022/1 diff --git a/autoload.php b/autoload.php index 5ec6218..289b5a2 100644 --- a/autoload.php +++ b/autoload.php @@ -24,6 +24,7 @@ 'ApiComparePages' => __DIR__ . '/includes/api/ApiComparePages.php', 'ApiContinuationManager' => __DIR__ . '/includes/api/ApiContinuationManager.php', 'ApiCreateAccount' => __DIR__ . '/includes/api/ApiCreateAccount.php', + 'ApiCSPReport' => __DIR__ . '/includes/api/ApiCSPReport.php', 'ApiDelete' => __DIR__ . '/includes/api/ApiDelete.php', 'ApiDisabled' => __DIR__ . '/includes/api/ApiDisabled.php', 'ApiEditPage' => __DIR__ . '/includes/api/ApiEditPage.php', diff --git a/includes/api/ApiCSPReport.php b/includes/api/ApiCSPReport.php new file mode 100644 index 0000000..a8d51db --- /dev/null +++ b/includes/api/ApiCSPReport.php @@ -0,0 +1,230 @@ +<?php +/** + * Copyright © 2015 Brian Wolff + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +use MediaWiki\Logger\LoggerFactory; + +/** + * Api module to receive and log CSP violation reports + * + * @ingroup API + */ +class ApiCSPReport extends ApiBase { + + private $log; + + /** + * These reports should be small. Ignore super big reports out of paranoia + */ + const MAX_POST_SIZE = 8192; + + /** + * Logs a content-security-policy violation report from web browser. + */ + public function execute() { + $reportOnly = $this->getParameter( 'reportonly' ); + $logname = $reportOnly ? 'csp-report-only' : 'csp'; + $this->log = LoggerFactory::getInstance( $logname ); + $userAgent = $this->getRequest()->getHeader( 'user-agent' ); + + $this->verifyPostBodyOk(); + $report = $this->getReport(); + $flags = $this->getFlags( $report ); + + $warningText = $this->generateLogLine( $flags, $report ); + $this->logReport( $flags, $warningText, [ + // XXX Is it ok to put untrusted data into log?? + 'csp-report' => $report['csp-report'], + 'method' => __METHOD__, + 'user' => $this->getUser()->getName(), + 'user-agent' => $userAgent, + 'source' => $this->getParameter( 'source' ), + ] ); + $this->getResult()->addValue( null, $this->getModuleName(), 'success' ); + } + + /** + * Log CSP report, with a different severity depending on $flags + * @param $flags Array Flags for this report + * @param $logLine String text of log entry + * @param $context Array logging context + */ + private function logReport( $flags, $logLine, $context ) { + if ( in_array( 'false-positive', $flags ) ) { + // These reports probably don't matter much + $this->log->debug( $logLine, $context ); + } else { + // Normal report. + $this->log->warning( $logLine, $context ); + } + } + + /** + * Get extra notes about the report. + * + * @param $report Array The CSP report + * @return Array + */ + private function getFlags( $report ) { + $reportOnly = $this->getParameter( 'reportonly' ); + $userAgent = $this->getRequest()->getHeader( 'user-agent' ); + $source = $this->getParameter( 'source' ); + + $flags = []; + if ( $source !== 'internal' ) { + $flags[] = 'source=' . $source; + } + if ( $reportOnly ) { + $flags[] = 'report-only'; + } + if ( CSP::falsePositiveBrowser( $userAgent ) + && $report['document-uri'] === "'self'" + ) { + // False positive due to: + // https://bugzilla.mozilla.org/show_bug.cgi?id=1026520 + + $flags[] = 'false-positive'; + } + } + + /** + * Output an api error if post body is obviously not OK. + */ + private function verifyPostBodyOk() { + $req = $this->getRequest(); + $contentType = $req->getHeader( 'content-type' ); + if ( $contentType !== 'application/json' + && $contentType !=='application/csp-report' + ) { + $this->error( 'wrongformat', __METHOD__ ); + } + if ( $req->getHeader( 'content-length' ) > self::MAX_POST_SIZE ) { + $this->error( 'toobig', __METHOD__ ); + } + } + + /** + * Get the report from post body and turn into associative array. + * + * @return Array + */ + private function getReport() { + $postBody = $this->getRequest()->getRawInput(); + if ( strlen( $postBody ) > self::MAX_POST_SIZE ) { + // paranoia, already checked content-length earlier. + $this->error( 'toobig', __METHOD__ ); + } + $status = FormatJson::parse( $postBody, FormatJson::FORCE_ASSOC ); + if ( !$status->isGood() ) { + list( $code, ) = $this->getErrorFromStatus( $status ); + $this->error( $code, __METHOD__ ); + } + + $report = $status->getValue(); + + if ( !isset( $report['csp-report'] ) ) { + $this->error( 'missingkey', __METHOD__ ); + } + return $report['csp-report']; + } + + /** + * Get text of log line. + * + * @param $flags Array of additional markers for this report + * @param $report Array the csp report + * @return String Text to put in log + */ + private function generateLogLine( $flags, $report ) { + $flagText = ''; + if ( count( $flags ) !== 0 ) { + $flagText = '[' . implode( $flags, ', ' ) . ']'; + } + + $blockedFile = isset( $report['blocked-uri'] ) ? $report['blocked-uri'] : 'n/a'; + $page = isset( $report['document-uri'] ) ? $report['document-uri'] : 'n/a'; + $line = isset( $report['line-number'] ) ? ':' . $report['line-number'] : ''; + $warningText = $flagText . + ' Received CSP report: <' . $blockedFile . + '> blocked from being loaded on <' . $page . '>' . $line; + return $warningText; + } + + /** + * Stop processing the request, and output/log an error + * + * @param $code String error code + * @param $method String method that made error + */ + private function error( $code, $method ) { + $this->log->info( 'Error reading CSP report: ' . $code, [ + 'method' => $method, + 'user-agent' => $this->getRequest()->getHeader( 'user-agent' ) + ] ); + // 500 so it shows up in browser's developer console. + $this->dieUsage( "Error processing CSP report: $code", 'cspreport-' . $code, 500 ); + } + + public function mustBePosted() { + return true; + } + + public function isWriteMode() { + return false; + } + + public function getAllowedParams() { + return [ + 'reportonly' => [ + ApiBase::PARAM_TYPE => 'boolean', + ApiBase::PARAM_DFLT => false + ], + 'source' => [ + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_DFLT => 'internal', + ApiBase::PARAM_REQUIRED => false + ] + ]; + } + + /** + * Mark as internal. This isn't meant to be used by normal api users + */ + public function isInternal() { + return true; + } + + /** + * Even if you don't have read rights, we still want your report. + */ + public function isReadMode() { + return false; + } + + /** + * Doesn't touch db, so max lag should be rather irrelavent. + * + * Also, this makes sure that reports aren't lost during lag events. + */ + public function shouldCheckMaxLag() { + return false; + } +} diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index c7c48b3..857522b 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -65,6 +65,7 @@ 'compare' => 'ApiComparePages', 'tokens' => 'ApiTokens', 'checktoken' => 'ApiCheckToken', + 'cspreport' => 'ApiCSPReport', // Write modules 'purge' => 'ApiPurge', diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json index 6ea643a..911a9fd 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -64,7 +64,9 @@ "apihelp-createaccount-param-language": "Language code to set as default for the user (optional, defaults to content language).", "apihelp-createaccount-example-pass": "Create user <kbd>testuser</kbd> with password <kbd>test123</kbd>.", "apihelp-createaccount-example-mail": "Create user <kbd>testmailuser</kbd> and email a randomly-generated password.", - + "apihelp-cspreport-description": "Used by browsers to report violations of the Content Security Policy. This module should never be used, except when used automatically by a CSP compliant web browser.", + "apihelp-cspreport-param-reportonly": "Mark as being a report from a monitoring policy, not an enforced policy", + "apihelp-cspreport-param-source": "What generated the CSP header that triggered this report", "apihelp-delete-description": "Delete a page.", "apihelp-delete-param-title": "Title of the page to delete. Cannot be used together with <var>$1pageid</var>.", "apihelp-delete-param-pageid": "Page ID of the page to delete. Cannot be used together with <var>$1title</var>.", diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json index 2108b33..b747fe1 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -68,6 +68,9 @@ "apihelp-createaccount-param-language": "{{doc-apihelp-param|createaccount|language}}", "apihelp-createaccount-example-pass": "{{doc-apihelp-example|createaccount}}", "apihelp-createaccount-example-mail": "{{doc-apihelp-example|createaccount}}", + "apihelp-cspreport-description": "{{doc-apihelp-description|cspreport}}", + "apihelp-cspreport-param-reportonly": "{{doc-apihelp-param|cspreport|reportonly}}", + "apihelp-cspreport-param-source": "{{doc-apihelp-param|cspreport|source}}", "apihelp-delete-description": "{{doc-apihelp-description|delete}}", "apihelp-delete-param-title": "{{doc-apihelp-param|delete|title}}", "apihelp-delete-param-pageid": "{{doc-apihelp-param|delete|pageid}}", -- To view, visit https://gerrit.wikimedia.org/r/274022 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Id92126ca7707186757e77fe50cd336ff1acb8b3f Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/core Gerrit-Branch: master Gerrit-Owner: Brian Wolff <bawolff...@gmail.com> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits