Possible to move inline scripts to a .js file for easier CSP implementations?
Not sure if this is even possible, but I thought it is worth to ask.
When calling shinyjs::useShinyjs, many functions are injected inline in the UI. This is a problem when implementing a strict CSP for a Shiny web application, since inline scripts are blocked by default. Would it be possible that these functions are loaded by calling a script file in the package inst folder instead of injecting them inline? Maybe using a variant of the inject.js script that was previously present in the package, to create the needed calls dynamically?
May be related to #101
On a sidenote: I suspect that the documentation on how to include shinyjs using a HTML template is outdated, since the function inject.js is not included in the package anymore: https://github.com/daattali/shinyjs/blob/0648faa0dc985a5be55bbc745fe042056a0ecfc3/vignettes/shinyjs-usage.Rmd#L167
Thanks for the sidenote, I'll look into that.
Regarding the CSP issue: I have not run into this problem before, but you're right it should be addressed. Loading the code via a script is definitely an option. I based on 1 mint of research it seems that perhaps it's possible to include a nonce in the script tag to allow it. Are you familiar with that? Passing a nonce parameter to useShinyjs could be another possible solution, what do you think?
Thanks for the quick response and taking the request into consideration.
Setting an nonce is also possible but, as far as my knowledge goes, setting it securely is not trivial. It needs to be set both within the Content-Security-Policy http header in the running web server and in every (inline) script. It needs to be a random string, that refreshes each time the web page refreshes, otherwise an attacker can just add the same nonce to an injected script and it will run without problems.
I think by far the best solution (and easiest at least for Shiny app developers and IT security departments) would be to place the scripts in an external file. Then no nonce or hash is needed. If this is not possible, it would indeed still be nice if we can set an nonce in every script tag.
The third option is to create a hash of the exact inline script, which can be added to the CSP. Downside is that this is a lot of manual work for the developer, and hard to maintain.
I've started looking into this, and I discovered that applying strict CSP policies doesn't even allow me to run a barebones shiny app. This line https://github.com/rstudio/shiny/blob/83219e3551c9c517b8d2a5f61ba41c8e8a53dcd9/srcts/src/shiny/bind.ts#L371 in shiny violates the unsafe eval rule.
What policies are you using that allow you to generally develop useful shiny apps that don't run into issues with other packages?
I tried the following shiny app to start troubleshooting:
library(shiny)
ui <- fluidPage(
tags$head(
tags$meta(`http-equiv`="Content-Security-Policy", content="script-src 'self';")
)
)
server <- function(input, output, session) {}
shinyApp(ui, server)
And I got an error
bind.ts:371 Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'self'".
pointing to https://github.com/rstudio/shiny/blob/83219e3551c9c517b8d2a5f61ba41c8e8a53dcd9/srcts/src/shiny/bind.ts#L371
Regarding the sidenote from the original post:
On a sidenote: I suspect that the documentation on how to include shinyjs using a HTML template is outdated, since the function inject.js is not included in the package anymore:
The documentation was just updated, thanks for pointing this out. The inject script was indeed an old script that is no longer needed, I have greatly simplified how shinyjs can be used and it can just be called normally in HTML templates
I've started looking into this, and I discovered that applying strict CSP policies doesn't even allow me to run a barebones shiny app. This line https://github.com/rstudio/shiny/blob/83219e3551c9c517b8d2a5f61ba41c8e8a53dcd9/srcts/src/shiny/bind.ts#L371 in shiny violates the unsafe eval rule.
What policies are you using that allow you to generally develop useful shiny apps that don't run into issues with other packages?
I tried the following shiny app to start troubleshooting:
library(shiny)
ui <- fluidPage( tags$head( tags$meta(
http-equiv="Content-Security-Policy", content="script-src 'self';") ) )server <- function(input, output, session) {}
shinyApp(ui, server)
And I got an error
bind.ts:371 Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'self'".pointing to https://github.com/rstudio/shiny/blob/83219e3551c9c517b8d2a5f61ba41c8e8a53dcd9/srcts/src/shiny/bind.ts#L371
I wish I had an answer here but unfortunately, I don't. Previously, I indeed tried to create a working and secure CSP for a validated application on our production server, but we got more than 150 violations. We were able to solve some of these by using a hash, but there were many remaining, and we postponed work on it. I opened an issue here, and also in the bslib package repository. I never tried a minimal application as you did.
I tried to implement a hash for the issue in the minimal app that you provided, but apparently, that is not possible. I will definitely look into this further, because ideally we would like to have this solved in our company. I think it is done before and thus possible (see here for example). My plan is to use Docker images to simulate deployment of a simple Shiny application on a (nginx) server and then try out several CSP configurations on that server. Then we can also use the report-only policy to see all violations and hopefully rapidly iterate.
If I gather more useful information on this topic, I will definitely share it with you. But it will probably take time before I get there.
@daattali my progress so far is summarized below (also for myself). It is not an easy problem to solve.
I simulated deployment of a Shiny application on a production server using a stack of several Docker images for testing a working CSP. You can find the entire test stack here, and can try it out if you have Docker installed. I am using a Shiny application that I developed instead of the minimal example above, to get a better overview of all CSP violations. The CSP that I created can be found here and is based on hashes of scripts that are allowed.
Couple of points here:
- I focused on making
script-srcworking, and setstyle-srcset as unsafe-inline: too many violations here unfortunately. -
unsafe-evalforscript-srcseems to be unavoidable unless these violations are addressed in the source code. See here for the ones that I found.
For the shinyjs package, I found the following script line that violates the unsafe-eval, probably some code is executed from a string?:
shinyjs-default-funcs.js:701 | runjs : function (params) {var defaultParams = {code : null } params = shinyjs.getParams(params, defaultParams); eval(params.code); }
Do you know if that can be re-written? Or would that be difficult to do?
Regarding the other unsafe-eval violations that I found: I think will open an issue regarding these in the respective packages' repositories soon. Not sure if it is feasible to adjust, but it would be worth a try to ask.
nonce-based CSP
An issue with the hashes is that they are probably hard to maintain. Some hashes are for scripts that are created dynamically.
An alternative for using hashes would be to use an nonce, in combination with strict-dynamic. This would be the basic setup for it. the idea is that scripts with a valid nonce are allowed to create new scripts without this nonce. This has potential, but you would need to use a custom shiny app template, in which javascript dependencies are hard coded with a placeholder nonce value in the script tag that the nginx server can detect properly. I will explore this further in the future. Note that this will still not solve the unsafe-eval issue mentioned above, it will be just easier to maintain (I think).