WellNuo/AUDIT_REPORT.md
Sergei f0d39af6dc Add security audit report and PRD for custom names
AUDIT_REPORT.md:
- Full security audit (90 findings reviewed)
- 6 critical tasks for immediate fix
- 45 recommendations for later
- Complete RLS implementation plan (1-2 weeks)
- Doppler for secrets management
- Winston + Sentry for logging

PRD.md:
- Personalized beneficiary names feature
- custom_name in user_access table
- Backend + Frontend tasks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 18:52:01 -08:00

1251 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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