Security Headers for Shiny Applications
Over the last few years, we have been performing audits on Posit set-ups, Shiny Applications and general R set-ups. One of our standard checks is to examine the server headers of a Shiny Server. Numerous websites do this check for you, but as we have an R-based/Quarto workflow, it was helpful to write a quick R package.
The package isn’t on CRAN, but is on the R-universe, so installing is straightforward
install.packages("serverHeaders",
repos = c("https://jumpingrivers.r-universe.dev",
"https://cloud.r-project.org"))
There are only a couple of exported functions. The core function is
check()
. As an example, let’s use
jumpingrivers.com.
# check returns an invisible data frame of results
serverHeaders::check("jumpingrivers.com")
##
## ── Checking Server ──
##
## ✔ Status code: 301 → 301 → 200
## ✔ SSL available
## ✔ SSL redirection successful: http -> https
## ✔ content-security-policy: Policy present but not parsed
## ✔ content-type: charset set
## ✔ permissions-policy: Value present but not verified
## ✔ referrer-policy: Acceptable setting found
## ✔ strict-transport-security: max_age = 365 days and is greater than 1 year
## ✔ x-content-type-options: Acceptable setting found
## ✔ x-frame-options: Acceptable setting found
The output to the console highlights key server headers that we are interested in. Of course, the definition of key is open to a lot of discussion, but we just used securityheaders.com for guidance.
Comments on jumpingrivers.com
Before we go further, it’s worth noting that a few years ago we decided to move from Wordpress to a static site generator - Hugo. We made this decision based on
- static sites are faster;
- static sites are easier to maintain;
- our previous site (WordPress) had to be constantly updated; dealing with numerous WordPress plugins always worried us - too much much for what is essentially a simple site.
One of the significant consequences of having a static site is the attack surface is significantly reduced.
Status codes
The first header is the status code. You’re probably familiar with a
status code of 200 indicating a successful request, and the dreaded 404
indicating a missing page. However, when we look at
jumpingrivers.com, we actually got
three status codes: 301
, 301
, and then the magical 200
. This is
fairly standard. What happens is that jumpingrivers.com is actually the
same as http://jumpingrivers.com
. This redirects (code 301
) to
https://jumpingrivers.com
which redirects to
https://www.jumpingrivers.com
A “bad” site, wouldn’t redirect to the “https” version.
Content security policy
We’ve covered Content Security Policies (or CSP) in previous blog posts. By being explicit about where external resources are loaded from, e.g. Javascript, it gives applications an extra layer of security.
For example, we can state that Javascript can only be loaded from jumpingrivers.com and example.com. Any JavaScript resource that is loaded from another site is automatically blocked by the browser. This safeguards against attacks such as cross-site scripting.
As jumpingrivers.com is a static site (we use Hugo), we don’t need to worry about cross-site scripting quite as much; it’s probably overkill. However, adding CSP to our site has highlighted exactly where we load external resources from and has encouraged us to keep resources local where possible.
Permissions policy
Permissions policy is similar to CSPs. Essentially, we specify the resources we would load on our website. For example, would we expect to use a camera or microphone? Again, for our static site this is overkill, but for a Shiny application it’s certainly something you should consider.
Referrer policy
When someone clicks a link on a site that takes them to another domain, the destination site receives information about where that user came from. This is how we get website analytics about our site traffic.
This isn’t too important for a site like jumpingrivers.com as we don’t have anything private on our site - everything is open to the world! However, if your URL contains potentially private information that you don’t want to be leaked, e.g. example.com/private-info then you should set the Referrer Policy.
For jumpingrivers.com, we set it to no-referrer-when-downgrade
. This
means when going from https to http, we won’t send the referrer header.
Other than that, we’ll send the full path.
Strict transport security
This header informs browsers that a site should only be accessed using HTTPS. Once set, any future visits will automatically convert http to https. Remember, from the status code, that typing jumpingrivers.com into a browser, the URL automatically resolves to http://jumpingrivers.com, so this (after the first visit) tightens up this issue.
X content type options
This stops a browser from trying to MIME-sniff the content type. This
should be set to x-content-type-options: nosniff
.
X frame options
This tells the browser whether or not you want to allow your site to be
framed. At jumpingrivers.com this is set to DENY
.
Shiny servers
The {serverHeaders} package checks common security related headers. There are certainly others, but the headers described above are certainly the important one. Many Shiny applications we work with contain sensitive data, help make business critical decisions and/or are fundamental to a business process. As such, spending some time securing your server is to be recommended (a little bit of understatement here).
Acknowlegements
This package is based on a package originally created by Bob Rudis - hdrs.