Content-Security-Policy is a security feature (MDN Web Docs) in modern web browsers that restricts the kind of content that helps to protect against certain types of attacks, such as Cross-Site Scripting (XSS) attacks. Since my Home Assistant has significant access to my home network and is reasonably well-known, I wanted to take some steps to protect against malicious actors using XSS or other injection attacks to taking over my network. In addition, there have been a few different CVEs (HA Security Disclosures) in Home Assistant that allowed for XSS),
In this blog post, I walk through the steps I took to design a CSP header and fix bugs I found along the way.
CSP won’t protect against all attacks, but it’s one tool as part of a layered security system to protect your system. CSP works by explicitly identifying all sources of JavaScript, CSS, XHR requests, etc. and explicitly listing them in an HTTP response header sent to the browser.
My Stack
I run Home Assistant inside Kubernetes which is fronted by ingress-nginx. This is important because I attach the CSP header into the response using ingress-nginx. Home Assistant itself does not expose a config value to set this header in the http integration. Using Kubernetes isn’t required,
If you’re using Kubernetes like me, then go to the HA Ingress and start setting the header like below. Right now, the headers are defaults, but we’ll fill those in.
|
|
A vanilla nginx config will look like this:
|
|
Traefik can use this.
A basic header
To start, I opened up my browser dev tools, reloaded the page, navigated around, then noted down all the different URLs that were loaded. Then I went to a CSP Generator. Most things load from the same HA endpoint, but things like maps and HCAS will load from external origins which must be explicitly permitted.
The header will look something like this:
default-src
/Default Source - Set this to'self'
as a fallback grant if a request doesn’t match another directive.script-src
/Script Source - Set this to'self' 'unsafe-inline' 'unsafe-eval'
. This directive is the most important because it scripts are what make XSS attacks useful (more on the risks below.)style-src
/Style Source - Set this to'self' 'unsafe-inline'
image-src
/Image Source - Set this to'self' data: https://basemaps.cartocdn.com https://*.basemaps.cartocdn.com https://brands.home-assistant.io https://raw.githubusercontent.com
.cartocdn.com
is used for Home Assistant’s maps,brands.home-assistant.io
is used to load the integration icons, andraw.githubusercontent.com
is used in HACS to view images for repositories. Make sure to add any other URLs that you’ve identified in the dev tools For example, I use the hass-tryfi integration, so I needed to addhttps://barkinglabs-media.s3.amazonaws.com
to this header.connect-src
/Connnect Source - Set this to'self' data: https://brands.home-assistant.io https://raw.githubusercontent.com
. Interestingly, since HA uses a Service Worker the initial request to populate the Service Worker cache is considered a connect, so if an image isn’t loading, try adding it to theconnect-src
too.frame-ancestors
/Frame Ancestors - Set this to'none'
. Home Assistant shouldn’t be embedded in an iframe. Replaces the X-Frame-Options header, see CVE-2023-41897 and GHSA-935v-rmg9-44mw.report-uri
/Report URI - Optional. I use the Sentry integration and collect CSP reports to investigate issues using this. Example:https://sentry.example.com/api/12345/security/?sentry_key=abcdef
Add the CSP header to the request router config, reload it, then refresh the browser page with the dev tools open. If anything does not load correctly, update the CSP header as needed. Note, that if you using HACS to load custom integrations, the repository pages will be intentionally partially broken. HACS shows the README.md from the GitHub repository, and often times developers include images from other origins, such as img.shields.io
as “shield icons” that show things like below:
I intentionally didn’t add this for privacy reasons and they don’t add a lot of value. However, if you want to see them, just add https://img.shields.io
to your image-src
.
Analysis of my header
Home Assistant is intentionally quite flexible in what JS code it can run. Since you can load arbitrary cards through HACS which bring in their own HTML, CSS, JS, and developers sometimes use inline <script>foo();</script>
tags, I had to enable unsafe-inline
.
This is a common source of drive-by XSS attacks and would still leave me susceptible to things like CVE-2017-16782 and CVE-2023-41896 since you could still inject JavaScript through incorrectly escaped content. Unescaped content isn’t the only source of XSS attacks though.
The next layer of defense is what I set for image-src
, style-src,
and connect-src
which are the mechanisms to then exfiltrate data to a malicious actor. Any origin listed there should be considered relevant trusted. Don’t add *
or anything.
Problems with iframes in Firefox
I’m using the weather-radar-card to show precipitation clouds. This required adding https://tilecache.rainviewer.com
to my image-src
directive. However after setting the CSP header, it would only show the first time I loaded the page, but not if it was in the cache. I had to hard refresh (shift-F5) to get it to load. Otherwise, it’d show an empty white box, like below:
It took me awhile to figure this out, because it only seemed to happen in Firefox, but looking at the HTML DOM, it looked like this:
|
|
According to the spec, iframe srcdocs are supposed to inherit the parent’s Origin and thus use the same header we set above, but for some reason, it was enforcing the header, but not using the same Origin. Even though we set 'self'
, it was ignoring that and thought it was different. To fix this, I added ha.example.com
to my script-src
and image-src
.
After that, the card started working:
Strict Login Header
Just for fun (this is optional), I added a second restricted header just for the login page to reduce my exposure even more.
|
|
All Together
Now, putting it all together, my Content-Security-Policy looks like:
|
|
And inserting that into my K8s Ingress:
|
|
Conclusion
Content-Security-Policy is a useful security feature that mitigates many (but not all) types of XSS attacks in the browser. It can be tricky to set in Home Assistant because HA uses a variety of different features like service workers, iframes, and allows arbitrary JS to be loaded by design through custom cards. However, this makes it more important to add a CSP header to protect as much as possible.