Content Security Policy configuration (in Phoenix)
This post is about setting a Content Security Policy, specifically in Phoenix. But being about a response header it is probably more widely applicable.
The other day I ran sobelow to check the security of a project I’ve been working on. It was happy enough, apart from suggesting that I added a Content Security Policy. Sobelow’s documentations says
When it comes to CSP, just about any policy is better than none. If you are unsure about which policy to use, the following mitigates most typical XSS vectors:
plug :put_secure_browser_headers,
%{"content-security-policy" => "default-src 'self'"}
Fair enough, I thought, and opened that can of worms. Firing up by development environment and pointing Safari to http://localhost:400 , I found it did not work. Opening up the browser console I saw
EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval'
is not an allowed source of script in the following Content Security Policy
directive: "default-src 'self'".
This seemed to be an issue with with Webpack and development mode. One option1 would be to not have a Content Security Policy in dev
mode, but it seems better to keep dev
fairly close to prod
, so we can work out issues on our own machines first. Only in dev
lets get unstuck and allow unsafe-eval
.
@content_security_policy (case Mix.env do
:prod -> "default-src 'self'"
_ -> "default-src 'self' 'unsafe-eval'"
end)
pipeline :browser do
# other browser plugs here
plug(:put_secure_browser_headers, %{"content-security-policy" => @content_security_policy})
end
Now Safari gives us:
Refused to connect to ws://localhost:4000/live/websocket? ...
because it appears in neither the connect-src directive nor the
default-src directive of the Content Security Policy
Oh no, our LiveView WebSocket is being blocked by Safari. (Chrome does not block the WebSocket, which does emphasise the importance of cross-browser testing.)
Checking the documentation it looks like we need to add an explict entry in a connect-src
directive. First let’s find our server host from configuration. (For the purposes of this post I’ve added a Content Security Policy to the example Starjumps application.)
# in the router.ex
@host :starjumps
|> Application.fetch_env!(StarjumpsWeb.Endpoint)
|> Keyword.fetch!(:url)
|> Keyword.fetch!(:host)
Now we can allow WebSockets
@content_security_policy (case Mix.env do
:prod -> "default-src 'self';connect-src wss://#{@host};"
_ -> "default-src 'self' 'unsafe-eval';connect-src ws://#{@host}:400"
end)
In both the example Starjumps project and my actual project, I am using URL.createObjectURL()
in Javascript to load images (from a WebSocket) into a HTML img
element. (I wrote about all that here.)
This now leads to this kind of browser error:
[Error] Refused to load blob:http://localhost:4000/e672fb22-5944-49b8-82d6-fb128d793a80
because it appears in neither the img-src directive nor the default-src
directive of the Content Security Policy.
We can solve that with adding blob:
(with the colon) to an img-src
directive.
@content_security_policy (case Mix.env do
:prod -> "default-src 'self';connect-src wss://#{@host};img-src 'self' blob:;"
_ -> "default-src 'self' 'unsafe-eval';connect-src ws://#{@host}:*;img-src 'self' blob:;"
end)
Note that we also need to add self
to img-src
if we want to be able to load normal images.
And that is us done. Unless you want to use (say) Phoenix’s Live Dashboard.
Urgh, that’s not great. Our browser error console is telling us
Refused to apply a stylesheet because its hash, its nonce, or 'unsafe-inline'
appears in neither the style-src directive nor the default-src directive of
the Content Security Policy.
Refused to load data:image/png;base64, iVBORw0KGg... because it does not appear
in the img-src directive of the Content Security Policy.
Refused to execute a script because its hash, its nonce, or 'unsafe-inline'
appears in neither the script-src directive nor the default-src
directive of the Content Security Policy.
Assuming we only want to load the dashbaord in dev
mode2 then we can solve the issues with unsave-inline
and allowing data:
as well as blob for image:
.
@content_security_policy (case Mix.env do
:prod -> "default-src 'self';connect-src wss://#{@host};img-src 'self' blob:;"
_ -> "default-src 'self' 'unsafe-eval' 'unsafe-inline';" <>
"connect-src ws://#{@host}:*;" <>
"img-src 'self' blob: data:;" <>
end)
We also then run into a font issue
Refused to load data:font/woff2;base64,d09GMgABAAAA ....
because it appears in neither the font-src directive nor the default-src
directive of the Content Security Policy.
Solveble with
@content_security_policy (case Mix.env do
:prod -> "default-src 'self';connect-src wss://#{@host};img-src 'self' blob:;"
_ -> "default-src 'self' 'unsafe-eval' 'unsafe-inline';" <>
"connect-src ws://#{@host}:*;" <>
"img-src 'self' blob: data:;"
"font-src data:;"
end)
Now we’re dashboarding:
This gets us working with a reasonable and working Content Security Policy. In a larger app you may be loading resources from a CDN or other places and will need to keep the policy up to date.
Useful links
Updates
2020-03-06
Updated from this comment in Elixir Forum: removed links to https://content-security-policy.com, replacing the the MDN version where appropriate
Also, while I’m here, the Live View Dasbhaord does now support nonces; I shall look into this when I’ve a bit of time. Also plug_content_security_policy
may be worth some investigaation.
-
Another option would be to ignore the advice from Sobelow, and not have such a policy at all. I aver that it is best to not ignore advice from security experts. A Content Security Policy may be a pain to set up and maintain but working through these issues helps us be able to put in place “Defence in Depth” against naughty script kiddies. Think about how the British Airways Magecart hack would have gone if browsers were refusing AJAX requests to baways.com ↩
-
There’s a good chance I will want a protected version of the Dashboard in production; I’ll look into the
unsafe-inline
, which does not seem like a great production setting and report back at some point. I expect the clue is in the phrase “its hash, its nonce, or ‘unsafe-inline’ appears in neither” ↩