From 291aa5491231348e6d0d3550b904370b507fbcb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 11:36:20 +0400 Subject: [PATCH] fix: corriger l'IDOR sur update/delete (parole, artiste, commentaire) --- src/api/artiste/routes/artiste.js | 6 ++ src/api/commentaire/routes/commentaire.js | 6 ++ src/api/parole/routes/parole.js | 6 ++ .../__tests__/is-document-owner.test.js | 73 +++++++++++++++++++ src/policies/is-document-owner.js | 35 +++++++++ 5 files changed, 126 insertions(+) create mode 100644 src/policies/__tests__/is-document-owner.test.js create mode 100644 src/policies/is-document-owner.js diff --git a/src/api/artiste/routes/artiste.js b/src/api/artiste/routes/artiste.js index 7e331e3..55c06de 100644 --- a/src/api/artiste/routes/artiste.js +++ b/src/api/artiste/routes/artiste.js @@ -6,6 +6,12 @@ module.exports = createCoreRouter('api::artiste.artiste', { config: { create: { policies: ['global::is-payload-owner'] + }, + update: { + policies: [{name: 'global::is-document-owner', config: {uid: 'api::artiste.artiste'}}] + }, + delete: { + policies: [{name: 'global::is-document-owner', config: {uid: 'api::artiste.artiste'}}] } } }) diff --git a/src/api/commentaire/routes/commentaire.js b/src/api/commentaire/routes/commentaire.js index 2a5dc0e..1418b6a 100644 --- a/src/api/commentaire/routes/commentaire.js +++ b/src/api/commentaire/routes/commentaire.js @@ -6,6 +6,12 @@ module.exports = createCoreRouter('api::commentaire.commentaire', { config: { create: { policies: ['global::is-payload-owner'] + }, + update: { + policies: [{name: 'global::is-document-owner', config: {uid: 'api::commentaire.commentaire'}}] + }, + delete: { + policies: [{name: 'global::is-document-owner', config: {uid: 'api::commentaire.commentaire'}}] } } }) diff --git a/src/api/parole/routes/parole.js b/src/api/parole/routes/parole.js index 58106f4..69c520c 100644 --- a/src/api/parole/routes/parole.js +++ b/src/api/parole/routes/parole.js @@ -6,6 +6,12 @@ module.exports = createCoreRouter('api::parole.parole', { config: { create: { policies: ['global::is-payload-owner'] + }, + update: { + policies: [{name: 'global::is-document-owner', config: {uid: 'api::parole.parole'}}] + }, + delete: { + policies: [{name: 'global::is-document-owner', config: {uid: 'api::parole.parole'}}] } } }) diff --git a/src/policies/__tests__/is-document-owner.test.js b/src/policies/__tests__/is-document-owner.test.js new file mode 100644 index 0000000..c25f614 --- /dev/null +++ b/src/policies/__tests__/is-document-owner.test.js @@ -0,0 +1,73 @@ +import {describe, it, expect, vi} from 'vitest' + +const {default: isDocumentOwner} = await import('../is-document-owner.js') + +function buildStrapi({jwtUserId, document}) { + const dbQuery = { + findOne: vi.fn(async () => document) + } + + return { + plugins: { + 'users-permissions': { + services: { + jwt: { + getToken: vi.fn(async () => ({id: jwtUserId})) + } + } + } + }, + db: { + query: vi.fn(() => dbQuery) + }, + dbQuery + } +} + +function buildPolicyContext({authorization = 'Bearer faketoken', paramId, bodyDocumentId} = {}) { + return { + params: paramId ? {id: paramId} : {}, + request: { + header: authorization ? {authorization} : {}, + body: {data: {documentId: bodyDocumentId}} + } + } +} + +describe('is-document-owner policy', () => { + it("refuse quand aucun en-tête d'autorisation n'est présent", async () => { + const strapi = buildStrapi({jwtUserId: 1, document: {documentId: 'doc-1', user: {id: 1}}}) + const policyContext = buildPolicyContext({authorization: null, paramId: 'doc-1'}) + + await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).rejects.toThrow('Opération non autorisée') + }) + + it('autorise quand le user du JWT est le propriétaire du document (id dans les params)', async () => { + const strapi = buildStrapi({jwtUserId: 1, document: {documentId: 'doc-1', user: {id: 1}}}) + const policyContext = buildPolicyContext({paramId: 'doc-1'}) + + await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).resolves.toBe(true) + expect(strapi.dbQuery.findOne).toHaveBeenCalledWith({where: {documentId: 'doc-1'}, populate: {user: true}}) + }) + + it('autorise quand le documentId vient du corps de la requête (cas du contrôleur parole.update)', async () => { + const strapi = buildStrapi({jwtUserId: 1, document: {documentId: 'doc-1', user: {id: 1}}}) + const policyContext = buildPolicyContext({bodyDocumentId: 'doc-1'}) + + await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).resolves.toBe(true) + }) + + it("refuse quand le user du JWT n'est pas le propriétaire du document", async () => { + const strapi = buildStrapi({jwtUserId: 999, document: {documentId: 'doc-1', user: {id: 1}}}) + const policyContext = buildPolicyContext({paramId: 'doc-1'}) + + await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).rejects.toThrow('Opération non autorisée') + }) + + it("refuse quand le document ciblé n'existe pas", async () => { + const strapi = buildStrapi({jwtUserId: 1, document: null}) + const policyContext = buildPolicyContext({paramId: 'doc-inconnu'}) + + await expect(isDocumentOwner(policyContext, {uid: 'api::parole.parole'}, {strapi})).rejects.toThrow('Ressource introuvable.') + }) +}) diff --git a/src/policies/is-document-owner.js b/src/policies/is-document-owner.js new file mode 100644 index 0000000..f25bed7 --- /dev/null +++ b/src/policies/is-document-owner.js @@ -0,0 +1,35 @@ +'use strict'; + +const { UnauthorizedError, NotFoundError } = require('@strapi/utils').errors + +module.exports = async (policyContext, config, {strapi}) => { + const {request, params} = policyContext + + if (!request?.header?.authorization) { + throw new UnauthorizedError('Opération non autorisée') + } + + let jwtUserId + try { + ({id: jwtUserId} = await strapi.plugins['users-permissions'].services.jwt.getToken(policyContext)) + } catch (err) { + throw new UnauthorizedError('Opération non autorisée') + } + + const documentId = params?.id ?? request.body?.data?.documentId + + const document = await strapi.db.query(config.uid).findOne({ + where: {documentId}, + populate: {user: true} + }) + + if (!document) { + throw new NotFoundError('Ressource introuvable.') + } + + if (document.user?.id !== jwtUserId) { + throw new UnauthorizedError('Opération non autorisée') + } + + return true +}