Securing your Shiny application is not just an added feature; it’s a fundamental necessity. Often, functionality and design are prioritized in development, but ensuring the security of your app is equally important, if not more so. Shiny security involves more than just adhering to general programming best practices like utilizing environment variables instead of hardcoding sensitive keys. With its unique features and capabilities, Shiny requires a specific approach to security.
This blog post will delve into some Shiny-specific dos and don’ts to help you fortify your application against potential threats and vulnerabilities.
Table of Contents
- Authentication
- SQL Queries
- User Interface
- Error Handling
- Rendering User Input
- Evaluating User Input
- Summing Up R Shiny Security
Shiny apps are frequently used for data analysis and visualization in corporate environments, where they might access confidential datasets. Any vulnerability in a Shiny app could lead to data breaches, unauthorized access to internal systems, or exposure of intellectual property.
Therefore, securing Shiny apps is not only about protecting the application itself but also safeguarding the valuable and sensitive data they process and the integrity of the systems they interact with.
Authentication
Don’t: Roll your own Authentication
Rolling your own authentication system can be a risky venture. Designing an authentication system requires a deep understanding of security protocols, encryption, and threat detection.
A self-made system might miss critical security features, making it vulnerable to attacks. Even if you design such a system that can address these issues, the main challenge lies in maintaining and updating the custom authentication system to keep pace with new security threats.
Do: Use Service Providers Such as Posit Connect
Opting for established service providers like Posit Connect for authentication is the best choice if you want to take Shiny security to the next level. These services are developed by teams of experts who are focused solely on security, ensuring that the authentication mechanism is as robust as possible.
They offer features like secure password handling, hardening against common attacks, and regular security updates, which are critical for safeguarding your application against unauthorized access. Using such services also allows you to focus on the core functionality of your Shiny app.
Read more on Why You Should Use RStudio (Posit) Connect Authentication And How to Set It Up to learn more about this topic.
SQL Queries
Don’t: Interpolate User Input Directly Into SQL Queries
Direct interpolation of user input into SQL queries is a common yet critical vulnerability in web development, including Shiny apps. This practice opens the door to SQL injection attacks, where malicious users can manipulate queries to gain unauthorized access to or manipulate your database. For example, consider a logic where the user input is directly used to construct a query:
query <- paste0("SELECT * FROM users WHERE name = '", input$username, "'")
An attacker could input a value like John'; DROP TABLE users; --
, which when interpolated, results in a query that first selects users named “John” and THEN DELETES YOUR ENTIRE users
TABLE.
Do: Use Parametrized Queries to Secure a Shiny Application
Parameterized queries ensure that user input is handled safely, treating it as data rather than part of the SQL command. Packages like {DBI} (sqlInterpolate) and {glue} (glue_sql) provide functionality for creating safe SQL queries. For example, using {glue}, you could rewrite the vulnerable query as:
query <- glue_sql("SELECT * FROM users WHERE name = {input$username}", .con = con)
This ensures that input$userName
is automatically quoted, treating the input as a string and preventing running it as an SQL command.
User Interface
Don’t: Rely on UI for security
Relying on the UI elements for security in Shiny applications can be a significant oversight. UI elements, no matter how well-designed, are inherently vulnerable because they are client-side and can be manipulated by users. Here is an example:
library(shiny)
important_data <- data.frame(
name = c("Alice", "Bob", "Charlie"),
surname = c("Smith", "Jones", "Brown"),
credit_card_number = c(1234, 5678, 9012)
)
ui <- fluidPage(
conditionalPanel(
condition = "input.user_role != 'admin'",
textInput("user_role", "Enter Your Role"),
),
conditionalPanel(
condition = "input.user_role == 'admin'",
sidebarLayout(
sidebarPanel(
selectInput("selected_column", "Select Column", c("name", "surname")),
),
mainPanel(
verbatimTextOutput("column_value")
)
)
)
)
server <- function(input, output) {
output$column_value <- renderPrint(important_data[, input$selected_column])
}
shinyApp(ui = ui, server = server)
This app first asks the user for their role. Then, if the role is admin, it displays a sidebarLayout
that shows the values for a given column in the data. On the surface, it might look like a secure app, but it is extremely vulnerable.
First of all, anyone can inspect the HTML code of this Shiny App and see that the required role is “admin”. Conditions of a conditionalPanel
are embedded in the data-display-if
attribute.
<div data-display-if="input.user_role != 'admin'" data-ns-prefix="">
Another flaw of the conditionalPanel is that they are hidden by the CSS attribute display: none
. So any attacker can easily bypass this input by deleting this CSS attribute to access the sidebarLayout.
Finally, even if you don’t include the column credit_card_number
in the selectInput
choices, the attacker can still select it by running Shiny.setInputValue("selected_column", "credit_card_number")
in the browser’s developer console. Causing the output$column_value to re-render and exposing the credit card numbers to the attacker.
Do: Implement server-side checks
Server-side checks validate user inputs and actions on the server, where they cannot be tampered with by end-users. Regardless of how an input is presented or hidden in the UI, the server should independently verify the legitimacy of every action – thus increasing the security of your R Shiny application. For instance, if a certain part of the UI has critical information that should be only shown based on a condition, use uiOutput
instead of conditionalPanel
. Additionally, always validate and sanitize all inputs on the server side instead of relying on the UI. Following on those ideas, we can improve the app like this:
library(shiny)
important_data <- data.frame(
name = c("Alice", "Bob", "Charlie"),
surname = c("Smith", "Jones", "Brown"),
credit_card_number = c(1234, 5678, 9012)
)
ui <- fluidPage(
div(
id = "user_role_ui",
textInput("user_role", "Enter Your Role"),
actionButton("submit", "Submit")
),
uiOutput("sidebar_layout")
)
server <- function(input, output) {
observe({
if (input$user_role == "admin") {
removeUI("#user_role_ui")
output$sidebar_layout <- renderUI({ sidebarLayout( sidebarPanel( selectInput( "selected_column", "Select Column", c("name", "surname") ), ), mainPanel( verbatimTextOutput("column_value") ) ) }) } }) |>
bindEvent(input$submit)
output$column_value <- renderPrint({
req(
length(input$selected_column) == 1 &&
input$selected_column %in% c("name", "surname")
)
important_data[, input$selected_column]
})
}
shinyApp(ui = ui, server = server)
Now when you inspect the page in your browser, you will only see the HTML code for the text input and the submit button. This is because we render the rest of the UI on the server side with renderUI
.
Furthermore, after you write “admin” and hit the submit button, you will not be able to select the credit_card_number column with the Shiny.setInputValue
trick because we require the input value to be either name or surname in renderPrint.
Error Handling
Don’t: Display Raw Error Messages
Although error messages can help developers debug the application during development, these messages often contain sensitive information about the app’s internal structure, such as file paths, database schema details, or even the logic behind certain functionalities.
Attackers can exploit this information for malicious purposes, such as identifying vulnerabilities in the application or the underlying system. For instance, a database error message might reveal table names or field structures, providing attackers with valuable insights for constructing SQL injection attacks.
Do: Sanitize Errors
You can use the options(shiny.sanitize.errors = TRUE)
setting in Shiny, which ensures that any error messages displayed to the user are generic and do not reveal any sensitive information about the application’s structure or the data it handles.
This setting is FALSE
by default to help developers debug their apps. To get the best out of both worlds in terms of securing a Shiny application, you can leave this setting off on the development environment while turning it on in production. For more information, read Sanitizing error messages.
Rendering User Input
Don’t: Allow Cross-Site Scripting
Cross-site scripting (XSS) is a critical security vulnerability that can occur in web applications, including Shiny apps, when they render user-provided HTML content. In Shiny, this risk is present when dynamic content is displayed based on user input.
If an attacker inputs a malicious script as part of this content, it can be executed in the browsers of other users, leading to data theft, session hijacking, or other security breaches.
For instance, consider a Shiny app that naively uses user input to dynamically generate page content without filtering or escaping:
# install.packages("shiny")
library(shiny)
ui <- fluidPage(
textInput("comment", "Write your comment"),
actionButton("submit_comment", "Comment"),
uiOutput("comment")
)
server <- function(input, output) {
observeEvent(input$submit_comment, {
output$comment <- renderUI({
HTML(input$comment)
})
})
}
shinyApp(ui = ui, server = server)
If the user’s comment contains a malicious script, it would be executed in the browser of anyone viewing that output, compromising the security of the application and its users. You can try it by commenting <script>alert('attack')</script>
after running the app.
Do: Sanitize User Inputs
To prevent XSS attacks in Shiny applications, it’s essential to sanitize user inputs. Instead of directly using functions like HTML()
, opt for safer alternatives like div()
, or p()
from the Shiny package, which automatically escapes HTML tags and prevents script execution. Additionally, instead of using uiOutput
and HTML, you can use textOutput / renderText.
Evaluating User Input
Don’t: Execute User Input as Code
Allowing user inputs to be executed as code is an enormous security risk in R Shiny. It’s similar to leaving your application’s front door unlocked, inviting anyone to enter and potentially take control.
This security vulnerability arises when user inputs are treated as executable R code using functions like eval
or parse
. It’s not just direct evaluation functions that pose a risk; other constructs, such as formulas or glue::glue
, can inadvertently evaluate user inputs as code. This can lead to severe consequences.
Do: Employ Controlled Execution Environments
The safest approach is to entirely avoid executing user inputs as code. Instead of using glue, use the glue_safe
function to prevent glue from executing any R code. If your Shiny app’s functionality inherently requires executing user-provided scripts or expressions, it is crucial to implement strict controls and safeguards.
One method is to use a controlled execution environment, such as a sandboxed interpreter, which restricts the commands that can be run and isolates them from your server and data.
Summing Up R Shiny Security
In conclusion, securing your Shiny application is a multifaceted challenge that demands attention to various aspects of application design and implementation. As new threats emerge and technologies evolve, it’s crucial to stay informed and adapt your security practices accordingly.
Regularly reviewing and updating your Shiny applications, considering both the code and the deployment environment, will help ensure that they remain robust against potential security threats.
The community around Shiny and R is a valuable resource. Engaging with the community through forums, social media, and conferences can provide insights into emerging best practices and common pitfalls in Shiny app development.
Stay vigilant, stay informed, and happy coding!