Security Headers for Shiny Applications

[This article was first published on The Jumping Rivers Blog, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

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

 repos = c("",

There are only a couple of exported functions. The core function is check(). As an example, let’s use

# check returns an invisible data frame of results
## ── 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 for guidance.

Comments on

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, we actually got three status codes: 301, 301, and then the magical 200. This is fairly standard. What happens is that is actually the same as This redirects (code 301) to which redirects to

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 and 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 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.

Want to ensure that your application or dashboard follows the latest standards? You might benefit from our Shiny health check.

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 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. then you should set the Referrer Policy.

For, 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 into a browser, the URL automatically resolves to, 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 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).


This package is based on a package originally created by Bob Rudis – hdrs.

For updates and revisions to this article, see the original post

To leave a comment for the author, please follow the link and comment on their blog: The Jumping Rivers Blog. offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)