30use Psr\Log\LoggerInterface;
40 private LoggerInterface $log;
45 private const MAX_POST_SIZE = 8192;
54 parent::__construct( $main, $action );
55 $this->urlUtils = $urlUtils;
63 $logname = $reportOnly ?
'csp-report-only' :
'csp';
64 $this->log = LoggerFactory::getInstance( $logname );
65 $userAgent = $this->
getRequest()->getHeader(
'user-agent' );
67 $this->verifyPostBodyOk();
68 $report = $this->getReport();
69 $flags = $this->getFlags( $report, $userAgent );
71 $warningText = $this->generateLogLine( $flags, $report );
72 $this->logReport( $flags, $warningText, [
74 'csp-report' => $report,
75 'method' => __METHOD__,
76 'user_id' => $this->
getUser()->getId() ?:
'logged-out',
77 'user-agent' => $userAgent,
89 private function logReport( $flags, $logLine, $context ) {
90 if ( in_array(
'false-positive', $flags ) ) {
92 $this->log->debug( $logLine, $context );
95 $this->log->warning( $logLine, $context );
106 private function getFlags( $report, $userAgent ) {
112 if (
$source !==
'internal' ) {
113 $flags[] =
'source=' .
$source;
116 $flags[] =
'report-only';
122 $report[
'blocked-uri'] ===
"self"
125 isset( $report[
'blocked-uri'] ) &&
126 $this->matchUrlPattern( $report[
'blocked-uri'], $falsePositives )
129 isset( $report[
'source-file'] ) &&
130 $this->matchUrlPattern( $report[
'source-file'], $falsePositives )
136 $flags[] =
'false-positive';
146 private function matchUrlPattern(
$url, array $patterns ) {
147 if ( isset( $patterns[
$url ] ) ) {
151 $bits = $this->urlUtils->parse(
$url );
156 unset( $bits[
'user'], $bits[
'pass'], $bits[
'query'], $bits[
'fragment'] );
158 $serverUrl = UrlUtils::assemble( $bits );
159 if ( isset( $patterns[$serverUrl] ) ) {
164 foreach ( $patterns as $pattern => $val ) {
168 if ( str_ends_with( $pattern,
'/' ) && str_starts_with(
$url, $pattern ) ) {
181 private function verifyPostBodyOk() {
183 $contentType = $req->getHeader(
'content-type' );
184 if ( $contentType !==
'application/json'
185 && $contentType !==
'application/csp-report'
187 $this->error(
'wrongformat', __METHOD__ );
189 if ( $req->getHeader(
'content-length' ) > self::MAX_POST_SIZE ) {
190 $this->error(
'toobig', __METHOD__ );
199 private function getReport() {
200 $postBody = $this->
getRequest()->getRawInput();
201 if ( strlen( $postBody ) > self::MAX_POST_SIZE ) {
203 $this->error(
'toobig', __METHOD__ );
205 $status = FormatJson::parse( $postBody, FormatJson::FORCE_ASSOC );
206 if ( !$status->isGood() ) {
207 $msg = $status->getMessages()[0]->getKey();
208 $this->error( $msg, __METHOD__ );
211 $report = $status->getValue();
213 if ( !isset( $report[
'csp-report'] ) ) {
214 $this->error(
'missingkey', __METHOD__ );
216 return $report[
'csp-report'];
226 private function generateLogLine( $flags, $report ) {
229 $flagText =
'[' . implode(
', ', $flags ) .
']';
232 $blockedOrigin = isset( $report[
'blocked-uri'] )
233 ? $this->originFromUrl( $report[
'blocked-uri'] )
235 $page = $report[
'document-uri'] ??
'n/a';
236 $line = isset( $report[
'line-number'] )
237 ?
':' . $report[
'line-number']
240 ' Received CSP report: <' . $blockedOrigin .
'>' .
241 ' blocked from being loaded on <' . $page .
'>' . $line;
248 private function originFromUrl(
$url ) {
249 $bits = $this->urlUtils->parse(
$url ) ?? [];
250 unset( $bits[
'user'], $bits[
'pass'], $bits[
'query'], $bits[
'fragment'] );
253 return UrlUtils::assemble( $bits );
263 private function error( $code, $method ) {
264 $this->log->info(
'Error reading CSP report: ' . $code, [
266 'user-agent' => $this->
getRequest()->getHeader(
'user-agent' )
270 [
'apierror-csp-report',
wfEscapeWikiText( $code ) ],
'cspreport-' . $code, [], 400
277 ParamValidator::PARAM_TYPE =>
'boolean',
278 ParamValidator::PARAM_DEFAULT => false
281 ParamValidator::PARAM_TYPE =>
'string',
282 ParamValidator::PARAM_DEFAULT =>
'internal',
283 ParamValidator::PARAM_REQUIRED => false
320class_alias( ApiCSPReport::class,
'ApiCSPReport' );
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
This is the main API class, used for both external and internal processing.
A class containing constants representing the names of configuration variables.
const CSPFalsePositiveUrls
Name constant for the CSPFalsePositiveUrls setting, for use with Config::get()
Handle sending Content-Security-Policy headers.
static falsePositiveBrowser( $ua)
Does this browser give false positive reports?