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

32 KiB
Raw Blame History

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:

  1. Регистрация на doppler.com
  2. Создать проект WellNuo
  3. Добавить все секреты через UI
  4. Установить CLI на сервер:
    curl -Ls https://cli.doppler.com/install.sh | sh
    doppler login
    doppler setup
    
  5. Изменить запуск в PM2:
    doppler run -- node index.js
    
  6. Удалить .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 пункта.

Что делаем:

  1. Winston для structured logs:

    npm install winston
    
    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 для алертов:

    npm install @sentry/node
    
    const Sentry = require('@sentry/node');
    Sentry.init({ dsn: process.env.SENTRY_DSN });
    
  3. 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()
    );
    
  4. 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 токена нет.

Рекомендуемое:

  1. Access token: 15-60 минут
  2. Refresh token: 7 дней
  3. При истечении 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 (объединено)

Статус: В рекомендациях

Что делаем:

  1. Удалить app/(tabs)/bug.tsx
  2. Добавить SSL pinning для API:
    npm install react-native-ssl-pinning
    
  3. Проверка 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.


Статус: В рекомендациях

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 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:

{
  "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 ассистентом

Статус: В рекомендациях

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 Создать роль для приложения

-- Создать роль для 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