From 8476e9cfca10632e23900b3a771ef6de9330f58d Mon Sep 17 00:00:00 2001 From: rodielavender Date: Wed, 13 May 2026 23:29:00 +0000 Subject: [PATCH] =?UTF-8?q?fix(security):=20IDOR=20=D0=BD=D0=B0=20PUT/DELE?= =?UTF-8?q?TE=20/api/offices/:id=20=E2=80=94=20=D0=BF=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=BA=D0=B0=20ownership?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/__tests__/offices.test.js | 60 ++++++++++++++++++++++++++ server/controllers/officeController.js | 35 +++++++++++++-- 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/server/__tests__/offices.test.js b/server/__tests__/offices.test.js index 3f77091..3ff45e3 100644 --- a/server/__tests__/offices.test.js +++ b/server/__tests__/offices.test.js @@ -86,4 +86,64 @@ describe('PUT /api/offices/:officeId', () => { expect(rows[0].name).toBe('Renamed Office'); expect(rows[0].address).toBe('addr2'); }); + + it('returns 403 when a different user tries to update someone else\'s office (IDOR)', async () => { + const owner = await registerDirector(app); + const stranger = await registerDirector(app); + + const created = await request(app) + .post('/api/offices') + .set('Authorization', `Bearer ${owner.token}`) + .send({ name: 'Owner Office', address: 'private addr' }); + const officeId = created.body.id; + + const res = await request(app) + .put(`/api/offices/${officeId}`) + .set('Authorization', `Bearer ${stranger.token}`) + .send({ name: 'HACKED', address: 'evil addr' }); + + expect(res.status).toBe(403); + + const [rows] = await db.query('SELECT name FROM offices WHERE id = ?', [officeId]); + expect(rows[0].name).toBe('Owner Office'); + }); +}); + +describe('DELETE /api/offices/:officeId', () => { + it('owner can delete own office', async () => { + const owner = await registerDirector(app); + const created = await request(app) + .post('/api/offices') + .set('Authorization', `Bearer ${owner.token}`) + .send({ name: 'To be deleted' }); + const officeId = created.body.id; + + const res = await request(app) + .delete(`/api/offices/${officeId}`) + .set('Authorization', `Bearer ${owner.token}`); + + expect([200, 204]).toContain(res.status); + const [rows] = await db.query('SELECT id FROM offices WHERE id = ?', [officeId]); + expect(rows.length).toBe(0); + }); + + it('returns 403 when a stranger tries to delete someone else\'s office (IDOR)', async () => { + const owner = await registerDirector(app); + const stranger = await registerDirector(app); + + const created = await request(app) + .post('/api/offices') + .set('Authorization', `Bearer ${owner.token}`) + .send({ name: 'Owner Office 2' }); + const officeId = created.body.id; + + const res = await request(app) + .delete(`/api/offices/${officeId}`) + .set('Authorization', `Bearer ${stranger.token}`); + + expect(res.status).toBe(403); + + const [rows] = await db.query('SELECT id FROM offices WHERE id = ?', [officeId]); + expect(rows.length).toBe(1); + }); }); diff --git a/server/controllers/officeController.js b/server/controllers/officeController.js index 0975d8f..afd30d0 100644 --- a/server/controllers/officeController.js +++ b/server/controllers/officeController.js @@ -2,6 +2,23 @@ const Office = require('../models/office'); const { formatOfficeResponse } = require('../utils/formatters'); const db = require('../db'); +/** + * Проверяет, что текущий пользователь — владелец офиса. + * Возвращает true для системной роли 'owner' или если offices.owner_id === user.id. + * Используется для гейта операций PUT/DELETE на офисе. + */ +async function isOfficeOwner(user, officeId) { + if (!user || !user.id || !officeId) return false; + const role = String(user.role || '').toLowerCase(); + if (role === 'owner') return true; + const [rows] = await db.query( + 'SELECT owner_id FROM offices WHERE id = ? LIMIT 1', + [officeId] + ); + if (!rows[0]) return false; + return Number(rows[0].owner_id) === Number(user.id); +} + const officeController = { /** * Получить данные о выручке офисов за указанный период @@ -219,12 +236,17 @@ const officeController = { try { const { officeId } = req.params; const { name, address, contact_phone, website } = req.body; - + const existingOffice = await Office.getById(officeId); if (!existingOffice) { return res.status(404).json({ success: false, message: 'Офис не найден' }); } - + + const allowed = await isOfficeOwner(req.user, officeId); + if (!allowed) { + return res.status(403).json({ success: false, message: 'Доступ запрещён' }); + } + if (!name) { return res.status(400).json({ success: false, message: 'Название офиса обязательно' }); } @@ -247,12 +269,17 @@ const officeController = { async deleteOffice(req, res) { try { const { officeId } = req.params; - + const office = await Office.getById(officeId); if (!office) { return res.status(404).json({ success: false, message: 'Офис не найден' }); } - + + const allowed = await isOfficeOwner(req.user, officeId); + if (!allowed) { + return res.status(403).json({ success: false, message: 'Доступ запрещён' }); + } + await Office.delete(officeId); return res.json({ success: true }); } catch (error) {