Want to share your content on R-bloggers? click here if you have a blog, or here if you don’t.
You can read the original post in its original format on Rtask website by ThinkR here: Security blind spots in Shiny: why your app is more vulnerable than you think
Buckle up, we’re talking about security…
While developing the Signature.py application, ThinkR recently faced a security issue – in Python – that deserves our full attention.
Versioned on Github, the Signature.py code is analyzed by Dependabot
. This is a robot that automatically scans projects for obsolete or vulnerable dependencies and can potentially suggest corrective measures.
Dependabot
alerted us to a security vulnerability related to the use of jinja2
, and this led us to a broader reflection: are we, as R and Shiny application developers, also concerned by these security issues?
The answer is a resounding YES.
What exactly are we talking about?
The impact of the problem identified in our Python application was potentially serious: unintentional code execution on the backend, potentially accessing the entire machine infrastructure. Fortunately, Signature.py only manipulates character strings, and updating jinja2
was able to fix the alert.
But what about the R ecosystem? Are Shiny developers also exposed to these types of vulnerabilities?
Code injection: an omnipresent danger
Code injection is a common security vulnerability that involves injecting malicious code into a page or application. This code is then executed, creating the security breach. There are several ways to inject code into an application, and Shiny is unfortunately not immune to these risks.
Vulnerabilities in Shiny
The good news
“Shiny is well-designed”! By default, inputs in Shiny return (almost) only character strings. As long as the input content is not evaluated, we are relatively safe.
For example, this simple application that simply displays the content entered by the user is secure:
library(shiny) ui <- fluidPage( textInput("template", "Enter something"), textOutput("result") ) server <- function(input, output, session) { output$result <- renderText({ paste("Your choice is", input$template) }) } shinyApp(ui, server)
The bad news
The input content can be evaluated and introduce a security flaw. As soon as we start evaluating the content of an input, risks appear.
The three major families of injection in web applications
1. XSS (Cross-Site Scripting): client-side
JavaScript or CSS code is injected and executed in another user’s browser. In Shiny, this can happen when we render HTML directly from user input without properly escaping it.
For example, this application is vulnerable:
library(shiny) ui <- fluidPage( textInput("text", "Enter something"), uiOutput("result") ) server <- function(input, output, session) { output$result <- renderUI({ HTML(input$text) }) } shinyApp(ui, server)
In the text input, a malicious user could enter <script>alert('hello')</script>
or even more dangerous code like:
<script> const fakeBtn = document.createElement("button"); fakeBtn.innerText = "🔐 Login required"; fakeBtn.style = "display:block; margin:10px 0; padding:10px; background-color:red; color:white;"; fakeBtn.onclick = function() { alert("Enter your credentials!"); }; document.body.prepend(fakeBtn); </script>
In the following vulnerable application, you can copy/paste the entirety of this code snippet into the application below, with the <script>
tags to see the threat.
You can write a first comment and then copy/paste the code to see the vulnerability.
No worries here, we don’t store the information in a database![]()
library(shiny) ui <- fluidPage( h2("💬 User comments"), textInput("pseudo", "Your username:", "Guest"), textAreaInput("message", "Your message:", rows = 3), actionButton("envoyer", "Send"), tags$hr(), h3("💬 Received comments:"), uiOutput("commentaires") ) server <- function(input, output, session) { commentaires <- reactiveVal( data.frame( pseudo = character(), message = character() ) ) observeEvent(input$envoyer, { new_entry <- data.frame( pseudo = input$pseudo, message = input$message ) commentaires(rbind(commentaires(), new_entry)) }) output$commentaires <- renderUI({ coms <- commentaires() if (nrow(coms) == 0) { return(NULL) } HTML(paste0( apply(coms, 1, function(row) { glue::glue("<p><strong>{row[['pseudo']]}</strong> : {row[['message']]}</p>") }), collapse = "n" )) }) } shinyApp(ui, server)
There are two main types of XSS:
- Reflected XSS: The malicious code is immediately executed.
- Stored XSS: The malicious code is stored in a database and executed when other users access the page.
The threat of Stored XSS is particularly concerning. In the example above, this malicious JavaScript code adds a fake login button that will appear for all future users of the application. Imagine the scenario: an attacker injects this code into your application, and all subsequent users see a red button requesting a login. They click on it, enter their credentials… and this information can be easily retrieved by the attacker.
2. Command Injection: server-side
The code is executed directly on the server. This can happen in Shiny when we directly evaluate the content of an input.
Consider this seemingly harmless application:
library(shiny) library(glue) ui <- fluidPage( textInput("template", "Enter something"), textOutput("result") ) server <- function(input, output, session) { output$result <- renderText({ glue(input$template) }) } shinyApp(ui, server)
This application is actually vulnerable because glue()
automatically evaluates what is between {}
. A malicious user could enter {system("ls /", intern = TRUE)}
or worse {system("rm -rf /")}
.
system()
is an R function that executes shell commands.
Whilesystem("ls /", intern = TRUE)
presents a relative risk as it only displays the contents of the root directory of your computer,{system("rm -rf /")}
is a potentially destructive command. This command will try to delete all files on the system! Handle with caution![]()
The use of glue()
seems harmless, yet glue::glue(input$template)
amounts to the same thing as eval(parse(text = input$template))
. The eval(parse(text = ...))
duo is an operation that will try to evaluate text. The developer’s use of these functions is clear: they are trying to evaluate text. However, the use of the glue()
function is more insidious here.
3. SQL Injection: database-side
SQL code is executed in the database. This can happen when we build SQL queries from user inputs without properly escaping them.
For example, this application is vulnerable:
library(shiny) library(DBI) library(RSQLite) con <- dbConnect(RSQLite::SQLite(), ":memory:") dbExecute( conn = con, "CREATE TABLE users ( id INTEGER, name TEXT )" ) dbExecute( conn = con, "INSERT INTO users (id, name) VALUES (1, 'Arthur'), (2, 'Adrien'), (3, 'Lucas'), (4, 'Lily'), (5, 'Margot')" ) ui <- fluidPage( h2("SQL Injection Test"), textInput( inputId = "user_input", label = "Select only 1 ID to find your individual", value = 1 ), actionButton( inputId = "submit", label = "Submit" ), tableOutput( outputId = "result" ) ) server <- function(input, output, session) { rv <- reactiveValues() observeEvent(input$submit, { req(input$user_input) rv$query <- paste0( "SELECT * FROM users WHERE id = ", input$user_input ) rv$result <- dbGetQuery(con, rv$query) }) output$result <- renderTable({ req(rv$result) rv$result }) } shinyApp(ui, server)
A user could enter 1 OR 1=1
to retrieve all data from the table, or even 1; DROP TABLE users;
to delete the table.
How to protect yourself?
Golden rule: Never trust user input, regardless of context
Here are some best practices to secure your Shiny applications:
- For XSS: Use
htmltools::htmlEscape()
to escape HTML or JavaScript tags if you need to store them in a database.
htmltools::htmlEscape("<script>alert('hello')</script>") [1] "<script>alert('hello')</script>"
- For Command Injections:
- Avoid using
eval(parse(text = input$template))
or strictly control user input. - Only manipulate character strings without evaluating them.
- Filter and secure user choices with
match.arg()
,switch()
, or conditionalif
structures. - Use a
sandbox
when evaluation is necessary. - For uploaded files, prefer simple formats like
.csv
or.txt
.
- Avoid using
- For SQL Injections:
- Use
sqlInterpolate()
rather than constructing queries withpaste()
:
- Use
# Vulnerable query_vuln <- paste0(" SELECT * FROM users WHERE id = ", input$user_input ) # Secure query_str <- " SELECT * FROM users WHERE id = ?id " query <- sqlInterpolate(con, query_str, id = input$user_input) query # <SQL> # SELECT * # FROM users # WHERE id = 1 dbGetQuery(con, query)
Conclusion
Security is a crucial aspect of web application development, including Shiny applications. Never underestimate the risks related to unvalidated or unescaped user inputs. By following a few simple best practices, you can significantly improve the security of your applications.
Don’t hesitate to contact us if you would like to discuss the security of your Shiny applications!
You can also find all our upcoming training sessions to discover Shiny application development here!
This post is better presented on its original ThinkR website here: Security blind spots in Shiny: why your app is more vulnerable than you think
R-bloggers.com 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.
Continue reading: Security blind spots in Shiny: why your app is more vulnerable than you think
Understanding and Addressing Security Vulnerabilities in Shiny Applications
The development of technology and the sprawl of open-source software like Shiny bring along with it increasing vulnerabilities in security that developers have to be cautious of. Protocol such as signature.py in Python, for example, recently highlighted a potential back-end security issue. While this problem was ultimately fixable, it drove the necessity for constant monitoring and updating of code, especially in languages such as R and Shiny, to avoid outdated or vulnerable dependencies.
Gauging the Vulnerabilities in Shiny Applications
Several key areas in Shiny that are at risk of security breaches happen to be widely prevalent among other software. Code injections are quite common and can be seriously harmful, injecting malicious code that can be extensively intrusive and destructive. For instance, it can lead to an entire system being compromised, as illustrated by the recent encounter with the Python application.
Identify and Deal with Potential Security Threats
XSS (Cross-Site Scripting): This is a breach where a harmful JavaScript or CSS is used across different user browsers. This could be a serious issue when user inputs aren’t properly supervised.
Command Injection: In this type of breach, the injection of harmful code takes place on the server itself, hence, need to be cautious of using eval(parse(text = input$template)) and similar functions.
SQL Injection: This type of breach involves conducting a harmful SQL code execution in the database itself, highlighting the need for proper scrutiny of user inputs in SQL queries.
Formulating Measures for Stronger Safety Practices
Given that both the potential and impacts of these cell injections are high, web application developers must take up some important steps for more secure applications:
- Thoroughly evaluate user inputs for escaping HTML or JavaScript tags using functions such as htmltools::htmlEscape()
- Avoid directly evaluating user inputs through functions such as eval(parse(text = input$template))
- Consider using simple formatting when uploaded files are to be evaluated
- Make sure to scrutinize user inputs in SQL queries, opting for sqlInterpolate() instead of paste().
Conclusion
These indications and suggestions are integral to improving the safety regime of Shiny. Investing in the security of applications and regularly updating and monitoring the code will pay dividends in the long run by avoiding major breaches and losses due to out-of-date dependencies. As a first step, the suggestions listed can help improve the security stance and ensure growth sustainability for the Shiny developer community. Don’t hesitate to seek expert counsel to get your Shiny applications evaluated for safety assurance.