# WellNuo Security & Quality Audit Report **Дата:** 2026-01-22 **Версия:** 2.0 (финальная после ревью) --- ## Резюме После ревью из 90 найденных пунктов: - **6 задач** — делаем обязательно (критичные) - **45 рекомендаций** — делаем потом - **39 пунктов** — убрали (не актуально, дубликаты, false positive) --- # ЧАСТЬ 1: Обязательные задачи ## Критичные задачи (делаем сейчас) ### 1. VULN-001: Stripe Webhook Signature Required **Файл:** `backend/src/routes/webhook.js:22-28` **Проблема:** Webhook signature verification можно обойти если `STRIPE_WEBHOOK_SECRET` не установлен: ```javascript if (webhookSecret) { event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret); } else { event = JSON.parse(req.body.toString()); // Нет проверки! } ``` **Риск:** Атакующий может подделать Stripe webhooks → создать заказы без оплаты. **Fix:** Сделать `STRIPE_WEBHOOK_SECRET` обязательным — проверка на старте сервера: ```javascript if (!process.env.STRIPE_WEBHOOK_SECRET) { console.error('STRIPE_WEBHOOK_SECRET is required!'); process.exit(1); } ``` **Effort:** 1 час --- ### 2. VULN-003: JWT Secret Validation on Startup **Файл:** `backend/src/index.js` **Проблема:** API запускается без проверки силы JWT_SECRET. **Риск:** Слабый secret = брутфорс токенов. **Fix:** Добавить проверку на старте: ```javascript if (!process.env.JWT_SECRET || process.env.JWT_SECRET.length < 32) { console.error('JWT_SECRET must be at least 32 characters!'); process.exit(1); } ``` **Effort:** 30 минут --- ### 3. VULN-004: OTP Rate Limiting **Файл:** `backend/src/routes/auth.js:92` **Проблема:** 6 цифр = 900,000 вариантов, нет rate limiting на verify-otp. **Риск:** Брутфорс OTP за минуты — атакующий может войти в любой аккаунт. **Fix:** Добавить rate limit: ```javascript const rateLimit = require('express-rate-limit'); const otpLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 минут max: 5, // максимум 5 попыток message: { error: 'Too many attempts, try again in 15 minutes' }, keyGenerator: (req) => req.body.email || req.ip }); router.post('/verify-otp', otpLimiter, async (req, res) => { ... }); ``` **Effort:** 2 часа --- ### 4. VULN-005: Input Validation **Файлы:** `beneficiaries.js:356`, `stripe.js:54`, `invitations.js:238` **Проблема:** Пользовательский ввод не валидируется. **Риск:** SQL injection, XSS, некорректные данные в БД. **Fix:** Добавить express-validator: ```bash npm install express-validator ``` ```javascript const { body, validationResult } = require('express-validator'); router.post('/beneficiaries', body('name').isString().trim().isLength({ min: 1, max: 200 }), body('email').optional().isEmail(), async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } // ... остальной код } ); ``` **Effort:** 4 часа --- ### 5. VULN-007: Secrets Management (Doppler) **Проблема:** Все секреты хранятся в `.env` файле на сервере. **Текущие секреты:** - DB_PASSWORD - JWT_SECRET - BREVO_API_KEY - STRIPE_SECRET_KEY - STRIPE_WEBHOOK_SECRET - ADMIN_API_KEY - LEGACY_API_PASSWORD - LIVEKIT_API_KEY - LIVEKIT_API_SECRET **Fix:** Перейти на Doppler: 1. Регистрация на doppler.com 2. Создать проект WellNuo 3. Добавить все секреты через UI 4. Установить CLI на сервер: ```bash curl -Ls https://cli.doppler.com/install.sh | sh doppler login doppler setup ``` 5. Изменить запуск в PM2: ```bash doppler run -- node index.js ``` 6. Удалить `.env` файл **Effort:** 2-3 часа --- ### 6. VULN-008: Dependency Vulnerability Fix **Проблема:** Пакет `qs` имеет известную DoS уязвимость. **Fix:** ```bash cd backend npm update qs npm audit fix ``` **Effort:** 30 минут --- # ЧАСТЬ 2: Рекомендации (делаем потом) ## Backend Security ### VULN-006 + INFO-002 + INFO-003: Логирование и обработка ошибок **Комплексная задача — объединили 3 пункта.** **Что делаем:** 1. **Winston для structured logs:** ```bash npm install winston ``` ```javascript const winston = require('winston'); const logger = winston.createLogger({ level: 'info', format: winston.format.json(), transports: [ new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }) ] }); ``` 2. **Sentry для алертов:** ```bash npm install @sentry/node ``` ```javascript const Sentry = require('@sentry/node'); Sentry.init({ dsn: process.env.SENTRY_DSN }); ``` 3. **Audit Trail таблица:** ```sql CREATE TABLE audit_log ( id SERIAL PRIMARY KEY, user_id TEXT, action TEXT, -- 'login', 'update_beneficiary', 'delete_user' entity_type TEXT, -- 'beneficiary', 'user', 'order' entity_id TEXT, old_values JSONB, new_values JSONB, ip_address TEXT, created_at TIMESTAMP DEFAULT NOW() ); ``` 4. **Generic error messages в production:** ```javascript app.use((err, req, res, next) => { logger.error(err.stack); Sentry.captureException(err); res.status(500).json({ error: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message }); }); ``` **Effort:** 1 день --- ### VULN-009: Admin API 2FA **Статус:** В рекомендациях (не срочно) **Проблема:** Admin routes только JWT + role check, нет 2FA. **Fix (когда понадобится):** Добавить TOTP через `speakeasy` или email code для admin операций. --- ### VULN-010: CORS без Origin **Статус:** В рекомендациях **Fix:** Настроить CORS только для разрешённых доменов: ```javascript const corsOptions = { origin: ['https://wellnuo.smartlaunchhub.com', 'exp://'], credentials: true }; ``` --- ### VULN-011: Rate Limiting настройка **Статус:** В рекомендациях (связано с VULN-004) **Рекомендуемые лимиты:** | Endpoint | Лимит | |----------|-------| | `/verify-otp` | 5/15min | | `/send-otp` | 3/15min | | Остальные | 100/15min | --- ### VULN-012: Request Size Limit **Статус:** В рекомендациях **Fix:** ```javascript app.use(express.json({ limit: '1mb' })); ``` --- ### VULN-013: Sensitive Data в логах **Статус:** В рекомендациях (решится с Winston) --- ### VULN-015: CSRF Protection **Статус:** В рекомендациях **Fix:** Для web версии добавить `csurf` middleware или использовать SameSite cookies. --- ### VULN-017: Short-lived JWT + Refresh Tokens **Статус:** В рекомендациях **Текущее:** JWT живёт 7 дней, refresh токена нет. **Рекомендуемое:** 1. Access token: 15-60 минут 2. Refresh token: 7 дней 3. При истечении access token — автоматический refresh **Реализация:** ```javascript // Генерация токенов const accessToken = jwt.sign({ userId }, JWT_SECRET, { expiresIn: '15m' }); const refreshToken = jwt.sign({ userId }, REFRESH_SECRET, { expiresIn: '7d' }); // Сохранить refresh token в БД await supabase.from('refresh_tokens').insert({ user_id: userId, token: refreshToken }); // Endpoint для refresh router.post('/refresh', async (req, res) => { const { refreshToken } = req.body; // Проверить в БД, сгенерировать новый access token }); ``` **Effort:** 1 неделя (backend + frontend) --- ## Frontend Security ### FE-001: Stripe Key в Env **Статус:** В рекомендациях **Fix:** Вынести в `EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY` --- ### FE-003: Legacy Credentials **Статус:** В рекомендациях **Проблема:** Hardcoded `anandk` / `anandk_8` в `services/api.ts:1444-1446` **Fix:** Перенести на backend proxy или убрать если не используется. --- ### FE-004 + FE-008: SSL Pinning (объединено) **Статус:** В рекомендациях **Что делаем:** 1. Удалить `app/(tabs)/bug.tsx` 2. Добавить SSL pinning для API: ```bash npm install react-native-ssl-pinning ``` 3. Проверка URL в WebView: ```javascript if (!url.startsWith('https://wellnuo.smartlaunchhub.com')) { return false; } ``` --- ### FE-006: Console.log Removal **Статус:** В рекомендациях **Fix:** Добавить babel plugin для production: ```bash npm install babel-plugin-transform-remove-console --save-dev ``` ```javascript // babel.config.js module.exports = { presets: ['babel-preset-expo'], env: { production: { plugins: ['transform-remove-console'] } } }; ``` --- ### FE-007: JWT Expiration Check **Статус:** В рекомендациях **Fix:** Проверять токен перед запросами: ```javascript function isTokenExpired(token) { const payload = JSON.parse(atob(token.split('.')[1])); return payload.exp * 1000 < Date.now(); } ``` --- ### FE-010: SecureStore для Sensitive Data **Статус:** В рекомендациях **Fix:** Заменить AsyncStorage на SecureStore для: - Tokens - User credentials - Любые sensitive данные --- ### FE-013: Developer Mode в Production **Статус:** В рекомендациях **Fix:** Проверить и убрать dev features из production build. --- ### FE-014: Deep Links Validation **Статус:** В рекомендациях **Fix:** Валидировать параметры deep links: ```javascript function handleDeepLink(url) { const parsed = Linking.parse(url); if (!isValidRoute(parsed.path)) return; if (!isValidParams(parsed.queryParams)) return; // ... обработка } ``` --- ### FE-017: Biometric Auth **Статус:** В рекомендациях **Fix:** Добавить Face ID / Touch ID: ```bash npx expo install expo-local-authentication ``` ```javascript import * as LocalAuthentication from 'expo-local-authentication'; const authenticate = async () => { const result = await LocalAuthentication.authenticateAsync({ promptMessage: 'Authenticate to access WellNuo', fallbackLabel: 'Use passcode' }); return result.success; }; ``` --- ### FE-019: Client-Side Rate Limiting **Статус:** В рекомендациях **Fix:** Добавить debounce на кнопки: ```javascript import { debounce } from 'lodash'; const handleSubmit = debounce(async () => { // ... отправка }, 1000, { leading: true, trailing: false }); ``` --- ## Code Quality Bugs ### BUG-001: Race Condition in AuthContext **Файл:** `contexts/AuthContext.tsx:44-47` **Fix:** Исправить зависимости useEffect, использовать useCallback. --- ### BUG-002: Stale Closure in addLocalBeneficiary **Файл:** `contexts/BeneficiaryContext.tsx:59-85` **Fix:** Использовать functional update: ```javascript setLocalBeneficiaries(prev => [...prev, newBeneficiary]); ``` --- ### BUG-003: Silent Auth Failures **Файл:** `services/api.ts:316-384` **Fix:** Не маскировать ошибки — правильно обрабатывать 401. --- ### BUG-004: Missing Dependencies in loadBeneficiary **Файл:** `app/(tabs)/beneficiaries/[id]/index.tsx:146-196` **Fix:** Добавить `id` в зависимости useEffect. --- ### BUG-005: Memory Leak — Interval Not Cleaned **Файл:** `app/(tabs)/beneficiaries/[id]/index.tsx:107-144` **Fix:** ```javascript useEffect(() => { const interval = setInterval(loadData, 30 * 60 * 1000); return () => clearInterval(interval); // cleanup! }, []); ``` --- ### LOGIC-001 — LOGIC-002, RACE-001 — RACE-002, STATE-001, ERROR-001, NULL-001 **Статус:** В рекомендациях — исправить при рефакторинге. --- ### SMELL-001 — SMELL-006: Code Smells **Статус:** В рекомендациях — исправить при рефакторинге: - Duplicate Navigation Logic - Magic Numbers → Constants - Mixed Auth Token Types - Excessive `any` Types (40+) - Callback Hell → async/await - Silent Fallbacks → add logging --- ### OPT-001 — OPT-002: Performance - Добавить useMemo для дорогих вычислений - Убрать лишний JSON parsing каждый render --- ### CLEAN-001 — CLEAN-002: Cleanup - Удалить закомментированный код - Удалить unused imports --- ### TYPE-001, TEST-001, DOC-001 - Добавить input validation на фронте - Добавить тесты для error states - Улучшить имена переменных --- ## App Store Compliance ### PERMISSION-001 + PERMISSION-002: Permission Descriptions **Статус:** В рекомендациях **Fix:** Обновить в `app.json`: ```json { "expo": { "ios": { "infoPlist": { "NSCameraUsageDescription": "WellNuo needs camera access to take photos for beneficiary profiles", "NSPhotoLibraryUsageDescription": "WellNuo needs photo library access to select profile pictures for your loved ones", "NSMicrophoneUsageDescription": "WellNuo uses microphone for voice conversations with AI assistant about your loved one's wellbeing" } } } } ``` --- ### DATA-001: Account Deletion (Anonymization) **Статус:** В рекомендациях **Подход:** Обезличивание данных (не полное удаление): ```javascript // Backend endpoint router.delete('/auth/account', async (req, res) => { const userId = req.user.id; await supabase.from('users').update({ email: `deleted_${userId}@anonymized.local`, first_name: null, last_name: null, phone: null, avatar_url: null, deleted_at: new Date().toISOString() }).eq('id', userId); // Удалить токены await supabase.from('refresh_tokens').delete().eq('user_id', userId); res.json({ success: true }); }); ``` --- ### PRIVACY-001: Privacy Policy URL **Статус:** В рекомендациях **Fix:** Добавить в `app.json`: ```json { "expo": { "ios": { "privacyManifests": { "NSPrivacyAccessedAPITypes": [] } } } } ``` --- ### PRIVACY-002: Privacy Manifest (iOS 17+) **Статус:** В рекомендациях **Fix:** Создать/заполнить `ios/WellNuo/PrivacyInfo.xcprivacy` --- ### BACKGROUND-001: Background Modes Justification **Статус:** В рекомендациях **Что объяснить Apple:** - **BLE background** — подключение к сенсорам WellNuo для мониторинга здоровья - **Audio background** — голосовое общение с AI ассистентом --- ### CONTENT-001: AI Chat Consent **Статус:** В рекомендациях **Fix:** Добавить consent dialog перед первым использованием AI чата. --- ### TEST-001: Test Features in Production **Статус:** В рекомендациях **Fix:** Убедиться что тестовые фичи скрыты в production. --- ### UX-001: Demo Mode Handling **Статус:** В рекомендациях **Fix:** Улучшить UX для demo режима. --- ### MARKETING-001: App Store Materials **Статус:** В рекомендациях **Что подготовить:** - Screenshots для всех размеров - App description - Keywords - What's New text --- # ЧАСТЬ 3: Приоритеты ## Сейчас (перед релизом) | # | Задача | Effort | |---|--------|--------| | 1 | VULN-001: Stripe webhook required | 1h | | 2 | VULN-003: JWT Secret validation | 30m | | 3 | VULN-004: OTP rate limiting | 2h | | 4 | VULN-005: Input validation | 4h | | 5 | VULN-007: Doppler secrets | 3h | | 6 | VULN-008: npm audit fix | 30m | **Общее время:** ~11 часов ## После релиза (по приоритету) 1. **RLS (Row Level Security)** — полная переделка защиты данных 2. **Логирование** (Winston + Sentry + Audit Trail) 3. **SSL Pinning** для API и WebView 4. **Bug fixes** (race conditions, memory leaks) 5. **Account deletion** (anonymization) 6. **Biometric auth** 7. **Refresh tokens** 8. **Code quality** improvements --- # ЧАСТЬ 4: RLS (Row Level Security) — Полная реализация ## Что это и зачем **Текущая защита:** Проверки доступа в коде backend. Если баг в коде — данные утекут. **С RLS:** Защита на уровне базы данных. Даже при баге в коде, SQL injection, или прямом доступе к БД — пользователь увидит только свои данные. **Аналогия:** Сейчас охранник проверяет пропуск на входе. С RLS — каждая дверь открывается только твоим ключом. --- ## План реализации ### Этап 1: Подготовка (2-3 часа) #### 1.1 Создать роль для приложения ```sql -- Создать роль для backend приложения CREATE ROLE wellnuo_app LOGIN PASSWORD 'strong_password_here'; -- Дать права на таблицы GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO wellnuo_app; GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO wellnuo_app; ``` #### 1.2 Включить RLS на всех таблицах с sensitive данными ```sql -- Основные таблицы ALTER TABLE beneficiaries ENABLE ROW LEVEL SECURITY; ALTER TABLE user_access ENABLE ROW LEVEL SECURITY; ALTER TABLE devices ENABLE ROW LEVEL SECURITY; ALTER TABLE orders ENABLE ROW LEVEL SECURITY; ALTER TABLE subscriptions ENABLE ROW LEVEL SECURITY; ALTER TABLE deployments ENABLE ROW LEVEL SECURITY; ALTER TABLE invitations ENABLE ROW LEVEL SECURITY; -- Принудительно применять RLS даже для владельца таблицы ALTER TABLE beneficiaries FORCE ROW LEVEL SECURITY; ALTER TABLE user_access FORCE ROW LEVEL SECURITY; ALTER TABLE devices FORCE ROW LEVEL SECURITY; ALTER TABLE orders FORCE ROW LEVEL SECURITY; ALTER TABLE subscriptions FORCE ROW LEVEL SECURITY; ALTER TABLE deployments FORCE ROW LEVEL SECURITY; ALTER TABLE invitations FORCE ROW LEVEL SECURITY; ``` --- ### Этап 2: Создание политик (4-6 часов) #### 2.1 Политика для beneficiaries ```sql -- SELECT: пользователь видит только beneficiaries к которым имеет доступ CREATE POLICY beneficiaries_select ON beneficiaries FOR SELECT USING ( id IN ( SELECT beneficiary_id FROM user_access WHERE accessor_id = current_setting('app.current_user_id', true) ) ); -- UPDATE: только custodian может редактировать CREATE POLICY beneficiaries_update ON beneficiaries FOR UPDATE USING ( id IN ( SELECT beneficiary_id FROM user_access WHERE accessor_id = current_setting('app.current_user_id', true) AND role = 'custodian' ) ); -- DELETE: только custodian может удалять CREATE POLICY beneficiaries_delete ON beneficiaries FOR DELETE USING ( id IN ( SELECT beneficiary_id FROM user_access WHERE accessor_id = current_setting('app.current_user_id', true) AND role = 'custodian' ) ); -- INSERT: любой авторизованный пользователь может создать CREATE POLICY beneficiaries_insert ON beneficiaries FOR INSERT WITH CHECK ( current_setting('app.current_user_id', true) IS NOT NULL ); ``` #### 2.2 Политика для user_access ```sql -- SELECT: видеть записи где ты accessor или beneficiary CREATE POLICY user_access_select ON user_access FOR SELECT USING ( accessor_id = current_setting('app.current_user_id', true) OR beneficiary_id IN ( SELECT beneficiary_id FROM user_access WHERE accessor_id = current_setting('app.current_user_id', true) AND role = 'custodian' ) ); -- INSERT: только custodian может добавлять доступ CREATE POLICY user_access_insert ON user_access FOR INSERT WITH CHECK ( beneficiary_id IN ( SELECT beneficiary_id FROM user_access WHERE accessor_id = current_setting('app.current_user_id', true) AND role = 'custodian' ) ); -- DELETE: только custodian может удалять доступ CREATE POLICY user_access_delete ON user_access FOR DELETE USING ( beneficiary_id IN ( SELECT beneficiary_id FROM user_access WHERE accessor_id = current_setting('app.current_user_id', true) AND role = 'custodian' ) ); ``` #### 2.3 Политика для devices ```sql -- Устройства видны тем, кто имеет доступ к beneficiary CREATE POLICY devices_select ON devices FOR SELECT USING ( beneficiary_id IN ( SELECT beneficiary_id FROM user_access WHERE accessor_id = current_setting('app.current_user_id', true) ) ); ``` #### 2.4 Политика для orders ```sql -- Заказы видны только владельцу CREATE POLICY orders_select ON orders FOR SELECT USING ( user_id = current_setting('app.current_user_id', true) ); ``` #### 2.5 Политика для users (собственный профиль) ```sql ALTER TABLE users ENABLE ROW LEVEL SECURITY; ALTER TABLE users FORCE ROW LEVEL SECURITY; -- Пользователь видит только себя CREATE POLICY users_select ON users FOR SELECT USING ( id = current_setting('app.current_user_id', true) ); -- Пользователь редактирует только себя CREATE POLICY users_update ON users FOR UPDATE USING ( id = current_setting('app.current_user_id', true) ); ``` --- ### Этап 3: Переделка Query Builder (1-2 дня) #### 3.1 Новый database.js с поддержкой RLS ```javascript // backend/src/config/database.js const { Pool } = require('pg'); const pool = new Pool({ host: process.env.DB_HOST, port: process.env.DB_PORT, database: process.env.DB_NAME, user: 'wellnuo_app', // Используем роль с RLS password: process.env.DB_PASSWORD, ssl: { rejectUnauthorized: false } }); /** * Выполнить запрос от имени пользователя (с RLS) */ async function queryAsUser(userId, queryText, params = []) { const client = await pool.connect(); try { await client.query('BEGIN'); // Установить текущего пользователя для RLS await client.query( `SET LOCAL app.current_user_id = $1`, [userId] ); // Выполнить основной запрос const result = await client.query(queryText, params); await client.query('COMMIT'); return result; } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } } /** * Транзакция от имени пользователя */ async function transactionAsUser(userId, callback) { const client = await pool.connect(); try { await client.query('BEGIN'); await client.query(`SET LOCAL app.current_user_id = $1`, [userId]); const result = await callback(client); await client.query('COMMIT'); return result; } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } } module.exports = { pool, queryAsUser, transactionAsUser }; ``` #### 3.2 Обновлённый Query Builder ```javascript // backend/src/config/supabase.js (переименовать в db.js) const { queryAsUser, transactionAsUser } = require('./database'); class QueryBuilder { constructor(tableName, userId) { this.tableName = tableName; this.userId = userId; // Обязательно передавать userId this._select = '*'; this._where = []; this._whereParams = []; // ... остальные поля } // ... существующие методы (select, eq, etc.) async execute() { const sql = this._buildQuery(); // Все запросы идут через RLS const result = await queryAsUser( this.userId, sql, this._whereParams ); return { data: this._single ? result.rows[0] : result.rows, error: null, count: this._count ? result.rowCount : undefined }; } } // Фабрика с привязкой к пользователю function createDb(userId) { return { from: (tableName) => new QueryBuilder(tableName, userId) }; } module.exports = { createDb }; ``` --- ### Этап 4: Обновление всех routes (2-3 дня) #### 4.1 Пример: beneficiaries.js ДО ```javascript // Старый код const { supabase } = require('../config/supabase'); router.get('/me/beneficiaries', auth, async (req, res) => { const { data, error } = await supabase .from('beneficiaries') .select('*'); // Ручная фильтрация по доступу const accessible = data.filter(b => hasAccess(req.user.id, b.id)); res.json(accessible); }); ``` #### 4.2 Пример: beneficiaries.js ПОСЛЕ ```javascript // Новый код с RLS const { createDb } = require('../config/db'); router.get('/me/beneficiaries', auth, async (req, res) => { const db = createDb(req.user.id); // Привязка к пользователю const { data, error } = await db .from('beneficiaries') .select('*'); // RLS автоматически вернёт только доступные! res.json(data); }); ``` #### 4.3 Файлы для обновления ``` backend/src/routes/ ├── admin.js # Отдельная логика для админов ├── auth.js # Без RLS (регистрация/логин) ├── beneficiaries.js # ✅ RLS ├── deployments.js # ✅ RLS ├── invitations.js # ✅ RLS ├── notification-settings.js # ✅ RLS ├── orders.js # ✅ RLS ├── push-tokens.js # ✅ RLS ├── stripe.js # ✅ RLS └── webhook.js # Без RLS (Stripe callbacks) backend/src/controllers/ ├── alarm.js # ✅ RLS ├── auth.js # Частично (profile = RLS) ├── beneficiary.js # ✅ RLS ├── caretaker.js # ✅ RLS ├── dashboard.js # ✅ RLS ├── deployment.js # ✅ RLS ├── device.js # ✅ RLS ├── sensor.js # ✅ RLS └── voice.js # ✅ RLS ``` --- ### Этап 5: Тестирование (1 день) #### 5.1 Unit тесты для RLS ```javascript describe('RLS Policies', () => { it('user can only see own beneficiaries', async () => { const db = createDb('user-1'); const { data } = await db.from('beneficiaries').select('*'); // Все возвращённые beneficiaries должны быть доступны user-1 for (const b of data) { const hasAccess = await checkAccess('user-1', b.id); expect(hasAccess).toBe(true); } }); it('user cannot see other users beneficiaries', async () => { const db = createDb('user-1'); const { data } = await db .from('beneficiaries') .select('*') .eq('id', 'beneficiary-of-user-2'); expect(data).toHaveLength(0); // RLS блокирует }); it('only custodian can update beneficiary', async () => { const db = createDb('guardian-user'); // не custodian const { error } = await db .from('beneficiaries') .update({ name: 'Hacked' }) .eq('id', 'some-beneficiary'); expect(error).toBeTruthy(); // RLS блокирует }); }); ``` #### 5.2 Интеграционные тесты ```javascript describe('API with RLS', () => { it('GET /me/beneficiaries returns only accessible', async () => { const res = await request(app) .get('/api/me/beneficiaries') .set('Authorization', `Bearer ${user1Token}`); expect(res.status).toBe(200); // Проверить что все beneficiaries принадлежат user1 }); it('cannot access other user beneficiary by ID', async () => { const res = await request(app) .get('/api/me/beneficiaries/other-user-beneficiary-id') .set('Authorization', `Bearer ${user1Token}`); expect(res.status).toBe(404); // RLS скрывает }); }); ``` --- ### Этап 6: Миграция и деплой (2-3 часа) #### 6.1 Миграция ```sql -- migrations/010_enable_rls.sql -- 1. Создать роль CREATE ROLE wellnuo_app LOGIN PASSWORD 'xxx'; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO wellnuo_app; GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO wellnuo_app; -- 2. Включить RLS ALTER TABLE beneficiaries ENABLE ROW LEVEL SECURITY; ALTER TABLE beneficiaries FORCE ROW LEVEL SECURITY; -- ... остальные таблицы -- 3. Создать политики CREATE POLICY beneficiaries_select ON beneficiaries FOR SELECT USING (...); -- ... остальные политики ``` #### 6.2 Деплой ```bash # 1. Применить миграцию ssh root@91.98.205.156 cd /var/www/wellnuo-api node run-migration.js # 2. Обновить код git pull origin main # 3. Обновить .env (новый пароль для wellnuo_app) # DB_USER=wellnuo_app # DB_PASSWORD=new_password # 4. Рестарт pm2 restart wellnuo-api # 5. Проверить логи pm2 logs wellnuo-api --lines 50 ``` --- ## Результат После внедрения RLS: | Сценарий | Без RLS | С RLS | |----------|---------|-------| | Баг в коде пропускает проверку | ❌ Данные утекают | ✅ БД блокирует | | SQL injection | ❌ Доступ ко всему | ✅ Только свои данные | | Прямой доступ к БД | ❌ Всё видно | ✅ Только свои данные | | Забыли добавить проверку в новом endpoint | ❌ Дыра | ✅ RLS защищает | --- ## Временные затраты | Этап | Время | |------|-------| | 1. Подготовка (роли, включение RLS) | 2-3 часа | | 2. Создание политик | 4-6 часов | | 3. Переделка Query Builder | 1-2 дня | | 4. Обновление routes/controllers | 2-3 дня | | 5. Тестирование | 1 день | | 6. Миграция и деплой | 2-3 часа | | **ИТОГО** | **1-2 недели** | --- ## Чеклист готовности - [ ] Роль `wellnuo_app` создана - [ ] RLS включен на всех таблицах - [ ] Политики созданы и протестированы - [ ] Query Builder переделан - [ ] Все routes используют `createDb(userId)` - [ ] Unit тесты пройдены - [ ] Интеграционные тесты пройдены - [ ] Миграция применена на проде - [ ] Код задеплоен - [ ] Мониторинг ошибок настроен --- **Файл:** `/Users/sergei/Desktop/WellNuo/AUDIT_REPORT.md` **Обновлён:** 2026-01-22