Content Security Policy in Magento : inline scripts and nonce
Introduction to Content Security Policy
Content Security Policy (CSP) is a HTTP header that promises to enhance security and prevent cartjack type of attacks or atleast make them difficult.
A popular attack vector is for malicious javascript code to be added to your webpage to skim credit card or other user data as a visitor enters the data. The malicious code is added by either attacking a vulnerability in the website, adding javascript to a header, or by attacking a 3rd party service, compromising a javascript that is part of the page.
The CSP header identifies the domains from which javascripts are loaded or provide signatures of legitimate scripts. It is a browser feature to not load scripts from other than whitelisted domains or when signatures do not match.
Inline javascripts
It is quite common to run javascripts inline - using a <script> html tag.
If a website is compromised - say the HTTP head that gets injected to the page from the database is compromised, critical user data including credit cards can be leaked.
It is essential to not only protect the site from getting injected with 3rd party js links, but also from being injected with javascript.
CSP's script-src can take a 'nonce-<id>' value that will only load javascript that are marked with this nonce attribute with the same value in the script tag.
Varnish caching and the nonce challenge
nonce stands for "number only once". By definition it should be unique value per page load. If Magento were to generate nonce values, it would not be possible these in varnish as a full page cache.
It is obvious that in order to generate the nonce value, it should be done on the "edge" - either in varnish as it returns a body, or in nginx which may be at the edge before varnish for TLS termination.
Using nginx subfilter command and a header filter javascript (using the njs module), luroConnect replaces a placeholder nonce value in Magento with a valid nonce value.
Securing the placeholder
The placeholder has to be a secret between Magento and the nginx that replaces the placeholder. Our nginx changes expect Magento to send this in a response header "nonce_unique" which is used for substitution.
Note : Writing a search and replace before Magento sends the http body is NOT considered secure. Recent COMICSTRING compromised websites may insert a script in the header and get legitmized with this code.
/etc/nginx/lc/njs/csp_nonce.js :
function csp_nonce_header_replace(r) { var unique_csp_nonce_placeholder = r.headersOut['nonce_unique']; if(!unique_csp_nonce_placeholder) return; var csp = r.headersOut['Content-Security-Policy']; if(!csp) return; csp = csp.replaceAll(unique_csp_nonce_placeholder, r.variables.ssl_session_id); r.headersOut['Content-Security-Policy'] = csp; delete r.headersOut['nonce_unique']; }; export default csp_nonce_header_replace;
in the nginx configuration that terminates TLS and proxies to varnish
location / { ## replace the placeholder in the CSP header js_header_filter csp_nonce_header_replace; ## replace the placeholder in the body sub_filter_once off; sub_filter_types *; sub_filter $upstream_http_nonce_unique $ssl_session_id; ## Proxy to varnish proxy_pass http://M2_lbbackend; proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-Port 443; proxy_set_header Host $host; ## in order for nonce replacement to work, varnish cannot use gzip proxy_set_header Accept-Encoding 'identity;q=0'; }
in nginx.conf
- load the js plugin
load_module modules/ngx_http_js_module.so;
- in http section add
http{ ... ## use njs to add servertime header js_path /etc/nginx/lc/njs/; js_import csp_nonce_header_replace from csp_nonce.js; ... }