- XSS (Cross-Site Scripting)
- Спам и злоупотребление
- Утечка данных
- DoS (Denial of Service)
Злоумышленник может попытаться внедрить JavaScript через:
- Поле
textзаметки - Поле
title - Поле
selector
1. Валидация входных данных
// Только plain text, никакого HTML
const sanitizeText = (text) => {
return text
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\//g, '/')
.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' })
// Стили и скрипты страницы не влияют на наш UI3. Content Security Policy
// В manifest.json
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
}Генерация:
// При установке расширения (один раз)
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 создания
Защита от:
- Спам-флуда заметками
- DoS атаки на API
- Злоупотребления анонимностью
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
})// 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": [
"storage", // Для локального UUID
"activeTab" // Только активная вкладка
],
"host_permissions": [
"https://*/", // Content script на HTTPS
"http://*/" // Content script на HTTP (для тестирования)
]
}❌ tabs — не нужна история вкладок
❌ webNavigation — не нужно отслеживать навигацию
❌ cookies — не нужны cookie
❌ identity — нет OAuth
- Обнаружение: bug report, security audit
- Оценка: критичность (low/medium/high/critical)
- Патч: фикс в приоритетном порядке
- Деплой: немедленно для critical
- Disclosure: публикация после фикса
Security issues: security@example.com (после публикации)
- XSS защита протестирована
- Rate limiting активирован
- HTTPS включен (production)
- Content Security Policy настроена
- Минимальные permissions в manifest
- Санитизация всех входных данных
- textContent вместо innerHTML
- Shadow DOM изоляция
- Мониторинг suspicious activity
- Регулярные бэкапы БД
- Обновление зависимостей
- Логи проверяются на утечки данных
- In-memory rate limiting — сбрасывается при рестарте
- Нет модерации — спам возможен
- Нет CAPTCHA — автоматизированный спам не предотвращен
- Нет IP ban — повторные нарушители не блокируются
Решения после MVP:
- Redis для персистентного rate limiting
- Флаги спама с порогом автоскрытия
- CAPTCHA при подозрительной активности
- IP blacklist для chronic abusers