Files
ipwhitelist-app/research/prompt-opus-ejs.md
T

246 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Промпт для 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
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Белые списки IP — Nubes</title>
<link rel="icon" href="/favicon.png" type="image/png">
<style>
:root {
--bg: #f5f5f5;
--card: #ffffff;
--text: #1a1a1a;
--muted: #6b7280;
--border: #d1d5db;
--grey-light: #f3f4f6;
--blue: #2563eb;
--blue-h: #1d4ed8;
--red: #dc2626;
--red-h: #b91c1c;
--green: #16a34a;
--amber: #d97706;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
font-size: 14px;
line-height: 1.5;
}
.page { max-width: 1100px; margin: 1.5rem auto; padding: 0 1rem; }
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: 0 1px 2px rgba(0,0,0,.04);
margin-bottom: 1rem;
overflow: hidden;
}
.card-header {
background: var(--grey-light);
padding: .75rem 1rem;
font-weight: 600;
font-size: 1rem;
border-bottom: 1px solid var(--border);
}
.card-body { padding: 1rem; }
.alert { padding: .75rem 1rem; border-radius: 8px; margin-bottom: 1rem; font-size: .9rem; }
.alert-ok { background: #dcfce7; color: #166534; border: 1px solid #bbf7d0; }
.alert-err { background: #fecaca; color: #991b1b; border: 1px solid #fca5a5; }
.alert-warn{ background: #fef3c7; color: #92400e; border: 1px solid #fde68a; }
.stats { display: flex; gap: 2rem; }
.stat { text-align: center; }
.stat .num { font-size: 1.6rem; font-weight: 700; }
.stat .lbl { font-size: .8rem; color: var(--muted); }
.stat.full .num { color: var(--red); }
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr auto;
gap: .75rem 1rem;
align-items: end;
}
.field { display: flex; flex-direction: column; gap: .25rem; }
.field label {
font-size: .8rem;
font-weight: 500;
color: var(--muted);
text-transform: uppercase;
letter-spacing: .5px;
}
.field input, .field textarea {
padding: .5rem .75rem;
border: 1px solid var(--border);
border-radius: 6px;
font-size: .9rem;
outline: none;
transition: border .15s;
background: #fff;
}
.field input:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(37,99,235,.1); }
.btn {
display: inline-flex;
align-items: center;
gap: .35rem;
padding: .5rem 1rem;
border: 1px solid var(--border);
border-radius: 6px;
font-size: .85rem;
font-weight: 500;
background: #fff;
cursor: pointer;
transition: background .15s;
white-space: nowrap;
}
.btn:hover { background: var(--grey-light); }
.btn-primary { background: var(--blue); color: #fff; border-color: var(--blue); }
.btn-primary:hover { background: var(--blue-h); }
.btn-primary:disabled { background: #93c5fd; cursor: not-allowed; border-color: #93c5fd; }
.btn-danger { color: var(--red); border-color: var(--red); }
.btn-danger:hover { background: #fecaca; }
.table-wrap {
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
table { width: 100%; border-collapse: collapse; font-size: .85rem; }
th {
text-align: left;
padding: .5rem .75rem;
background: var(--grey-light);
color: var(--muted);
font-weight: 500;
font-size: .8rem;
text-transform: uppercase;
letter-spacing: .5px;
border-bottom: 1px solid var(--border);
border-right: 1px solid var(--border);
}
th:last-child { border-right: none; }
td {
padding: .5rem .75rem;
border-bottom: 1px solid var(--border);
border-right: 1px solid var(--border);
}
td:last-child { border-right: none; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: #f8fafc; }
code { background: #f1f5f9; padding: .15rem .4rem; border-radius: 3px; font-size: .9em; }
.empty { text-align: center; color: var(--muted); padding: 2rem; }
.text-center { text-align: center; }
.mt-3 { margin-top: 1rem; }
</style>
</head>
<body>
<header style="background:#fff;border-bottom:1px solid var(--border);padding:0 1.5rem;height:48px;display:flex;align-items:center;gap:.75rem;font-size:.9rem;color:var(--muted);">
<svg width="130" height="28" viewBox="330 228 311 69" style="display:block;">...</svg>
<span>|</span>
<span style="font-weight:500;color:var(--text);">Белые списки IP</span>
</header>
<div class="page">
<% if (message) { %><div class="alert <%= wasNormalized ? 'alert-warn' : 'alert-ok' %>"><%= message %></div><% } %>
<% if (error) { %><div class="alert alert-err"><%= error %></div><% } %>
<div class="card">
<div class="card-body">
<div class="stats">
<div class="stat <%= used >= limit ? 'full' : '' %>">
<div class="num"><%= used %> / <%= limit %></div>
<div class="lbl">записей</div>
</div>
<div class="stat">
<div class="num"><%= user.clientId %></div>
<div class="lbl">компания</div>
</div>
<div class="stat">
<div class="num"><%= user.email %></div>
<div class="lbl">пользователь</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">Добавить адрес</div>
<div class="card-body">
<form method="POST" action="/add">
<div class="form-grid">
<div class="field">
<label>IPv4 адрес или подсеть CIDR</label>
<input name="value" placeholder="Например: 203.0.113.10 или 203.0.113.0/24" required <%= used >= limit ? 'disabled' : '' %>>
</div>
<div class="field">
<label>Комментарий</label>
<input name="comment" placeholder="Необязательно" maxlength="255">
</div>
<button class="btn btn-primary" type="submit" <%= used >= limit ? 'disabled' : '' %> style="align-self:end">
<%= used >= limit ? 'Лимит исчерпан' : 'Добавить' %>
</button>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-header">Доверенные адреса</div>
<% if (entries.length === 0) { %>
<div class="empty">Нет добавленных адресов</div>
<% } else { %>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Адрес / Подсеть</th>
<th>Комментарий</th>
<th>Добавил</th>
<th>Дата</th>
<th class="text-center">Действие</th>
</tr>
</thead>
<tbody>
<% entries.forEach(e => { %>
<tr>
<td><code><%= e.value_cidr %></code></td>
<td><%= e.comment || '—' %></td>
<td><%= e.created_by %></td>
<td><%= new Date(e.created_at).toLocaleDateString('ru', {day:'numeric',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit'}) %></td>
<td class="text-center">
<form method="POST" action="/delete/<%= e.id %>" style="display:inline" onsubmit="return confirm('Удалить запись?')">
<button class="btn btn-danger">Удалить</button>
</form>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
<% } %>
</div>
</div>
</body>
</html>
```