Les angles morts de sécurité dans Shiny : Pourquoi votre application est plus vulnérable que vous ne le pensez

Shiny Security
Author : Arthur Bréant
Categories : base de données, développement, security, shiny, sql
Tags : security, shiny, sql
Date :

Mettez votre ceinture, on parle de sécurité…

En développant l’application Signature.py, ThinkR a récemment été confrontée à une problématique de sécurité – en Python – qui mérite toute notre attention.

Versionné sur Github, le code de Signature.py est analysé par le Dependabot. Il s’agit d’un robot qui scanne automatiquement les projets à la recherche de dépendances obsolètes ou vulnérables et peut, éventuellement, proposer la mesure corrective.

Dependabot nous a signalé une faille de sécurité liée à l’utilisation de jinja2, et cela nous a amenés à une réflexion plus large : sommes-nous, développeurs d’applications R et Shiny, également concernés par ces questions de sécurité ?

La réponse est un grand OUI.

De quoi parlons-nous exactement ?

L’impact du problème identifié dans notre application Python était potentiellement grave : exécution de code involontaire côté backend, pouvant techniquement accéder à toute l’infrastructure de la machine. Heureusement, Signature.py ne manipule que des chaînes de caractères et la mise à jour de jinja2 a pu corriger l’alerte.

Mais qu’en est-il dans l’écosystème R ? Les développeurs Shiny sont-ils aussi exposés à ce type de vulnérabilités ?

L’injection de code : un danger omniprésent

L’injection de code est une vulnérabilité de sécurité courante qui consiste à injecter du code malveillant dans une page ou une application. Ce code est ensuite exécuté, générant ainsi la faille de sécurité. Il existe plusieurs façons d’injecter du code dans une application, et Shiny n’est malheureusement pas immunisé contre ces risques.

Les vulnérabilités dans Shiny

La bonne nouvelle

Shiny est bien foutu” ! Par défaut, les inputs dans Shiny renvoient (presque) uniquement des chaînes de caractères. Tant que le contenu de l’input n’est pas évalué, nous sommes relativement en sécurité.

Par exemple, cette application simple qui affiche simplement le contenu saisi par l’utilisateur est sécurisée :

library(shiny)
ui <- fluidPage(
  textInput("template", "Saisir quelque chose"),
  textOutput("result")
)
server <- function(input, output, session) {
  output$result <- renderText({
    paste("Ton choix est", input$template)
  })
}
shinyApp(ui, server)

La mauvaise nouvelle

Le contenu de l’input peut être évalué et introduire une faille de sécurité. Dès que nous commençons à évaluer le contenu d’un input, les risques apparaissent.

Les trois grandes familles d’injection dans les applications web

1. XSS (Cross-Site Scripting) : côté client

Le code JavaScript ou CSS est injecté et exécuté dans le navigateur d’un autre utilisateur. Dans Shiny, cela peut se produire lorsque nous rendons du HTML directement à partir d’un input utilisateur sans l’échapper correctement.

Par exemple, cette application est vulnérable :

library(shiny)
ui <- fluidPage(
  textInput("text", "Saisir quelque chose"),
  uiOutput("result")
)
server <- function(input, output, session) {
  output$result <- renderUI({
    HTML(input$text)
  })
}
shinyApp(ui, server)

Dans l’input de texte, un utilisateur malveillant pourrait saisir <script>alert('coucou')</script> ou même des codes plus dangereux comme :

<script>
  const fakeBtn = document.createElement("button");
  fakeBtn.innerText = "🔐 Connexion requise";
  fakeBtn.style = "display:block; margin:10px 0; padding:10px; background-color:red; color:white;";
  fakeBtn.onclick = function() {
    alert("Saisissez vos identifiants !");
  };
  document.body.prepend(fakeBtn);
</script>

Dans l’application vulnérable suivante, vous pouvez copier/coller l’intégralité de ce morceau de code dans l’application ci-dessous, avec les balises <script> pour voir la menace.

Vous pouvez écrire un premier commentaire puis copier/coller le code pour voir la vulnérabilité.
Pas de crainte ici, nous n’enregistrons pas les informations dans une base de données 😉

library(shiny)
ui <- fluidPage(
  h2("💬 Commentaires des utilisateurs"),
  textInput("pseudo", "Ton pseudo :", "Invité"),
  textAreaInput("message", "Ton message :", rows = 3),
  actionButton("envoyer", "Envoyer"),
  tags$hr(),
  h3("💬 Commentaires reçus :"),
  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)

Il existe deux types principaux de XSS :

  • Reflected XSS : Le code malveillant est immédiatement exécuté.
  • Stored XSS : Le code malveillant est stocké dans une base de données et exécuté lorsque d’autres utilisateurs accèdent à la page.

La menace des Stored XSS est particulièrement préoccupante. Dans l’exemple ci-dessus, ce code JavaScript malveillant ajoute un faux bouton de connexion qui apparaîtra pour tous les futurs utilisateurs de l’application. Imaginez le scénario : un attaquant injecte ce code dans votre application, et tous les utilisateurs suivants voient un bouton rouge demandant une connexion. Ils cliquent dessus, saisissent leurs identifiants… et ces informations peuvent être facilement récupérées par l’attaquant.

2. Command Injection : côté serveur

Le code est exécuté directement sur le serveur. Cela peut se produire dans Shiny lorsque nous évaluons directement le contenu d’un input.

Considérez cette application apparemment inoffensive :

library(shiny)
library(glue)
ui <- fluidPage(
  textInput("template", "Saisir quelque chose"),
  textOutput("result")
)
server <- function(input, output, session) {
  output$result <- renderText({
    glue(input$template)
  })
}
shinyApp(ui, server)

Cette application est pourtant vulnérable car glue() évalue automatiquement ce qui se trouve entre {}. Un utilisateur malveillant pourrait saisir {system("ls /", intern = TRUE)} ou pire {system("rm -rf /")}.

systeme() est une fonction R qui exécute des commandes shell.
Bien que system("ls /", intern = TRUE) présente un risque relatif car elle affiche uniquement le contenu du répertoire racine de votre ordinateur, {system("rm -rf /")}est une commande potentiellement destructrice. Cette commande va essayer de supprimer tous les fichiers du système ! À manipuler avec précaution 😉

L’utilisation de glue() paraît inoffensive, pourtant glue::glue(input$template) revient à la même chose que eval(parse(text = input$template)). Le duo eval(parse(text = ...)) est une opération qui va chercher à évaluer du texte. L’utilisation de ces fonctions par le développeur est claire : il cherche à évaluer du texte. Or, l’utilisation de la fonction glue() est plus insidieuse ici.

3. SQL Injection : côté base de données

Le code SQL est exécuté dans la base de données. Cela peut se produire lorsque nous construisons des requêtes SQL à partir d’inputs utilisateurs sans les échapper correctement.

Par exemple, cette application est vulnérable :

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("Test d'injection SQL"),
  textInput(
    inputId = "user_input",
    label = "Sélectionnez 1 seul ID pour trouver votre individu",
    value = 1
  ),
  actionButton(
    inputId = "submit",
    label = "Soumettre"
  ),
  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)

Un utilisateur pourrait saisir 1 OR 1=1 pour récupérer toutes les données de la table, ou même 1; DROP TABLE users; pour supprimer la table.

Comment se protéger ?

Règle d’or : Ne faites jamais confiance à l’entrée utilisateur, quel que soit le contexte

Voici quelques bonnes pratiques pour sécuriser vos applications Shiny :

  1. Pour les XSS : Utilisez htmltools::htmlEscape() pour échapper les balises HTML ou JavaScript si vous devez les stocker en base.
htmltools::htmlEscape("<script>alert('coucou')</script>")
[1] "&lt;script&gt;alert('coucou')&lt;/script&gt;"
  1. Pour les Command Injections :
    • Évitez l’utilisation de eval(parse(text = input$template)) ou contrôlez strictement la saisie de l’utilisateur.
    • Manipulez uniquement des chaînes de caractères sans les évaluer.
    • Filtrez et sécurisez les choix des utilisateurs avec match.arg(), switch(), ou des structures conditionnelles if.
    • Utilisez une sandbox lorsque l’évaluation est nécessaire.
    • Pour les fichiers chargés, privilégiez les formats simples comme .csv ou .txt.
  2. Pour les SQL Injections :
    • Utilisez sqlInterpolate() plutôt que de construire les requêtes avec paste() :
# Vulnérable
query_vuln <- paste0("
  SELECT * 
  FROM users 
  WHERE id = ", input$user_input
)
# Sécurisé
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

La sécurité est un aspect crucial du développement d’applications web, y compris pour les applications Shiny. Ne sous-estimez jamais les risques liés aux entrées utilisateur non validées ou non échappées. En suivant quelques bonnes pratiques simples, vous pouvez considérablement améliorer la sécurité de vos applications.

N’hésitez pas à nous contacter si vous souhaitez discuter de la sécurité de vos applications Shiny !

Vous pouvez également retrouver toutes nos futures formations pour découvrir le développement d’applications Shiny à venir juste ici !


Comments


Also read