security(auth): anti-enumeration, returnTo-валидация, лимит попыток логина
Шаг 2.1 плана production-готовности: - единый 401 при любом отказе логина + constant-time bcrypt - forgot-password: идентичный ответ независимо от существования email - sanitizeReturnTo (open redirect fix в /api/documents/generate-link) - 10 неудачных попыток → блок 15 мин (in-memory, аудит) - DATABASE_SSL_REJECT_UNAUTHORIZED env (дефолт true) - доделка 0.7: статичные тексты в llm-providers/rag/finance-di2 - 8 новых тестов (104/104)
This commit is contained in:
10
client/src/lib/return-to.ts
Normal file
10
client/src/lib/return-to.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// Валидация returnTo против open redirect: принимаем только относительные
|
||||
// пути с одиночным '/' в начале. '//host' (protocol-relative) и '/\host'
|
||||
// (браузеры трактуют '\' как '/') отклоняются. Невалидное значение → ''.
|
||||
export function sanitizeReturnTo(value: string | null | undefined): string {
|
||||
if (!value) return '';
|
||||
if (!value.startsWith('/')) return '';
|
||||
if (value.length > 1 && (value[1] === '/' || value[1] === '\\')) return '';
|
||||
if (value.includes('://')) return '';
|
||||
return value;
|
||||
}
|
||||
@@ -3,11 +3,12 @@ import { useLocation } from 'wouter';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { CheckSquare, ArrowLeft, CheckCircle, Loader2 } from 'lucide-react';
|
||||
import { sanitizeReturnTo } from '@/lib/return-to';
|
||||
|
||||
function getReturnTo(): string {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const returnTo = params.get('returnTo');
|
||||
if (returnTo && returnTo.startsWith('/') && !returnTo.startsWith('/login')) {
|
||||
const returnTo = sanitizeReturnTo(params.get('returnTo'));
|
||||
if (returnTo && !returnTo.startsWith('/login')) {
|
||||
return returnTo;
|
||||
}
|
||||
return '';
|
||||
|
||||
@@ -17,19 +17,14 @@ import {
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { CheckSquare, Eye, EyeOff, CloudOff } from 'lucide-react';
|
||||
import { sanitizeReturnTo } from '@/lib/return-to';
|
||||
|
||||
function getReturnTo(): string {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const returnTo = params.get('returnTo');
|
||||
// Разрешаем только относительные пути внутри приложения.
|
||||
// Блокируем open redirect: //evil.com, /login, javascript:, https:// и т.п.
|
||||
if (
|
||||
returnTo &&
|
||||
returnTo.startsWith('/') &&
|
||||
!returnTo.startsWith('//') &&
|
||||
!returnTo.includes('://') &&
|
||||
!returnTo.startsWith('/login')
|
||||
) {
|
||||
// Блокируем open redirect: //evil.com, /\evil.com, /login, javascript:, https:// и т.п.
|
||||
const returnTo = sanitizeReturnTo(params.get('returnTo'));
|
||||
if (returnTo && !returnTo.startsWith('/login')) {
|
||||
return returnTo;
|
||||
}
|
||||
return '/home';
|
||||
|
||||
@@ -3,13 +3,13 @@ import { useLocation } from 'wouter';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { CheckSquare, Eye, EyeOff, Loader2, ArrowLeft, CheckCircle } from 'lucide-react';
|
||||
import { sanitizeReturnTo } from '@/lib/return-to';
|
||||
|
||||
function getParams(): { token: string; returnTo: string } {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const token = params.get('token') || '';
|
||||
const returnTo = params.get('returnTo') || '';
|
||||
const safeReturnTo =
|
||||
returnTo && returnTo.startsWith('/') && !returnTo.startsWith('/login') ? returnTo : '';
|
||||
const returnTo = sanitizeReturnTo(params.get('returnTo'));
|
||||
const safeReturnTo = returnTo && !returnTo.startsWith('/login') ? returnTo : '';
|
||||
return { token, returnTo: safeReturnTo };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user