Skip to content

Security: listeomin/Anotum

Security

docs/security.md

Security — Безопасность

Основные угрозы

  1. XSS (Cross-Site Scripting)
  2. Спам и злоупотребление
  3. Утечка данных
  4. DoS (Denial of Service)

XSS защита

Вектор атаки

Злоумышленник может попытаться внедрить JavaScript через:

  • Поле text заметки
  • Поле title
  • Поле selector

Защита на сервере

1. Валидация входных данных

// Только plain text, никакого HTML
const sanitizeText = (text) => {
  return text
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;')
    .replace(/\//g, '&#x2F;')
    .trim()
}

// Перед сохранением
note.text = sanitizeText(note.text)
note.title = sanitizeText(note.title || '')

2. Белый список символов для selector

const SAFE_SELECTOR_REGEX = /^[a-zA-Z0-9\s\.\#\[\]\:\(\)\>\+\~\-\_]+$/

const validateSelector = (selector) => {
  if (!selector) return true
  return SAFE_SELECTOR_REGEX.test(selector)
}

Защита на клиенте

1. Рендер через textContent

// ✅ Правильно
const noteText = document.createElement('div')
noteText.textContent = note.text  // Безопасно

// ❌ Опасно
element.innerHTML = note.text  // XSS уязвимость!

2. Shadow DOM изоляция

// Создаем изолированный Shadow DOM для слоя заметок
const shadowRoot = container.attachShadow({ mode: 'closed' })

// Стили и скрипты страницы не влияют на наш UI

3. Content Security Policy

// В manifest.json
"content_security_policy": {
  "extension_pages": "script-src 'self'; object-src 'self'"
}

Анонимность

Локальный UUID

Генерация:

// При установке расширения (один раз)
import { v4 as uuidv4 } from 'uuid'

const generateDeviceUUID = async () => {
  let uuid = await chrome.storage.local.get('device_uuid')
  
  if (!uuid.device_uuid) {
    uuid.device_uuid = uuidv4()
    await chrome.storage.local.set({ device_uuid: uuid.device_uuid })
  }
  
  return uuid.device_uuid
}

Хранение:

  • chrome.storage.local — не синхронизируется между устройствами
  • Не передается в headers (только в body запросов)
  • Не логируется на сервере

Что НЕ собираем

❌ IP адреса (только для rate limiting, не сохраняем) ❌ User-Agent ❌ Fingerprinting данные ❌ История посещений ❌ Поведенческие данные

Что собираем

author_id — только для удаления своих заметок ✅ url — для привязки заметок к странице ✅ created_at — timestamp создания


Rate Limiting

Цель

Защита от:

  • Спам-флуда заметками
  • DoS атаки на API
  • Злоупотребления анонимностью

Реализация (MVP)

In-memory rate limiter:

const rateLimit = require('express-rate-limit')

// POST /notes
const createLimiter = rateLimit({
  windowMs: 60 * 1000,  // 1 минута
  max: 10,               // 10 запросов
  keyGenerator: (req) => req.body.author_id,
  message: { error: 'Rate limit exceeded', retry_after: 60 }
})

// DELETE /notes
const deleteLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 10,
  keyGenerator: (req) => req.body.author_id
})

// GET /notes (по IP)
const readLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 60,
  standardHeaders: true,
  legacyHeaders: false
})

Ограничения MVP

⚠️ In-memory store сбрасывается при рестарте сервера ⚠️ Не работает при horizontal scaling

Будущее улучшение

// Redis-backed rate limiting
const RedisStore = require('rate-limit-redis')
const redis = require('redis')

const client = redis.createClient()

const limiter = rateLimit({
  store: new RedisStore({ client }),
  windowMs: 60 * 1000,
  max: 10
})

Анти-спам

Валидация на уровне данных

1. Длина текста

const NOTE_TEXT_MIN = 1
const NOTE_TEXT_MAX = 1000
const NOTE_TITLE_MAX = 120

if (text.length < NOTE_TEXT_MIN || text.length > NOTE_TEXT_MAX) {
  throw new ValidationError('Invalid text length')
}

