From b238124d2d727bfe916905c6616f1bdd3efaf62e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 30 May 2026 08:06:15 +0300 Subject: [PATCH] =?UTF-8?q?research:=204=20=D0=BF=D1=80=D0=BE=D0=BC=D0=BF?= =?UTF-8?q?=D1=82=D0=B0=20=D0=B4=D0=BB=D1=8F=20Opus=20=E2=80=94=20queries,?= =?UTF-8?q?=20server,=20schema,=20ejs,=20auth-arch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- research/prompt-opus-auth.md | 49 +++++++ research/prompt-opus-ejs.md | 245 ++++++++++++++++++++++++++++++++ research/prompt-opus-queries.md | 168 ++++++++++++++++++++++ research/prompt-opus-schema.md | 59 ++++++++ research/prompt-opus-server.md | 124 ++++++++++++++++ 5 files changed, 645 insertions(+) create mode 100644 research/prompt-opus-auth.md create mode 100644 research/prompt-opus-ejs.md create mode 100644 research/prompt-opus-queries.md create mode 100644 research/prompt-opus-schema.md create mode 100644 research/prompt-opus-server.md diff --git a/research/prompt-opus-auth.md b/research/prompt-opus-auth.md new file mode 100644 index 0000000..c523c43 --- /dev/null +++ b/research/prompt-opus-auth.md @@ -0,0 +1,49 @@ +# Промпт для Claude Opus 4 — архитектура OIDC/JWT для внешнего auth-api + +## Контекст (не анализируй) + +Делаем микросервис на Node.js + Express. Он стоит ЗА общим auth-api платформы. Платформа сама не наша, мы не можем менять Keycloak. + +**Схема авторизации платформы:** +1. Браузер → lk-api-gateway → Keycloak (логин) +2. После логина платформа обменивает code на токен через СВОЙ auth-api +3. auth-api выпускает JWT с `"iss": "auth-api"` (не Keycloak!) +4. Фронтенд платформы хранит access_token в localStorage и шлёт `Authorization: Bearer ` к своему бэкенду + +**Нам неизвестно:** +- JWKS URL auth-api (публичный ключ для проверки подписи) +- Как именно фронтенд платформы будет вызывать НАШ сервис (прямой запрос браузера с токеном? Или через их API-гейтвей?) +- Есть ли у нас доступ к этому auth-api или только к самому JWT + +**Реальный JWT (из HAR трафика):** +```json +{ + "iss": "auth-api", + "sub": "0199e325-1cdf-7cda-9319-e5302a85e291", + "ClientID": "WZ01325", + "company_id": "3e64aac6-dcfc-4082-88dc-da19c86555a5", + "company_name": "Тест", + "email": "tazet@narod.ru", + "token_type": "access", + "realm_access": { "roles": null }, + "resource_access": { "account": { "roles": null } }, + "groups": null +} +``` + +## Что нужно + +Предложи стратегию проверки токенов в нашем сервисе. Мы не знаем JWKS URL auth-api и не имеем к нему доступа (пока). Нужно найти золотую середину между «вообще не проверяем подпись» и «требуем JWKS которого нет». + +Конкретные вопросы: +1. Если JWKS недоступен — что проверять ВМЕСТО подписи? (exp, iss, audience?) +2. Может ли наш сервис валидировать iss='auth-api' без криптографии? +3. Стоит ли делать промежуточный вариант: проверять exp+iss сейчас, а JWKS добавить когда дадут URL? +4. Как защититься от подделки токена если подпись не проверяется? +5. Нужен ли нам client_secret/shared secret с auth-api? + +Ограничения: +- Не предлагай «спросить у команды платформы» — мы и так спросим, но ответа пока нет +- Не предлагай поднять свой Keycloak +- Только практические варианты, которые можно закодить сейчас +- ВЕСЬ ОТВЕТ ОДНИМ БЛОКОМ — без свёрток, без интерактивных элементов diff --git a/research/prompt-opus-ejs.md b/research/prompt-opus-ejs.md new file mode 100644 index 0000000..5295827 --- /dev/null +++ b/research/prompt-opus-ejs.md @@ -0,0 +1,245 @@ +# Промпт для Claude Opus 4 — XSS, CSRF, безопасность index.ejs + +## Контекст (не анализируй) +Node.js + Express + EJS. Шаблон серверного рендеринга. Данные приходят из БД (email пользователя, CIDR, комментарий) и из query-параметров (error, message). EJS по умолчанию экранирует `<%= ... %>`, НО не экранирует `<%- ... %>`. В этом шаблоне `<%-` не используется. + +## Что нужно +Ниже полный `index.ejs`. Найди: +- XSS-векторы (экранирование, query-параметры в URL) +- CSRF (нет токена в формах) +- Clickjacking (отсутствие X-Frame-Options / CSP frame-ancestors) +- Утечка данных (видны ли clientId/email других компаний?) +- Client-side валидация (отсутствует, хотя ТЗ требует) +- Проблемы с disabled-полями (можно ли обойти через DevTools?) + +Ограничения: +- Не предлагай менять стек/фреймворк +- Только конкретные строки с исправлениями +- ВЕСЬ ОТВЕТ ОДНИМ БЛОКОМ — без свёрток, без интерактивных элементов, чистый текст + +```html + + + + + Белые списки IP — Nubes + + + + + +
+ ... + | + Белые списки IP +
+ +
+ + <% if (message) { %>
<%= message %>
<% } %> + <% if (error) { %>
<%= error %>
<% } %> + +
+
+
+
+
<%= used %> / <%= limit %>
+
записей
+
+
+
<%= user.clientId %>
+
компания
+
+
+
<%= user.email %>
+
пользователь
+
+
+
+
+ +
+
Добавить адрес
+
+
+
+
+ + = limit ? 'disabled' : '' %>> +
+
+ + +
+ +
+
+
+
+ +
+
Доверенные адреса
+ <% if (entries.length === 0) { %> +
Нет добавленных адресов
+ <% } else { %> +
+ + + + + + + + + + + + <% entries.forEach(e => { %> + + + + + + + + <% }) %> + +
Адрес / ПодсетьКомментарийДобавилДатаДействие
<%= e.value_cidr %><%= e.comment || '—' %><%= e.created_by %><%= new Date(e.created_at).toLocaleDateString('ru', {day:'numeric',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit'}) %> +
+ +
+
+
+ <% } %> +
+ +
+ + +``` diff --git a/research/prompt-opus-queries.md b/research/prompt-opus-queries.md new file mode 100644 index 0000000..b9d53a3 --- /dev/null +++ b/research/prompt-opus-queries.md @@ -0,0 +1,168 @@ +# Промпт для Claude Opus 4 — гонки, транзакции, безопасность в queries.js + +## Контекст (не анализируй) +Node.js + Express + PostgreSQL (pg pool). Микросервис IP WhiteList. Клиенты создают до 15 доверенных IPv4/CIDR. Многоарендность (изоляция по company_id). Аудит всех изменений обязателен. + +## Что нужно +Ниже полный код `queries.js`. Найди: +- Race conditions (TOCTOU между проверкой лимита и INSERT) +- Отсутствие транзакций там где они нужны +- SQL-инъекции +- Ошибки изоляции (может ли пользователь компании А затронуть записи компании Б?) +- Проблемы с audit_log (пишется ли при ошибках?) + +Ограничения: +- Не предлагай менять стек +- Только конкретные строки с исправлениями +- ВЕСЬ ОТВЕТ ОДНИМ БЛОКОМ — без свёрток, без интерактивных элементов, без ссылок. Чистый текст. + +```js +const { pool } = require('./db'); +const { validate, overlaps } = require('./validators'); + +// ── Companies ── + +async function getOrCreateCompany(clientId, companyName) { + let res = await pool.query('SELECT * FROM companies WHERE client_id = $1', [clientId]); + if (res.rows.length > 0) return res.rows[0]; + + res = await pool.query( + 'INSERT INTO companies (client_id, name) VALUES ($1, $2) RETURNING *', + [clientId, companyName || clientId] + ); + return res.rows[0]; +} + +async function getLimit(company) { + const defaultLimit = parseInt(process.env.DEFAULT_LIMIT, 10) || 15; + return company.custom_limit || defaultLimit; +} + +// ── Entries ── + +async function listEntries(companyId, includeDeleted = false) { + let sql = 'SELECT * FROM whitelist_entries WHERE company_id = $1'; + if (!includeDeleted) sql += ' AND deleted_at IS NULL'; + sql += ' ORDER BY created_at DESC'; + return (await pool.query(sql, [companyId])).rows; +} + +async function createEntry(companyId, rawValue, comment, userEmail) { + const { cidr, wasNormalized } = validate(rawValue); + + // Проверка лимита + const company = (await pool.query('SELECT * FROM companies WHERE id = $1', [companyId])).rows[0]; + const limit = await getLimit(company); + const cnt = (await pool.query( + 'SELECT COUNT(*)::int AS c FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL', + [companyId] + )).rows[0].c; + + if (cnt >= limit) throw new Error(`Лимит исчерпан: ${cnt} из ${limit}`); + + // Проверка дубликатов и пересечений + const existing = (await pool.query( + 'SELECT value_cidr FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL', + [companyId] + )).rows; + + for (const row of existing) { + if (row.value_cidr === cidr) throw new Error('Такой адрес уже существует'); + if (overlaps(cidr, row.value_cidr)) + throw new Error(`Пересечение с существующей записью ${row.value_cidr}`); + } + + const res = await pool.query( + `INSERT INTO whitelist_entries (company_id, value_cidr, comment, created_by) + VALUES ($1, $2, $3, $4) RETURNING *`, + [companyId, cidr, comment || null, userEmail] + ); + + // Аудит + await logAudit(userEmail, companyId, 'CREATE', null, cidr, res.rows[0].id); + + return { entry: res.rows[0], wasNormalized }; +} + +async function updateEntry(entryId, companyId, rawValue, comment, userEmail) { + const old = (await pool.query( + 'SELECT * FROM whitelist_entries WHERE id = $1 AND company_id = $2 AND deleted_at IS NULL', + [entryId, companyId] + )).rows[0]; + if (!old) throw new Error('Запись не найдена'); + + const { cidr, wasNormalized } = validate(rawValue); + + const existing = (await pool.query( + 'SELECT value_cidr FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL AND id <> $2', + [companyId, entryId] + )).rows; + for (const row of existing) { + if (row.value_cidr === cidr) throw new Error('Такой адрес уже существует'); + if (overlaps(cidr, row.value_cidr)) + throw new Error(`Пересечение с существующей записью ${row.value_cidr}`); + } + + const res = await pool.query( + `UPDATE whitelist_entries SET value_cidr = $1, comment = $2, updated_by = $3, updated_at = NOW() + WHERE id = $4 AND company_id = $5 RETURNING *`, + [cidr, comment || old.comment, userEmail, entryId, companyId] + ); + + await logAudit(userEmail, companyId, 'UPDATE', old.value_cidr, cidr, entryId); + + return { entry: res.rows[0], wasNormalized }; +} + +async function deleteEntry(entryId, companyId, userEmail) { + const old = (await pool.query( + 'SELECT * FROM whitelist_entries WHERE id = $1 AND company_id = $2 AND deleted_at IS NULL', + [entryId, companyId] + )).rows[0]; + if (!old) throw new Error('Запись не найдена'); + + await pool.query( + 'UPDATE whitelist_entries SET deleted_by = $1, deleted_at = NOW() WHERE id = $2', + [userEmail, entryId] + ); + + await logAudit(userEmail, companyId, 'DELETE', old.value_cidr, null, entryId); +} + +// ── Export ── + +async function getExportCIDRs() { + const rows = (await pool.query( + 'SELECT value_cidr FROM whitelist_entries WHERE deleted_at IS NULL ORDER BY value_cidr' + )).rows; + return rows.map(r => r.value_cidr); +} + +// ── Audit ── + +async function logAudit(userEmail, companyId, action, oldValue, newValue, entryId) { + await pool.query( + `INSERT INTO audit_log (user_email, company_id, action, old_value, new_value, entry_id) + VALUES ($1, $2, $3, $4, $5, $6)`, + [userEmail, companyId, action, oldValue, newValue, entryId || null] + ); +} + +async function getAudit(companyId = null) { + let sql = 'SELECT * FROM audit_log'; + const params = []; + if (companyId) { + sql += ' WHERE company_id = $1'; + params.push(companyId); + } + sql += ' ORDER BY created_at DESC LIMIT 500'; + return (await pool.query(sql, params)).rows; +} + +module.exports = { + getOrCreateCompany, getLimit, + listEntries, createEntry, updateEntry, deleteEntry, + getExportCIDRs, + getAudit, +}; +``` diff --git a/research/prompt-opus-schema.md b/research/prompt-opus-schema.md new file mode 100644 index 0000000..0186f07 --- /dev/null +++ b/research/prompt-opus-schema.md @@ -0,0 +1,59 @@ +# Промпт для Claude Opus 4 — ревью схемы БД + +## Контекст (не анализируй) +PostgreSQL. Микросервис IP WhiteList. Многоарендность: у каждой компании (companies) свои записи (whitelist_entries). Лимит по умолчанию 15 активных записей на компанию, custom_limit переопределяет. Soft delete. Аудит всех изменений (audit_log). Сервис на Node.js + pg pool. + +## Что нужно +Ниже `schema.sql`. Найди: +- Отсутствующие индексы (полный скан таблиц под нагрузкой) +- Отсутствующие уникальные constraint'ы (дубликаты на уровне БД, не только в коде) +- Проблемы с внешними ключами (каскадное удаление?) +- Неоптимальные типы данных +- Уязвимости в структуре (можно ли обойти изоляцию через БД?) +- Что добавить для production + +Ограничения: +- Только конкретные DDL-строки +- ВЕСЬ ОТВЕТ ОДНИМ БЛОКОМ — без свёрток, без интерактивных элементов + +```sql +-- IP WhiteList schema + +CREATE TABLE IF NOT EXISTS companies ( + id SERIAL PRIMARY KEY, + client_id VARCHAR(64) UNIQUE NOT NULL, + name VARCHAR(255), + custom_limit INTEGER DEFAULT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS whitelist_entries ( + id SERIAL PRIMARY KEY, + company_id INTEGER NOT NULL REFERENCES companies(id), + value_cidr VARCHAR(18) NOT NULL, + comment VARCHAR(255), + created_by VARCHAR(255) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_by VARCHAR(255), + updated_at TIMESTAMPTZ, + deleted_by VARCHAR(255), + deleted_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_entries_active + ON whitelist_entries(company_id, value_cidr) WHERE deleted_at IS NULL; + +CREATE TABLE IF NOT EXISTS audit_log ( + id SERIAL PRIMARY KEY, + user_email VARCHAR(255) NOT NULL, + company_id INTEGER NOT NULL, + action VARCHAR(32) NOT NULL, + old_value TEXT, + new_value TEXT, + entry_id INTEGER, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_audit_company ON audit_log(company_id); +``` diff --git a/research/prompt-opus-server.md b/research/prompt-opus-server.md new file mode 100644 index 0000000..17d7c07 --- /dev/null +++ b/research/prompt-opus-server.md @@ -0,0 +1,124 @@ +# Промпт для Claude Opus 4 — безопасность server.js (auth, CSRF, XSS) + +## Контекст (не анализируй) +Node.js + Express + EJS + PostgreSQL. Микросервис IP WhiteList для облачного провайдера. Аутентификация через внешний auth-api (JWT в Authorization header). Сейчас DEV_MODE=true — временная заглушка. Продакшен с реальными токенами. + +## Что нужно +Ниже полный код `server.js`. Найди: +- Баги в auth middleware (обход при DEV_MODE, пустой req.user) +- CSRF-уязвимости (POST-формы без токенов) +- XSS через query-параметры (error в редиректе) +- Race conditions на уровне роутов +- Проблемы обработки ошибок +- Небезопасные HTTP-заголовки +- Отсутствующие middleware (helmet, rate-limit, etc.) +- Несоответствия ТЗ (где обещано, но не реализовано) + +Ограничения: +- Не предлагай менять стек +- Только конкретные строки и исправления +- ВЕСЬ ОТВЕТ ОДНИМ БЛОКОМ — без свёрток, без интерактивных элементов. Чистый текст. + +```js +const express = require('express'); +const path = require('path'); +require('dotenv').config(); +const { checkConnection } = require('./src/db'); +const q = require('./src/queries'); + +const app = express(); +const PORT = process.env.PORT || 3000; +const DEV = process.env.DEV_MODE === 'true'; + +app.set('view engine', 'ejs'); +app.set('views', path.join(__dirname, 'views')); +app.use(express.static(path.join(__dirname, 'public'))); +app.use(express.urlencoded({ extended: true })); + +// ── Auth middleware ── +app.use((req, res, next) => { + if (DEV) { + req.user = { email: 'dev@test.local', clientId: 'WZ01325', companyId: null, companyName: 'DEV' }; + return next(); + } + const auth = req.headers.authorization || ''; + try { + const payload = JSON.parse(Buffer.from(auth.replace('Bearer ', '').split('.')[1], 'base64').toString()); + req.user = { + email: payload.email || 'unknown', + clientId: payload.ClientID, + companyId: payload.company_id, + companyName: payload.company_name || payload.ClientID, + }; + } catch { req.user = {}; } + next(); +}); + +// ── Health ── +app.get('/healthz', (req, res) => res.send('OK')); + +// ── Главная ── +app.get('/', async (req, res) => { + const { clientId, companyName } = req.user; + try { + const company = await q.getOrCreateCompany(clientId, companyName); + const limit = await q.getLimit(company); + const entries = await q.listEntries(company.id); + res.render('index', { entries, limit, used: entries.length, user: req.user, error: null, message: null, wasNormalized: false }); + } catch (e) { + res.render('index', { entries: [], limit: 15, used: 0, user: req.user, error: e.message, message: null, wasNormalized: false }); + } +}); + +// ── Создать ── +app.post('/add', async (req, res) => { + const { value, comment } = req.body; + const { clientId, companyName, email } = req.user; + try { + const company = await q.getOrCreateCompany(clientId, companyName); + const result = await q.createEntry(company.id, value, comment, email); + const limit = await q.getLimit(company); + const entries = await q.listEntries(company.id); + res.render('index', { + entries, limit, used: entries.length, user: req.user, + message: result.wasNormalized ? `Адрес нормализован в ${result.entry.value_cidr}` : 'Добавлено', + error: null, wasNormalized: result.wasNormalized, + }); + } catch (e) { + const company = await q.getOrCreateCompany(clientId, companyName).catch(() => null); + const entries = company ? await q.listEntries(company.id).catch(() => []) : []; + const limit = company ? await q.getLimit(company).catch(() => 15) : 15; + res.render('index', { entries, limit, used: entries.length, user: req.user, error: e.message, message: null, wasNormalized: false }); + } +}); + +// ── Удалить (soft) ── +app.post('/delete/:id', async (req, res) => { + const { clientId, companyName, email } = req.user; + try { + const company = await q.getOrCreateCompany(clientId, companyName); + await q.deleteEntry(req.params.id, company.id, email); + res.redirect('/'); + } catch (e) { + res.redirect('/?error=' + encodeURIComponent(e.message)); + } +}); + +// ── Экспорт ── +app.get('/export', async (req, res) => { + try { + const cidrs = await q.getExportCIDRs(); + res.setHeader('Content-Type', 'text/plain; charset=utf-8'); + res.send(cidrs.join('\n') + '\n'); + } catch (e) { + res.status(500).send('Export error'); + } +}); + +// ── Старт ── +checkConnection() + .then(() => console.log('DB connected')) + .catch(e => console.error('DB not ready:', e.message)); + +app.listen(PORT, () => console.log(`Server on port ${PORT}`)); +```