Catégories
Start-up et applications

Comment sécuriser votre API Rails sans être un expert en sécurité

Ruby on Rails est un outil si doux à utiliser. En tant que développeur full-stack, je trouve que c'est mon framework de choix chaque fois que j'ai besoin de construire un prototype / produit minimum viable. Sa facilité d'utilisation et sa configuration rapide me permettent d'avancer rapidement dans la construction du front-end de mes projets.

Cette commodité peut amener un développeur à ignorer les aspects de sécurité de son code. Je sais que j'en suis souvent coupable. Même si l'on est habitué au développement piloté par les tests, cela ne garantit pas que le code qu'ils écrivent est sécurisé.

Personnellement, je n'ai jamais vraiment prêté beaucoup d'attention à la sécurité lorsque j'ai créé des applications. La raison en est que je me concentre sur la construction de prototypes rapides et de produits minimalement viables (MVP). Mais ce n'est pas une excuse pour écrire du code bâclé qui peut facilement ouvrir les utilisateurs aux attaques.

C'est pourquoi, à tout le moins, en tant qu'ingénieurs, nous devons être conscients et pouvoir corriger les vulnérabilités les plus courantes que nous pouvons accidentellement intégrer à nos projets.

Les bases de données open source telles que le laboratoire de vulnérabilité de WhiteSource vous fournissent des informations mises à jour sur la plupart des vulnérabilités.

Vous pouvez en savoir plus sur chaque vulnérabilité, notamment sa gravité et les correctifs disponibles. Si vous découvrez également de nouvelles vulnérabilités, vous pouvez contacter ses responsables pour les confirmer et les ajouter à leur base de données.

Principales vulnérabilités

Dans cet article, je vais détailler les trois vulnérabilités les plus courantes que l'on peut rencontrer dans une application Ruby on Rails API uniquement. Cependant, certains des concepts présentés s'appliquent quelle que soit la plate-forme que vous utilisez et vous devez rechercher les outils similaires disponibles pour votre plate-forme si vous n'êtes pas sur Rails.

Je limite le sujet aux projets JSON-API car j'utiliserais personnellement Rails pour créer uniquement des API (et utiliser React ou tout autre framework frontal pour créer le côté client).

Détournement de session

Dans une application API Rails, une façon courante de procéder à l'authentification consiste à générer un jeton Web JSON (JWT). Chaque fois qu'une demande `POST` est faite au point de terminaison d'authentification (généralement` / sessions`), l'application Rails peut générer le JWT à partir de l'ID de l'utilisateur actuel et d'un horodatage d'expiration (généralement 24 heures).

Un exemple de la façon de générer ce JWT utilise la gemme `jwt` de Ruby:

def self.encode(payload, exp = 24.hours.from_now)
   payload(:exp) = exp.to_i
   JWT.encode(payload, SECRET_KEY)
 end

Le JWT sera envoyé dans le cadre de la réponse à l'application frontale.

Pour les demandes authentifiées (comme obtenir une liste de tous les amis des utilisateurs actuels dans une application de réseau social), l'application Rails doit vérifier les en-têtes de la demande pour la présence de ce JWT.

Si la demande ne contient aucun JWT ou un JWT expiré, l'API répondra simplement avec une erreur 401 (non autorisée).

Pendant l'authentification:

Si la demande ne contient aucun JWT ou un JWT expiré, l'API répondra simplement avec une erreur 401 (non autorisée).

Demandes ultérieures nécessitant l'authentification d'un demandeur:

Demandes ultérieures nécessitant l'authentification d'un demandeur:

Le problème ici est que, en supposant que le JWT n'a pas encore expiré, un pirate peut être en mesure de voler le JWT et d'accéder à l'API en tant qu'utilisateur authentifié. Les jetons d'authentification API ont été exploités de cette manière dans plusieurs attaques réelles.

Dans ce cas, la seule mesure contre cela consiste à faire expirer le JWT plus tôt. Ainsi, au lieu de 24 heures, le JWT peut expirer en 1 heure.

