Sécuriser ses applications Shiny
2026-06-16
Aux đ Rencontres R 2023, Pierrot maquettait son application Shiny.
La cuisine se dessine.
Aux đ Rencontres R 2025, Pierrot testait son application Shiny.
La cuisine est solide : tiroirs testés, modules emboßtés, repas cuisiné.
Son application est maquettée.
Elle est testée.
Elle est en production chez son client.
Tout va bien.
đ âPierrot, on a un gros problĂšme avec lâappâŠâ
> Vous avez été piratés.
La porte forcée
Les tiroirs vidés
Le coffre-fort ouvert
â Mal montĂ©e
â Pas robuste
â Mal testĂ©e
â CambriolĂ©e
Au mĂȘme titre que :
â Les tests
â La documentation
Pas rĂ©servĂ©e aux experts en cybersĂ©curitĂ©. Câest notre mĂ©tier de dev.
đ shiny::runApp("security/apps/reprex1")
Inputs â strings.
On tape :
<script>alert()</script>
â Pas dâĂ©valuation = safe.
đ shiny::runApp("security/apps/reprex2")
Une seule diff : HTML(input$message) au lieu de input$message.
On tape :
<script>alert()</script>
đ„ le script sâexĂ©cute.
đ Faille XSS
Le principe : injecter du code malveillant qui sera ensuite exĂ©cutĂ© par lâapplication.
đ XSS (Cross-Site Scripting) : cĂŽtĂ© navigateur (client)
đ Command Injection : cĂŽtĂ© serveur
đ SQL Injection : cĂŽtĂ© base de donnĂ©es
Le point commun : exploiter lâabsence de validation des donnĂ©es utilisateur.
Une saisie utilisateur depuis lâinterfaceâŠ
âŠĂvaluĂ©e par lâapplicationâŠ
â Nâimporte qui peut exĂ©cuter du code R sur votre serveur.
Lecture de fichiers, accĂšs au systĂšme, exfiltration de secrets
đ shiny::runApp("security/apps/reprex3")
glue("Ton choix est : {input$xyz}")
On tape :
Hello {1+1}
Lâinput reste une string, jamais Ă©valuĂ©e comme du code.
â Pas dâĂ©valuation = safe.
đ shiny::runApp("security/apps/reprex4")
glue(input$xyz)
Lâinput est traitĂ© comme du code Ă interprĂ©ter, plus comme du contenu.
On rajoute les accolades :
Hello {1+1}
â Hello 2.
đ„ Lâinput est Ă©valuĂ© comme du code.
glue(input$template) â eval(parse(text = input$template))
Mais glue() est plus discret car il Ă©value automatiquement ce quâil y a entre {}.
selectInput !âđ shiny::runApp("security/apps/reprex5")
selectInput â 3 choix uniquement⊠cĂŽtĂ© UI.
Console navigateur :
Shiny.setInputValue("color", "{1+1}")
đ„ selectInput contournĂ©.
Ou via switch() / if selon le besoin.
Toute donnĂ©e venant du client doit ĂȘtre validĂ©e serveur-side. Toujours.
â
.csv, .txt, .json â du contenu, pas du code sĂ©rialisĂ©.
đĄïž Si vraiment besoin dâun .rds : sandbox.
(callr, processx en environnement restreint, containerâŠ)
đ shiny::runApp("security/apps/reprex7")
paste0("SELECT ⊠WHERE id = ", input$x)
Tape 1 â Robert â
Tape 1 OR 1=1 â toute la table đ„
Fuite : on récupÚre toutes les lignes de la table.
selectInput !â (bis)đ shiny::runApp("security/apps/reprex8")
selectInput â 5 IDs uniquement⊠cĂŽtĂ© UI.
Console navigateur :
Shiny.setInputValue("user_input", "1 OR 1=1")
đ„ MĂȘme fuite, malgrĂ© le selectInput.
â Avant
MĂȘme attaque 1 OR 1=1 â requĂȘte vide, table intacte.
â Avant
Lâattaquant injecte du code dans une pageâŠ
âŠstockĂ© cĂŽtĂ© serveur ou simplement reflĂ©tĂ©âŠ
â exĂ©cutĂ© chez tous les visiteurs qui consultent la page.
đ shiny::runApp("security/apps/reprex11")
Pierrot poste un message :
Salut tout le monde !
Tout fonctionne â
đ shiny::runApp("security/apps/reprex11")
Un autre poste :
<script>alert(...)</script>
đ„ Alert chez tous les visiteurs.
Et à chaque rechargement : ça repart.
htmltools::htmlEscape()Le <script> devient du texte affiché, pas exécuté.
Ă appliquer sur tout contenu utilisateur : pseudo et message.
đ Command Injection : jamais Ă©valuer un input
đ SQL Injection : sqlInterpolate() ou glue_sql(), toujours
đ XSS : htmlEscape() tout ce qui sâaffiche
đ Et la rĂšgle n°1 : ne jamais faire confiance Ă lâutilisateur
đ Lâauthentification : shinymanager, Posit Connect, OAuth⊠Câest un autre sujet, mais essentiel.
đŠ Les dĂ©pendances Ă jour : pas de Dependabot natif en R.
Mise à jour réguliÚre, surveillance manuelle des annonces upstream des paquets critiques.
đ connect.thinkr.fr/hackthisshiny/
đ shiny.posit.co/r/articles/build/sql-injections/

Pierrot sâest fait pirater | Retrouvez nous sur https://thinkr.fr