Daniela Matos de Carvalho

April 08, 2019

Security Trivia Series: Understanding CSP's Reporting

In our previous blogpost about Content Security Policy (CSP) we promised to have another one about reporting and mentioned report-uri. Turns out that report-uri was deprecated in Content Security Policy Level 3:

The report-uri directive is deprecated in favor of the new report-to directive

In this blogpost we will check how report-to works and the main differences from report-uri. We will also check out the new Reporting API!

Use case

Imagine that we have a page with three iframes that link to Mozilla's website and subdomains, e.g. the following simplified HTML snippet:

html
  head
    title= title
  body
    iframe(src="https://developer.mozilla.org")
    iframe(src="https://support.mozilla.org")
    iframe(src="https://mozilla.org")

How can we implement a CSP that allow these iframes to be loaded?

As we have seen in the previous blogpost we need to set the following CSP header value:

Content-Security-Policy: "default-src 'self'; frame-src https://mozilla.org https://*.mozilla.org;"

However, if some errors arise we might want to report them properly so we can act on them. Let's say that we only included Mozilla's subdomains and forgot to add https://mozilla.org to the allowed domains:

Content-Security-Policy: "default-src 'self'; frame-src https://*.mozilla.org;"

This should create an error. Let's learn how to report it!

BEFORE: Using report-uri (DEPRECATED!)

report-uri receives a endpoint, e.g

Content-Security-Policy: ... ; report-uri <report-endpoint>

Let's use a Node express.js server to test this. If we set the CSP header from the use case (where we expect to have an error) and use it on the /content route:

const express = require('express')
const app = express()

app.get('/content', (req, res) => {
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; frame-src https://*.mozilla.org; report-uri /report"
  )
  res.render('index')
})

app.listen(3000, () => console.log(`Example app listening on port ${port}!`))

Notice that we are telling the browser to send all report errors to /report route using report-uri. Let's add a /report route to check those errors:

app.post('/report', (req, res) => {
  console.log(req.body)
  return res.send('CSP fail: Report received')
})

This will fail for the last iframe (https://mozilla.org) as expected and we will get a POST on /report route with the following details:

{
  "csp-report": {
    "document-uri": "http://localhost:3000/content",
    "referrer": "",
    "violated-directive": "frame-src",
    "effective-directive": "frame-src",
    "original-policy": "default-src 'self'; frame-src https://*.mozilla.org; report-uri /report",
    "disposition": "enforce",
    "blocked-uri": "https://mozilla.org",
    "line-number": 1,
    "source-file": "http://localhost:3000/content",
    "status-code": 200,
    "script-sample": ""
  }
}

There is plenty of information here to be processed and used to help to solve browser issues that can appear. We know exactly what was the policy that reported the error and what failed.

report-uri is widely supported in most browsers. However, it is not using a consistent reporting framework and that is the main reason why the Reporting API was created and the new CSP key report-to was created.

Hint

Some browsers request the Content-Type as application/csp-report, others as application/json so in order to use it we have to setup some configurations on our express server:

const bodyParser = require('body-parser')

app.use(
  bodyParser.json({
    type: ['application/json', 'application/csp-report'],
  })
)

//...

Using report-to

report-to will replace report-uri soon, but currently it is only supported on Chrome (> version 70) and Android.

Instead of a URI, it receives a groupname:

Content-Security-Policy: ... ; report-to <groupname>

and it needs an additional header to be set, Report-to:

app.get('/content-report-to', (req, res) => {
  const reportTo = [
    {
      endpoints: [
        {
          url: 'https://localhost:3000/report', // or your report url
        },
      ],
      include_subdomains: true,
      group: 'csp-endpoint',
      max_age: 31536000, // one year
    },
    // ...
  ]

  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; frame-src https://*.mozilla.org; report-to csp-endpoint;"
  )
  res.setHeader('Report-to', reportTo.map(JSON.stringify).join(', '))

  res.render('index')
})

We can set multiple groups with multiple endpoints, which is useful because we can have the CSP group that will report CSP errors, the network group to report network errors, and so on.

Endpoints can have priorities (for failover) and weights (to distribute load) and a caching value (max_age) is required to be set.

The body received from report-to is also different from what we got with report-uri, but it tells essentially the same story:

[
  {
    "age": 16796,
    "body": {
      "blocked-uri": "https://mozilla.org",
      "disposition": "enforce",
      "document-uri": "https://localhost:3000/content-report-to",
      "effective-directive": "frame-src",
      "line-number": 1,
      "original-policy": "default-src 'self'; frame-src https://*.mozilla.org; report-to csp-endpoint;",
      "referrer": "",
      "script-sample": "",
      "sourceFile": "https://localhost:3000/content-report-to",
      "violated-directive": "frame-src"
    },
    "type": "csp",
    "url": "https://localhost:3000/content-report-to",
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36"
  }
]

Note: In this case type is csp but if we had a network error it would be nel and for a certificate error it would be hpkp. Check some examples in the sample reports from the Reporting API.

Hint

In order to use report-to the Content-Type has to be application/reports+json. We also need to setup our express server to serve HTTPS with a valid certificate, otherwise you can't test/use it.

The New Reporting API

The working draft explains it better than me but let's sum up some key points about the Reporting API:

  • It catches CSP errors, network errors or browser crashes (not only CSP errors!);
  • You can specify the endpoints that are going to receive the report, but it is done in a different way to what we are doing with report-uri;
  • Multiple endpoints can be specified but only one gets the data.
  • Priorities can be set per endpoint. If the one with higher priority fails it tries the next one (failover is supported);
  • The browser sends the report when there is nothing with higher priority to be done, so the report information might only be sent when the browser is idle.

There are some differences between report-uri and report-to, mainly related to how to specify it and the extra Report-to header but in the end the data received is similar. However, the Reporting API opens a new world of possibilities to grab all kinds of errors in the browser.

Want to know more? We will cover CSP's unsafe dynamic and nonce in another blogpost, follow us and stay tuned!

Originally published at blog.yld.io on April 8, 2019 by Daniela Matos de Carvalho (@sericaia on Twitter/Github)