fix: убрать утёкший токен, обновить Next.js, rate limit на /api/contact (PRJ-4)
- .env.local.example: реальный TELEGRAM_BOT_TOKEN заменён на placeholder (токен скомпрометирован — был в git-истории) - next 14.2.5 -> 14.2.35 (закрывает известные CVE), eslint-config-next синхронно - /api/contact: rate limit 5 запросов / 10 минут по IP (in-memory sliding window) + 429 - build проверен: 12 страниц, ошибок нет
This commit is contained in:
@@ -8,6 +8,42 @@ const MAX_LENGTHS = {
|
||||
message: 2000,
|
||||
};
|
||||
|
||||
// --- Rate limiting (in-memory, per IP) ---
|
||||
// Простой sliding-window лимитер без внешних зависимостей.
|
||||
// Для продакшена с несколькими инстансами замените на Redis/внешний стор.
|
||||
const RATE_LIMIT_WINDOW_MS = 10 * 60 * 1000; // 10 минут
|
||||
const RATE_LIMIT_MAX = 5; // максимум 5 запросов за окно
|
||||
const rateStore = new Map<string, { count: number; windowStart: number }>();
|
||||
|
||||
function getClientIp(req: NextRequest): string {
|
||||
const forwarded = req.headers.get("x-forwarded-for");
|
||||
if (forwarded) {
|
||||
return forwarded.split(",")[0].trim();
|
||||
}
|
||||
return req.headers.get("x-real-ip") ?? "unknown";
|
||||
}
|
||||
|
||||
function isRateLimited(ip: string): boolean {
|
||||
const now = Date.now();
|
||||
const entry = rateStore.get(ip);
|
||||
if (!entry || now - entry.windowStart >= RATE_LIMIT_WINDOW_MS) {
|
||||
rateStore.set(ip, { count: 1, windowStart: now });
|
||||
return false;
|
||||
}
|
||||
entry.count += 1;
|
||||
return entry.count > RATE_LIMIT_MAX;
|
||||
}
|
||||
|
||||
// Периодическая очистка устаревших записей, чтобы Map не рос бесконечно.
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [ip, entry] of rateStore) {
|
||||
if (now - entry.windowStart >= RATE_LIMIT_WINDOW_MS) {
|
||||
rateStore.delete(ip);
|
||||
}
|
||||
}
|
||||
}, RATE_LIMIT_WINDOW_MS).unref?.();
|
||||
|
||||
// Убираем HTML-теги и обрезаем длину, чтобы нельзя было сломать
|
||||
// форматирование сообщения в Telegram или протащить лишний код.
|
||||
function sanitize(value: unknown, maxLength: number): string {
|
||||
@@ -21,6 +57,15 @@ function escapeMarkdown(text: string): string {
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const ip = getClientIp(req);
|
||||
if (isRateLimited(ip)) {
|
||||
console.warn(`Rate limit exceeded for ${ip}`);
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: "Слишком много запросов. Попробуйте позже" },
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
|
||||
const token = process.env.TELEGRAM_BOT_TOKEN;
|
||||
const chatId = process.env.TELEGRAM_CHAT_ID;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user