R Shiny Security: How to Make Your Shiny Apps Secured
Securing your Shiny application is not just an added feature; it's a fundamental necessity. Often, functionality and <a href="https://appsilon.com/how-to-make-your-shiny-app-beautiful/" target="_blank" rel="noopener">design</a> 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. <strong>With its unique features and capabilities, Shiny requires a specific approach to security.</strong>
This blog post will delve into some Shiny-specific dos and don'ts to help you <strong>fortify your application</strong> against potential threats and vulnerabilities.
<h3>Table of Contents</h3>
<ul><li><a href="#authentication"><strong>Authentication</strong></a></li><li><strong><a href="#sql-queries">SQL Queries</a></strong></li><li><strong><a href="#user-interface">User Interface</a></strong></li><li><strong><a href="#error-handling">Error Handling</a></strong></li><li><strong><a href="#rendering-user-input">Rendering User Input</a></strong></li><li><strong><a href="#evaluating-user-input">Evaluating User Input</a></strong></li><li><strong><a href="#summing-up-r-shiny-security">Summing Up R Shiny Security</a></strong></li></ul>
<hr />
<a href="https://appsilon.com/shiny-fluent-tutorial/" target="_blank" rel="noopener noreferrer">Shiny apps</a> 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.
<h2 id="authentication">Authentication</h2>
<h3>Don’t: Roll your own Authentication</h3>
<strong>Rolling your own authentication system can be a risky venture.</strong> 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.
<h3>Do: Use Service Providers Such as Posit Connect</h3>
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.
<blockquote>Read more on <a href="https://appsilon.com/why-use-rstudio-connect-authentication/" target="_blank" rel="noopener">Why You Should Use RStudio (Posit) Connect Authentication And How to Set It Up</a> to learn more about this topic.</blockquote>
<h2 id="sql-queries">SQL Queries</h2>
<img class="aligncenter wp-image-23319 size-full" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65e9e6401d436a831615d229_e6e148a2_image1-1.webp" alt="A two-panel meme with Drake showing disapproval in the top panel and approval in the bottom panel. In the top panel, the text reads "paste, paste0, sprintf." In the bottom panel, the text reads "glue::glue_sql, DBI::sqlInterpolate."" width="500" height="500" />
<h3>Don’t: Interpolate User Input Directly Into SQL Queries</h3>
Direct interpolation of user input into <a href="https://appsilon.com/intermediate-sql/" target="_blank" rel="noopener">SQL queries</a> 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:
<pre><code class="language-r">
query <- paste0("SELECT * FROM users WHERE name = '", input$username, "'")
</code></pre>
An attacker could input a value like <code>John'; DROP TABLE users; --</code>, which when interpolated, results in a query that first selects users named “John” and THEN DELETES YOUR ENTIRE <code>users</code> TABLE.
<h3>Do: Use Parametrized Queries to Secure a Shiny Application</h3>
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:
<pre><code class="language-r">
query <- glue_sql("SELECT * FROM users WHERE name = {input$username}", .con = con)
</code></pre>
This ensures that <code>input$userName</code> is automatically quoted, treating the input as a string and preventing running it as an SQL command.
<h2 id="user-interface">User Interface</h2>
<h3>Don’t: Rely on UI for security</h3>
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:
<pre><code class="language-r">
library(shiny)
<br>important_data <- data.frame(
name = c("Alice", "Bob", "Charlie"),
surname = c("Smith", "Jones", "Brown"),
credit_card_number = c(1234, 5678, 9012)
)
<br>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")
)
)
)
)
<br>server <- function(input, output) {
output$column_value <- renderPrint(important_data[, input$selected_column])
}
<br>shinyApp(ui = ui, server = server)
</code></pre>
This app first asks the user for their role. Then, if the role is admin, it displays a <code>sidebarLayout</code> 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 <code>conditionalPanel</code> are embedded in the <code>data-display-if</code> attribute.
<pre><code class="language-r">
<div data-display-if="input.user_role != 'admin'" data-ns-prefix="">
</code></pre>
Another flaw of the conditionalPanel is that they are hidden by the CSS attribute <code>display: none</code>. 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 <code>credit_card_number</code> in the <code>selectInput</code> choices, the attacker can still select it by running <code>Shiny.setInputValue("selected_column", "credit_card_number")</code> in the browser’s developer console. Causing the output$column_value to re-render and exposing the credit card numbers to the attacker.
<h3>Do: Implement server-side checks</h3>
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 <code>uiOutput</code> instead of <code>conditionalPanel</code>. 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:
<pre><code class="language-r">
library(shiny)
<br>important_data <- data.frame(
name = c("Alice", "Bob", "Charlie"),
surname = c("Smith", "Jones", "Brown"),
credit_card_number = c(1234, 5678, 9012)
)
<br>ui <- fluidPage(
div(
id = "user_role_ui",
textInput("user_role", "Enter Your Role"),
actionButton("submit", "Submit")
),
uiOutput("sidebar_layout")
)
<br>server <- function(input, output) {
observe({
if (input$user_role == "admin") {
removeUI("#user_role_ui")
<br> output$sidebar_layout <- renderUI({ sidebarLayout( sidebarPanel( selectInput( "selected_column", "Select Column", c("name", "surname") ), ), mainPanel( verbatimTextOutput("column_value") ) ) }) } }) |>
bindEvent(input$submit)
<br> output$column_value <- renderPrint({
req(
length(input$selected_column) == 1 &&
input$selected_column %in% c("name", "surname")
)
important_data[, input$selected_column]
})
}
<br>shinyApp(ui = ui, server = server)
</code></pre>
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 <code>renderUI</code>.
Furthermore, after you write “admin” and hit the submit button, you will not be able to select the credit_card_number column with the <code>Shiny.setInputValue</code> trick because we require the input value to be either name or surname in renderPrint.
<h2 id="error-handling">Error Handling</h2>
<h3>Don’t: Display Raw Error Messages</h3>
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 <a href="https://owasp.org/www-community/attacks/SQL_Injection" target="_blank" rel="noopener noreferrer">SQL injection attacks</a>.
<h3>Do: Sanitize Errors</h3>
You can use the <code>options(shiny.sanitize.errors = TRUE)</code> 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 <code>FALSE</code> 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 <a href="https://shiny.posit.co/r/articles/improve/sanitize-errors/" target="_blank" rel="noopener noreferrer">Sanitizing error messages</a>.
<h2 id="rendering-user-input">Rendering User Input</h2>
<h3>Don’t: Allow Cross-Site Scripting</h3>
<a href="https://owasp.org/www-community/attacks/xss/" target="_blank" rel="noopener noreferrer">Cross-site scripting (XSS)</a> 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:
<pre><code class="language-r">
# install.packages("shiny")
library(shiny)
<br>
ui <- fluidPage(
textInput("comment", "Write your comment"),
actionButton("submit_comment", "Comment"),
uiOutput("comment")
)
<br>server <- function(input, output) {
observeEvent(input$submit_comment, {
output$comment <- renderUI({
HTML(input$comment)
})
})
}
<br>shinyApp(ui = ui, server = server)
</code></pre>
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 <code><script>alert('attack')</script></code> after running the app.
<h3>Do: Sanitize User Inputs</h3>
To prevent XSS attacks in Shiny applications, it's essential to sanitize user inputs. Instead of directly using functions like <code>HTML()</code>, opt for safer alternatives like <code>div()</code>, or <code>p()</code> from the Shiny package, which automatically escapes HTML tags and prevents script execution. Additionally, instead of using <code>uiOutput</code> and HTML, you can use textOutput / renderText.
<h2 id="evaluating-user-input">Evaluating User Input</h2>
<h3>Don’t: Execute User Input as Code</h3>
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 <code>eval</code> or <code>parse</code>. It's not just direct evaluation functions that pose a risk; other constructs, such as formulas or <code>glue::glue</code>, can inadvertently evaluate user inputs as code. This can lead to severe consequences.
<h3>Do: Employ Controlled Execution Environments</h3>
The safest approach is to entirely avoid executing user inputs as code. Instead of using glue, use the <code>glue_safe</code> 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.
<h2 id="summing-up-r-shiny-security">Summing Up R Shiny Security</h2>
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 <a href="https://join.slack.com/t/shinyconf/shared_invite/zt-2asfuoxtw-~2GN8~1cowG9KuU2t_gCKQ" target="_blank" rel="noopener">forums</a>, social media, and <a href="https://www.shinyconf.com/" target="_blank" rel="noopener">conferences</a> can provide insights into emerging best practices and common pitfalls in Shiny app development.
Stay vigilant, stay informed, and happy coding!
<h4>External resources:</h4><ul> <li><a href="https://mastering-shiny.org/scaling-security.html" target="_blank" rel="noopener noreferrer">Mastering Shiny: Security </a></li> <li><a href="https://owasp.org/www-project-secure-coding-practices-quick-reference-guide/" target="_blank" rel="noopener noreferrer">OWASP Secure Coding Practices-Quick Reference Guide</a></li></ul>