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>
32 KiB
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 не установлен:
if (webhookSecret) {
event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
} else {
event = JSON.parse(req.body.toString()); // Нет проверки!
}
Риск: Атакующий может подделать Stripe webhooks → создать заказы без оплаты.
Fix: Сделать STRIPE_WEBHOOK_SECRET обязательным — проверка на старте сервера:
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: Добавить проверку на старте:
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:
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:
npm install express-validator
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:
- Регистрация на doppler.com
- Создать проект WellNuo
- Добавить все секреты через UI
- Установить CLI на сервер:
curl -Ls https://cli.doppler.com/install.sh | sh doppler login doppler setup - Изменить запуск в PM2:
doppler run -- node index.js - Удалить
.envфайл
Effort: 2-3 часа
6. VULN-008: Dependency Vulnerability Fix
Проблема: Пакет qs имеет известную DoS уязвимость.
Fix:
cd backend
npm update qs
npm audit fix
Effort: 30 минут
ЧАСТЬ 2: Рекомендации (делаем потом)
Backend Security
VULN-006 + INFO-002 + INFO-003: Логирование и обработка ошибок
Комплексная задача — объединили 3 пункта.
Что делаем:
-
Winston для structured logs:
npm install winstonconst 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' }) ] }); -
Sentry для алертов:
npm install @sentry/nodeconst Sentry = require('@sentry/node'); Sentry.init({ dsn: process.env.SENTRY_DSN }); -
Audit Trail таблица:
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() ); -
Generic error messages в production:
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 только для разрешённых доменов:
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:
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 токена нет.
Рекомендуемое:
- Access token: 15-60 минут
- Refresh token: 7 дней
- При истечении access token — автоматический refresh
Реализация:
// Генерация токенов
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 (объединено)
Статус: В рекомендациях
Что делаем:
- Удалить
app/(tabs)/bug.tsx - Добавить SSL pinning для API:
npm install react-native-ssl-pinning - Проверка URL в WebView:
if (!url.startsWith('https://wellnuo.smartlaunchhub.com')) { return false; }
FE-006: Console.log Removal
Статус: В рекомендациях
Fix: Добавить babel plugin для production:
npm install babel-plugin-transform-remove-console --save-dev
// babel.config.js
module.exports = {
presets: ['babel-preset-expo'],
env: {
production: {
plugins: ['transform-remove-console']
}
}
};
FE-007: JWT Expiration Check
Статус: В рекомендациях
Fix: Проверять токен перед запросами:
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:
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:
npx expo install expo-local-authentication
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 на кнопки:
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:
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:
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
anyTypes (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:
{
"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)
Статус: В рекомендациях
Подход: Обезличивание данных (не полное удаление):
// 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:
{
"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 часов
После релиза (по приоритету)
- RLS (Row Level Security) — полная переделка защиты данных
- Логирование (Winston + Sentry + Audit Trail)
- SSL Pinning для API и WebView
- Bug fixes (race conditions, memory leaks)
- Account deletion (anonymization)
- Biometric auth
- Refresh tokens
- Code quality improvements
ЧАСТЬ 4: RLS (Row Level Security) — Полная реализация
Что это и зачем
Текущая защита: Проверки доступа в коде backend. Если баг в коде — данные утекут.
С RLS: Защита на уровне базы данных. Даже при баге в коде, SQL injection, или прямом доступе к БД — пользователь увидит только свои данные.
Аналогия: Сейчас охранник проверяет пропуск на входе. С RLS — каждая дверь открывается только твоим ключом.
План реализации
Этап 1: Подготовка (2-3 часа)
1.1 Создать роль для приложения
-- Создать роль для 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 данными
-- Основные таблицы
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
-- 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
-- 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
-- Устройства видны тем, кто имеет доступ к 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
-- Заказы видны только владельцу
CREATE POLICY orders_select ON orders
FOR SELECT
USING (
user_id = current_setting('app.current_user_id', true)
);
2.5 Политика для users (собственный профиль)
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
// 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
// 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 ДО
// Старый код
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 ПОСЛЕ
// Новый код с 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
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 Интеграционные тесты
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 Миграция
-- 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 Деплой
# 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