La bonne nouvelle est que le seul moyen pour un pirate de voler ce jeton est d'envoyer l'utilisateur accidentellement (ou bêtement) ce jeton via le Web ou si le pirate se rend physiquement sur la machine dans laquelle se trouve ce jeton (chez le client – côté le plus souvent, un jeton d'authentification est enregistré dans le stockage local).

Pourtant, nous voulons couvrir autant de terrain que possible en ce qui concerne la sécurité de toute application que nous construisons.

Ainsi, une manière alternative mais plus lourde de stocker notre jeton d'authentification consiste pour l'API Rails à enregistrer chaque JWT généré dans une base de données. Voici comment se déroule cette logique:

  1. Une table de base de données (et un modèle) supplémentaires appelés `tokens` seront générés
        $ rails g model token value:string
  2. Chaque fois qu'une connexion réussie a été établie, l'API Rails génère un nouveau JWT et l'enregistre en tant qu'enregistrement dans la table `tokens`
        def self.encode(payload, exp = 24.hours.from_now)
           payload(:exp) = exp.to_i
           token = JWT.encode(payload, SECRET_KEY)
           Token.create(value: token)
        end
  3. Ce même JWT sera envoyé au demandeur dans le cadre de la réponse.
  4. Pour les demandes qui ne sont autorisées que pour les utilisateurs authentifiés, l'API Rails vérifie l'existence d'un JWT dans les en-têtes. Automatiquement, la réponse sera un 401 (non autorisé) si les en-têtes n'en contiennent pas.
  5. Si le JWT est présent dans les en-têtes, la première chose que l'API Rails va faire est de vérifier son existence dans la table de base de données `tokens`. S'il ne peut pas être trouvé, un 401 sera envoyé comme réponse.
  6. Si le JWT existe dans la table `tokens`, alors l'API Rails essaiera de le décoder (en utilisant une méthode personnalisée). S'il n'est pas valide (c'est-à-dire expiré), une réponse 401 sera à nouveau envoyée.
  7. Si le JWT est valide, une réponse de réussite sera envoyée avec les données demandées (ou des actions appropriées seront prises dans l'API).

Nous pouvons écrire une méthode dans le fichier `application_controller.rb` pour vérifier l'existence du jeton dans les en-têtes, puis dans la base de données, et ensuite pour sa validité.

Utilisation de la «pierre précieuse» de Ruby:

def pundit_user
   header = request.headers('Authorization')
   header = header.split(' ').last if header
   begin
    if Token.find_by(value: header)
             decoded = JsonWebToken.decode(header)
             return User.find(decoded('id'))
      end
    decoded = JsonWebToken.decode(nil)
   rescue ActiveRecord::RecordNotFound
     render json: { message: 'You are not authorized' }, status: :unauthorized
   rescue JWT::DecodeError
     render json: { message: 'Unauthorized access' }, status: :unauthorized
end

Dessiner cette séquence dans un organigramme:

Dessin de la séquence de pierres précieuses de Ruby dans un organigramme

Le seul inconvénient de cette solution est que chaque connexion unique de l'utilisateur ajoutera un nouvel enregistrement dans la table `tokens`. Pour la durée de vie de chaque utilisateur, cela peut signifier des milliers à des millions d'enregistrements selon la fréquence de connexion.

Un correctif possible est d'avoir un nettoyage de base de données automatisé. Mais ceci est un article sur les failles de sécurité et les correctifs.

Cependant, l'avantage de cette approche est que nous pouvons créer une méthode qui nous permet de déconnecter tous les appareils en détruisant tous les enregistrements de la table `tokens`:

def logout_all
   Token.destroy_all
 end

Enfin, il existe toujours une menace très réelle que les pirates puissent pénétrer dans la base de données et voler les jetons. La meilleure façon de s'assurer que les JWT sont sécurisés de manière à les rendre inutiles aux pirates est d'utiliser des signatures numériques.

Injection SQL

Cette vulnérabilité de sécurité particulière n'affecte pas uniquement les applications Rails. Toutes les applications Web (ou mobiles) qui effectuent des requêtes SQL sur Internet y sont sensibles.

En termes simples, une attaque par injection SQL se produit chaque fois qu'un utilisateur malveillant manipule les paramètres de demande afin d'accéder au contenu de la base de données.

Par exemple, supposons que nous ayons une table de base de données appelée «utilisateurs» (et un modèle «utilisateur» correspondant). Disons également que la façon dont nous avons codé une requête pour obtenir les données d'un utilisateur particulier est la suivante:

User.where("first_name = '%#{params(:first_name)}%'")

Ainsi, la chaîne de requête suivante dans l'URL renverra un utilisateur de collection avec le nom "Michael":

/users?first_name=Michael

Cependant, un utilisateur malveillant "injectera" simplement n'importe quelle valeur de chaîne dans cette chaîne de requête afin d'étendre l'instruction de requête SQL dans la première ligne. Cela pourrait permettre à cet utilisateur malveillant d'accéder à tous les utilisateurs et de faire ce qu'il veut avec ces données.

En raison de sa prévalence, on pourrait penser que les responsables de Ruby on Rails disposeraient d'une protection prête à l'emploi pour cela. Malheureusement non.

Ce qui est génial, c'est que nous pouvons sécuriser nos API Rails contre cette vulnérabilité grâce à de simples ajustements dans nos requêtes ActiveRecord.

Nous pouvons ensuite réviser notre requête SQL ci-dessus pour la rendre plus sécurisée:

User.where("first_name = ?", params(:first_name))

Mis à part la différence syntaxique évidente, en quoi cette requête SQL nouvellement formée est-elle différente de la requête à cordes pure susmentionnée?

Dans la requête de chaîne pure, qui n'est pas sûre, chaque bit est transmis à la base de données comme si.

Ainsi, un pirate pourrait passer une chaîne de requête SQL étendue, comme mentionné précédemment, pour obtenir une liste de toutes les données utilisateur afin qu'il puisse vendre ou supprimer (tout ce qu'il veut).

Dans la requête SQL révisée, le deuxième argument est dynamique (puisque Rails "l'échappe") et ne va pas directement dans la base de données avant d'être filtré par Rails. Cela empêche toute tentative malveillante d'un pirate d'accéder à la base de données via des chaînes de requête.

Vulnérabilités d'autorisation et d'accès

L'authentification d'un utilisateur consiste simplement à vérifier si l'utilisateur qui fait une demande est connecté. L'autorisation fournit une autre couche de protection d'accès aux données.

Autoriser un utilisateur signifie fournir uniquement un certain niveau d'accès aux fonctionnalités en fonction de la catégorie d'utilisateurs qui fait de telles demandes.

Par exemple, les échanges de messages entre deux utilisateurs doivent être privés entre eux. Par conséquent, un système d'autorisation doit être en place pour vérifier si un utilisateur d'application essayant d'accéder aux données de conversation participe à cette conversation.

J'ai répertorié cette vulnérabilité en dernier car elle peut être causée davantage par des décisions architecturales que par du code. Il existe certaines meilleures pratiques que nous pouvons utiliser pour garantir que notre API dispose d'un système d'autorisation de haute qualité. Mais la mesure architecturale la plus simple est probablement celle qui fournit le moins d'accès par défaut.

Par exemple, le seul accès par défaut que tous les utilisateurs devraient avoir est leurs propres données.

Comme l'utilisateur bénéficie de privilèges supplémentaires (c'est-à-dire lorsqu'il devient administrateur), c'est le moment où il a également accès à des informations ou des fonctionnalités supplémentaires.

Utilisation de la gemme `pundit`

Heureusement, en tant que développeurs Rails, nous pouvons utiliser `pundit`. Pundit nous permet de créer facilement des «politiques» qui restreignent les types de demandes que les utilisateurs peuvent faire en fonction de certains attributs du modèle.

Pour des politiques plus avancées, le développeur peut simplement créer de nouvelles classes. Les politiques résultantes sont également très facilement testées.

Conclusion

Il existe mille types de vulnérabilités que tout projet peut avoir. Tous les trois que j'ai énumérés ici sont facilement détectés. Mais il y en a des centaines qui sont dues aux types de dépendances que nous installons dans nos projets.

Heureusement pour nous si nous poussons notre code vers Github, nous recevrons des notifications si une vulnérabilité est détectée dans l'une des dépendances de notre code.

Nous poussons notre code vers Github, nous recevrons des notifications si une vulnérabilité est détectée dans l'une des dépendances de notre code.

Ce qui est encore plus fascinant, c'est que Github nous fournit déjà la solution:

Ce qui est encore plus fascinant, c'est que Github nous fournit déjà la solution suivante:

Le principal message que je souhaite transmettre dans cet article est que les développeurs commencent à devenir plus orientés vers la sécurité, de rester au courant des mises à jour de sécurité de Github aux vulnérabilités du langage de programmation.

Le domaine de la sécurité peut être une discipline entièrement nouvelle pour la plupart d'entre nous, mais nous n'avons pas besoin d'être des experts pour pouvoir créer des applications sécurisées.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *