A Content Security Policy (or CSP) is a set of rules which website owners can implement to approve origins of content that web browsers should or should not be allowed to load on their websites. For example, a CSP can be used to prevent a website from loading resources such as images, frames, or scripts from 3rd party websites.
The purpose of a CSP is to prevent cross-site scripting (XSS), clickjacking and other code injection attacks resulting from execution of malicious content in the trusted web page context.
CSP rules are set and sent by a website through a “Content-Security-Policy” HTTP header.
A typical Content-Security-Policy HTTP header may look something like this:
Content-Security-Policy "default-src 'none'; script-src 'self'; img-src 'self'; style-src 'self'; report-uri https://your_domain/csp.php;"
As part of this header, a website owner can optionally specify a “report-uri
” directive. If present, the “report-uri
” directive instructs web browsers to transmit CSP violations to a specified URL. This URL can then process/store these CSP violation reports.
The ability to log CSP violations is extremely useful, as it allows a website owner to ensure that their CSP rules are behaving as intended, and not blocking anything they shouldn’t be. A website owner may choose to have these violations logged to their own server, or use a 3rd party service, such as report-uri.io, to remotely record violations and present the data in easy to read graphical format.
However, specifying a “report-uri
” directive can have some unexpected and very undesired consequences which in some cases can not only break your website’s PCI DSS (Payment Card Industry Data Security Standard) compliance, but also leak sensitive unencrypted full credit card details to 3rd party CSP violation logging websites, like report-uri.io.
I’ll come back to this a little later, but first I want to mention Stripe. Stripe is a popular card payment processing service, similar to services such as PayPal. However, unlike PayPal (where the customer is transferred to a “hosted” payment page on the PayPal website in order to complete their purchase) Stripe allows website owners to accept payments directly from their own websites.
Website owners can still comply with PCI DSS when implementing Stripe as their payment processor, as even though the customer makes the payment on the owner’s own website, no sensitive data is actually stored or indeed passes through the owner’s site – all data and transactions and processed by Stripes servers.
In Stripe’s own words: “Stripe.js meets these [PCI DSS] criteria by performing all transmission of sensitive cardholder data within an iframe served off of a stripe.com domain controlled by Stripe… If you’re using Stripe.js by including it off our domain, and not hosting it yourself locally … you do not need to change anything [to be compliant]“
Now, I was recently asked to debug a website where Stripe was implemented as the preferred payment option, after reports that customers had begun seeing messages that “A network error has occurred, and you have not been charged. Please try again” when attempting to complete their payments via Stripe.
After working with Stripe’s excellent and very responsive support team, we were able to trace the issue back to the website’s CSP.
According to Stripe’s information on CSP settings, the following directives are required in order for Stripe to function:
connect-src https://api.stripe.com
frame-src https://js.stripe.com
script-src https://js.stripe.com
These directives had been previously successfully implemented on the website in question, however, it transpired that the website’s owner had recently replaced the “frame-src
” directive (because it’s technically deprecated) with the “child-src
” directive (the replacement for the now deprecated “frame-src
” directive)
With the “child-src
” directive being used in place of the deprecated “frame-src
” directive, and with logging enabled (via the report-uri
directive), I observed a number of occurrences of the following two CSP violations:
"blocked-uri:"https://js.stripe.com/v2/channel.html?stripe_xdm_e=https%3A%2F%2F[redacted]&stripe_xdm_c=default230622&stripe_xdm_p=1"
"violated-directive":"default-src"
"blocked-uri":"https://api.stripe.com/v1/tokens?..."
"violated-directive":"script-src"
So it appeared that in addition to the existing connect-src
, child-src
, and script-src
CSP directives for Stripe, api.stripe.com needed to be added to script-src
, and js.stripe.com needed to be added to default-src
.
However, more worryingly, I noticed that present in the logging of the violated script-src
directive was the card holder’s name, card number, expiry date, and CVC number – all unencrypted in plain text!
"blocked-uri":"https://api.stripe.com/v1/tokens?card[name]=XXXX+XXXX&card[number]=XXXX+XXXX+XXXX+XXXX&card[cvc]=XXX&card[exp_month]=XX&card[exp_year]=XXXX&key=pk_live_XXXXXXXXXXXXXXXXXXXXXXXX&payment_user_agent=stripe.js%2F699b119&callback=sjsonp1452064784723&_method=POST&_accept_language=en-US"
As CSP logging was enabled, the website was actually – unbeknownst to the site’s owner (who was under the assumption that no sensitive card data passed through his website) – storing in plain text the full credit card details for everyone who had attempted to complete a purchase through their website, as this information was present in the query string of a blocked resource. Had a 3rd party report-uri logging service been used, instead of just “local” logging to the site owner’s own server, then customer’s card details would have been transmitted to and then stored by a remote 3rd party server!
Implications
Obviously, the implications of this are numerous, but include:
- A website believing they are compliant by not storing card details, may unwittingly be storing unencrypted card details on their servers via CSP logging
- A website believing they are compliant by not storing card details, may unwittingly be sending unencrypted card details to remote servers over unsecure http (as the report-uri directive doesn’t enforce https)
- If a malicious attacker was able to modify/tamper with a site’s “Content-Security-Policy” HTTP header they could easily steel customers card details without the customer – or even website owner – being aware!
I immediately alerted Stripe’s security team to the issue.
Stripe’s response:
“In Jan/Feb of 2015 (about a year ago), we started using a channel iframe to make requests to our API, due to new PCI regulations in PCI DSS 3. However, we knew we’d have lots of site that were incompatible with the change (which was required to happen), so we put a few fallbacks in place.
We first try the correct path, which is to postmessage the tokenization details to the channel iframe, and make a CORS POST request to our API from there. If that, and a few other things fail, we ultimately try to make the payment via the old method that we previously had used until 2015, which was via JSONP, direct to our API. We knew it’d be a slow upgrade for lots of businesses, but since their sites already worked with this old method, it was a pretty good bet it’d be a good last fallback (the data that we log supports this theory pretty well).
That’s what these two violations are from. First, the channel file trying to exist, and then the fallback to the pre-dss3 code via the blocked script-src to the api.
The reason you’re getting the extra “script-src” request to api.stripe.com is because the dss3 channel method was blocked and we fell back to a different request type. So if that’s unblocked, this should work correctly, and without any fallbacks that would require more “script-src” domains.
Our JS loads from js.stripe.com, we inject an iframe only ever at js.stripe.com, and non-pci related requests to our api avoid the iframe method and use CORS directly from your page, which is the reason for the “connect-src” directive.
It’s pretty rare that someone has CSP setup *and* falls back to (and violates) the jsonp direct-post mode from above, but it’s still something that I’d rather not happen. However, information after a # is not sent to the server, so we wouldn’t be able to use that method.
It’s been very rare to see someone in your exact predicament, having implemented CSP, but blocking both the current and fallback methods. This is primarily because folks with CSP turned on before the change last year had the jsonp ” script-src https://api.stripe.com ” directive on, and the fallback violation didn’t leak anything… Or, they implemented CSP after the change, at which time they’d notice the violations in testing and things would break before they ever got them out of the door.
The only real fix to this specific problem is to stop falling back to the old jsonp method, since that’d stop any requests w/ sensitive info from occurring inside a non-stripe frame. However, we’d break a ton of sites. The rough and unconfirmed plan is to eventually put out a version 3 of the script that doesn’t use the fallback, that way the upgrade would be opt-in, and you’d be able to resolve any CSP/frame-busting/postmessage interference that existed as you upgraded.”
Given the “very rare” likelihood of this specific issue being widely encountered, Stripe do not consider this to be a serious vulnerability, however, they did rewarded my disclosure and will be working to improve their service so that CSP misconfigurations won’t lead to sensitive data to be exposed.
In Conclusion
So what lessons can webmasters learn from this?
- Even though the “
frame-src
” CSP directive is officially deprecated in place of “child-src
“, many web browser’s haven’t yet caught up, and still rely on “frame-src
” rather than “child-src
” – therefore, for the time being, be wary of removing a “frame-src
” directive from your CSP. - Consider your use of the “
report-uri
” CSP directive and the implications for the transmission and storage of CSP violation reports containing potentially very sensitive and unencrypted data – even over https. - Whilst services like report-url.io serve a legitimate and useful purpose, by configuring your site’s CSP to automatically report violations to a remote 3rd party service, you no longer retain exclusive control over the CSP violation data being stored/used by the 3rd party.
- If you do utilize the “
report-uri
” directive, ensure that you’re sending reports over secure https (this will at least ensure that the transmission itself is secure, even if the data being sent isn’t) - If you are logging CSP violations to your own server, ensure the data it is stored securely, isn’t accessible to anyone else, and is only stored for as long as is needed to allow you to debug your CSP.
- If you are logging CSP violations to your own server, consider not storing any query parameter data contained in blocked URLS
Disclosure Timeline (All times UTC)
- 28th January 2016 11:10 – Responsible disclosure to Stripe
- 1st February 2016 23:08 – Stripe reward responsible disclosure
- 19th April 2016 – Public disclosure
UPDATE – 20th April 2016
As security consultant (and man behind the man behind report-uri.io) Scott Helmpoints out, web browsers should not be sending query parameters with CSP reports, if the blocked resource resides on a different domain:
[Source]
It appears that when this issue first came to light back in January, Firefox 44 at least WAS including query parameter data in CSP reports, in direct violation of CSP reporting rules.
Testing again today and comparing the data logged for the same transaction in a CSP report sent by both Firefox 44 and Firefox 47, shows a clear difference: