ORM : exploitation relations en cascade et défaut de validation entrées utilisateur

En 2021, le top 10 de l’OWASP, qui met en lumière les vulnérabilités les plus courantes des applications, a quelque peu évolué. En effet les failles d’injection, auparavant les plus critiques, se retrouvent désormais sur la troisième marche du podium.

Cela peut s’expliquer notamment par le fait que les développeurs prennent davantage conscience des risques liés aux vulnérabilités d’injections via la mise en œuvre d’outils et de pratiques plus sûrs pour le développement d’applications. Et évidemment LA mesure essentielle pour limiter le risque d’injection SQL consiste en l’utilisation de requêtes préparées.

Pour ce faire, on utilise généralement un ORM, ce qui peut introduire de nouveaux risques comme nous allons le voir dans cet article.

Qu’est-ce qu’un ORM ? 

La plupart des applications ont besoin de manipuler et de stocker des données. Pour ce faire et pour faciliter leur travail, les développeurs peuvent avoir recours à des ORM (Object-Relational Mapping), qui permettent de gérer l’accès à une base de données de manière sécurisée (en théorie).

En effet, un ORM consiste en un ensemble de classes et de méthodes permettant de manipuler les tables d’une base de données relationnelle comme des objets.

Ici, nous allons nous intéresser à TypeORM, un ORM pour TypeScript et JavaScript. Le développeur écrira alors des modèles qui pourront être en relation les uns avec les autres. Ce qui nous intéresse, ce sont les propriétés des relations entre deux modèles, notamment celles de cascade.

Si cette propriété est définie comme étant vraie entre deux entités, alors mettre à jour un côté de la relation et faire la sauvegarde sur l’autre mettra à jour les deux côtés dans la base de données, et pourra même créer de nouveaux objets.

Cependant, comme indiqué dans la documentation des cascades, ces dernières peuvent sembler être un moyen simple et efficace de travailler avec des relations, mais elles peuvent également être à l’origine de bugs et de problèmes de sécurité. De plus, elles constituent un moyen moins explicite d’enregistrer de nouveaux objets dans la base de données.

Cela n’empêche cependant pas les développeurs de les utiliser. Il faut dire que c’est quand même bien pratique lorsqu’il y a une mise à jour de plusieurs entités en relation de n’avoir à en sauvegarder qu’une seule pour sauvegarder les autres.

Scénario d’exploitation des relations en cascade avec un défaut de validation des entrées utilisateurs

Intéressons-nous à un exemple concret, inspiré d’un cas rencontré lors d’un pentest d’application web.

Il est commun pour des applications de séparer la gestion de chaque ressource et d’implémenter des tests de sécurité à l’endroit qui correspond.

On peut donc imaginer une application avec des utilisateurs et des documents associés.

ts 
@Entity()
class User {
        @PrimaryGeneratedColumn()
        id!: number

        @Column()
        name!: string

        @Column({ default: false })
        admin!: boolean
}

@Entity() 
class Document {
        @PrimaryGeneratedColumn()
        id!: number

        @Column()
        content!: String

        @Column()
        authorId!: number

        @OneToOne(() => User, { cascade: true })
        @JoinColumn()
        author!: User
}

Comme on peut le voir, les deux ressources sont reliées grâce à la relation OneToOne sur Document qui a la particularité d’être en cascade.

On peut désormais imaginer que chaque ressource possède son propre contrôleur.

Par exemple, celui pour les utilisateurs pourrait permettre la lecture de n’importe quelle ressource, mais limiter la mise à jour de champs protégés (comme les rôles par exemple) ou plus simplement ne pas permettre la mise à jour d’un utilisateur.

ts
app.get("/user", async (_request, reply) => {
        reply.send(await users.findOneBy({ id: 1 }))
})

Alors que le contrôleur de la ressource associée serait un peu plus permissif en permettant la mise à jour de tous les champs en prenant directement l’entrée de l’utilisateur et la sauvegarderait directement en base de données.

ts
app.get("/document", async (_request, reply) => {
        reply.send(await documents.findOneBy({ id: 1 }))
})

app.put("/document", async (request, reply) => {
        const result = { ...request.body as any, id: 1 }
        reply.send(await documents.save(result))
})

Ici, si on jette un coup d’œil à notre application, il est possible de récupérer un utilisateur.

GET /user

{"id":1,"name":"Dummy user","admin":false}

Il est aussi possible de récupérer le document qui lui est associé.

GET /document

{"id":1,"content":"My document content!","authorId":1}

Et aussi de mettre à jour ce document.

PUT /document

{"content":"Modified content"}
GET /document 

{"id":1,"content":"Modified content","authorId":1}

Cependant, la fonctionnalité de mise à jour du document devient vulnérable à cause de la propriété cascade de la relation. En effet, un attaquant pourrait lors de la mise à jour du document mettre aussi à jour l’utilisateur associé en le mettant administrateur par exemple, ce qui n’aurait pas été possible avec le contrôleur de l’utilisateur.

PUT /document

{"content":"Modified content","author":{"id":1,"admin":true}}

En faisant cette requête l’attaquant va ajouter l’objet utilisateur qui correspond à l’auteur du document, en sauvegardant le document, l’utilisateur associé sera aussi sauvegardé à cause de la propriété cascade.

GET /user

{"id":1,"name":"Dummy user","admin":true}

En regardant l’utilisateur, nous voyons que ses propriétés ont bien été mises à jour.

Pour prévenir ce type de risques, il est recommandé d’éviter les relations en cascade, mais encore plus important, de bien vérifier ce qui est envoyé par l’utilisateur. En effet, l’objet pour l’auteur n’aurait jamais dû être présent lors de la sauvegarde de la ressource, il aurait fallu filtrer les propriétés autorisées.

Conclusion

L’utilisation d’ORM peut être très utile afin de réduire le risque d’injection ainsi que le temps de développement et de maintenance des applications. Cependant, cette pratique peut également entraîner des problèmes de sécurité si elle est mal gérée. Une bonne pratique est d’utiliser des propriétés de cascade avec prudence et de bien contrôler les entrées des utilisateurs afin de s’assurer que seules les données autorisées sont sauvegardées.

Auteur : Arnaud PASCAL – Pentester @Vaadata