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>
1251 lines
32 KiB
Markdown
1251 lines
32 KiB
Markdown
# 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
|