2. Повторяющиеся заметки

// Проверка на дубли (последние 10 минут, тот же author_id + url)
const recentNotes = await db.prepare(`
  SELECT text FROM notes
  WHERE author_id = ? AND url = ?
  AND created_at > datetime('now', '-10 minutes')
`).all(author_id, url)

const isDuplicate = recentNotes.some(n => n.text === text)
if (isDuplicate) {
  throw new Error('Duplicate note detected')
}

3. Частота создания

// Не больше 3 заметок на одной странице за 1 минуту
const recentCount = await db.prepare(`
  SELECT COUNT(*) as count FROM notes
  WHERE author_id = ? AND url = ?
  AND created_at > datetime('now', '-1 minute')
`).get(author_id, url)

if (recentCount.count >= 3) {
  throw new Error('Too many notes created recently')
}

Обнаружение паттернов

1. Одинаковые заметки на разных URL

// После MVP: флаг подозрительной активности
const suspiciousPattern = await db.prepare(`
  SELECT COUNT(DISTINCT url) as url_count
  FROM notes
  WHERE author_id = ? AND text = ?
  AND created_at > datetime('now', '-1 hour')
`).get(author_id, text)

if (suspiciousPattern.url_count > 10) {
  // Возможно спам — требуется модерация
}

Защита данных

Хранение

SQLite файл:

  • Локально на сервере
  • Не содержит чувствительных данных
  • Регулярные бэкапы

Шифрование:

  • MVP: не требуется (все публичное)
  • После MVP: можно зашифровать приватные заметки

Передача

HTTPS обязателен в production:

// Middleware для редиректа на HTTPS
app.use((req, res, next) => {
  if (req.header('x-forwarded-proto') !== 'https' && process.env.NODE_ENV === 'production') {
    res.redirect(`https://${req.header('host')}${req.url}`)
  } else {
    next()
  }
})

Логирование

Что логируем:

// Только минимум для отладки
logger.info('Note created', {
  note_id: note.id,
  url: note.url,
  text_length: note.text.length
  // НЕ логируем author_id!
})

Что НЕ логируем:

  • author_id (кроме критических ошибок)
  • IP адреса
  • Полный текст заметок

Permissions (Chrome Extension)

Минимальные разрешения

{
  "permissions": [
    "storage",      // Для локального UUID
    "activeTab"     // Только активная вкладка
  ],
  "host_permissions": [
    "https://*/",   // Content script на HTTPS
    "http://*/"     // Content script на HTTP (для тестирования)
  ]
}

Что НЕ запрашиваем

tabs — не нужна история вкладок ❌ webNavigation — не нужно отслеживать навигацию ❌ cookies — не нужны cookie ❌ identity — нет OAuth


Vulnerability Response

Процесс обработки

  1. Обнаружение: bug report, security audit
  2. Оценка: критичность (low/medium/high/critical)
  3. Патч: фикс в приоритетном порядке
  4. Деплой: немедленно для critical
  5. Disclosure: публикация после фикса

Контакты

Security issues: security@example.com (после публикации)

Чек-лист безопасности

Pre-launch

  • XSS защита протестирована
  • Rate limiting активирован
  • HTTPS включен (production)
  • Content Security Policy настроена
  • Минимальные permissions в manifest
  • Санитизация всех входных данных
  • textContent вместо innerHTML
  • Shadow DOM изоляция

Post-launch

  • Мониторинг suspicious activity
  • Регулярные бэкапы БД
  • Обновление зависимостей
  • Логи проверяются на утечки данных

Известные ограничения MVP

  1. In-memory rate limiting — сбрасывается при рестарте
  2. Нет модерации — спам возможен
  3. Нет CAPTCHA — автоматизированный спам не предотвращен
  4. Нет IP ban — повторные нарушители не блокируются

Решения после MVP:

  • Redis для персистентного rate limiting
  • Флаги спама с порогом автоскрытия
  • CAPTCHA при подозрительной активности
  • IP blacklist для chronic abusers

There aren’t any published security advisories