Loto / src /utils /SecurityUtils.ts
Raí Santos
oi
4c1e4ec
/**
* Utilitários de segurança para a aplicação
*/
// Sanitização de dados de entrada
export class InputSanitizer {
// Sanitizar HTML para prevenir XSS
static sanitizeHTML(input: string): string {
const element = document.createElement('div');
element.textContent = input;
return element.innerHTML;
}
// Sanitizar URLs para prevenir ataques
static sanitizeURL(url: string): string | null {
try {
const parsedUrl = new URL(url);
// Lista de protocolos seguros
const allowedProtocols = ['http:', 'https:', 'mailto:', 'tel:'];
if (!allowedProtocols.includes(parsedUrl.protocol)) {
console.warn('Protocolo não permitido:', parsedUrl.protocol);
return null;
}
// Verificar esquemas suspeitos
// eslint-disable-next-line no-script-url
const suspiciousSchemes = [
'javascript:',
'data:',
'vbscript:',
'file:',
'ftp:'
];
if (suspiciousSchemes.some(scheme => url.toLowerCase().includes(scheme))) {
console.warn('URL suspeita detectada:', url);
return null;
}
return parsedUrl.toString();
} catch (error) {
console.warn('URL inválida:', url, error);
return null;
}
}
// Validar entrada numérica
static validateNumber(input: any, min?: number, max?: number): number | null {
const num = Number(input);
if (isNaN(num) || !isFinite(num)) {
return null;
}
if (min !== undefined && num < min) {
return null;
}
if (max !== undefined && num > max) {
return null;
}
return num;
}
// Validar entrada de string
static validateString(input: any, maxLength: number = 1000): string | null {
if (typeof input !== 'string') {
return null;
}
if (input.length > maxLength) {
console.warn('String muito longa:', input.length, 'max:', maxLength);
return null;
}
// Remover caracteres de controle
// eslint-disable-next-line no-control-regex
const sanitized = input.replace(/[\x00-\x1F\x7F]/g, '');
return sanitized;
}
}
// Content Security Policy helpers
export class CSPHelper {
private static policies: Record<string, string[]> = {
'default-src': ["'self'"],
'script-src': ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
'style-src': ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
'font-src': ["'self'", 'https://fonts.gstatic.com'],
'img-src': ["'self'", 'data:', 'https:'],
'connect-src': ["'self'", 'https://servicebus2.caixa.gov.br'],
'frame-src': ["'none'"],
'object-src': ["'none'"],
'base-uri': ["'self'"],
'form-action': ["'self'"]
};
static generateCSP(): string {
return Object.entries(this.policies)
.map(([directive, sources]) => `${directive} ${sources.join(' ')}`)
.join('; ');
}
static addAllowedSource(directive: string, source: string): void {
if (this.policies[directive]) {
if (!this.policies[directive].includes(source)) {
this.policies[directive].push(source);
}
}
}
static setCSPHeader(): void {
const csp = this.generateCSP();
const meta = document.createElement('meta');
meta.httpEquiv = 'Content-Security-Policy';
meta.content = csp;
document.head.appendChild(meta);
}
}
// Rate limiting para API calls
export class RateLimiter {
private calls: Map<string, number[]> = new Map();
private limits: Map<string, { maxCalls: number; windowMs: number }> = new Map();
setLimit(key: string, maxCalls: number, windowMs: number): void {
this.limits.set(key, { maxCalls, windowMs });
}
isAllowed(key: string): boolean {
const limit = this.limits.get(key);
if (!limit) return true;
const now = Date.now();
const calls = this.calls.get(key) || [];
// Limpar chamadas antigas
const validCalls = calls.filter(time => now - time < limit.windowMs);
if (validCalls.length >= limit.maxCalls) {
console.warn(`Rate limit excedido para ${key}`);
return false;
}
validCalls.push(now);
this.calls.set(key, validCalls);
return true;
}
getRemainingCalls(key: string): number {
const limit = this.limits.get(key);
if (!limit) return Infinity;
const now = Date.now();
const calls = this.calls.get(key) || [];
const validCalls = calls.filter(time => now - time < limit.windowMs);
return Math.max(0, limit.maxCalls - validCalls.length);
}
// BUG FIX: Adicionar método de reset que estava faltando
reset(key?: string): void {
if (key) {
this.calls.delete(key);
} else {
this.calls.clear();
}
console.log(`🔄 Rate limiter resetado${key ? ` para ${key}` : ''}`);
}
}
// Validador de dados de API com sanitização rigorosa
export class APIDataValidator {
static validateLotomaniaResult(data: any): {
isValid: boolean;
sanitized: any;
errors: string[]
} {
const errors: string[] = [];
const sanitized: any = {};
try {
if (!data || typeof data !== 'object') {
errors.push('Dados da API não são um objeto válido');
return { isValid: false, sanitized: {}, errors };
}
// Validação rigorosa do número do concurso
const numeroRaw = data.numero || data.concurso;
if (!numeroRaw || typeof numeroRaw !== 'number' || numeroRaw < 1 || numeroRaw > 9999) {
errors.push(`Número do concurso inválido: ${numeroRaw}`);
sanitized.numero = Math.max(1, parseInt(numeroRaw?.toString()) || 1);
} else {
sanitized.numero = numeroRaw;
}
// Validação rigorosa das dezenas
const dezenasRaw = data.listaDezenas || data.dezenas || data.numeros;
if (!Array.isArray(dezenasRaw)) {
errors.push('Lista de dezenas não é um array');
sanitized.listaDezenas = this.generateDefaultNumbers();
} else {
const validNumbers = dezenasRaw
.map((dezena: any) => {
const num = parseInt(dezena?.toString(), 10);
return !isNaN(num) && num >= 0 && num <= 99 ? num : null;
})
.filter((num: number | null) => num !== null) as number[];
if (validNumbers.length !== 20) {
errors.push(`Esperadas 20 dezenas, encontradas ${validNumbers.length} válidas`);
// Completar ou truncar para 20 números
while (validNumbers.length < 20) {
const randomNum = Math.floor(Math.random() * 100);
if (!validNumbers.includes(randomNum)) {
validNumbers.push(randomNum);
}
}
if (validNumbers.length > 20) {
validNumbers.splice(20);
}
}
sanitized.listaDezenas = validNumbers.sort((a, b) => a - b);
}
// Validação de data
const dataRaw = data.dataApuracao || data.data;
if (!dataRaw || typeof dataRaw !== 'string' || dataRaw.length < 8) {
errors.push('Data de apuração inválida');
sanitized.dataApuracao = new Date().toLocaleDateString('pt-BR');
} else {
// Tentar parsear a data
const dateRegex = /^(\d{2})\/(\d{2})\/(\d{4})$/;
if (!dateRegex.test(dataRaw)) {
errors.push('Formato de data inválido');
sanitized.dataApuracao = new Date().toLocaleDateString('pt-BR');
} else {
sanitized.dataApuracao = dataRaw;
}
}
// Validar valores monetários
const campos = ['valorArrecadado', 'valorAcumuladoProximoConcurso', 'valorEstimadoProximoConcurso'];
campos.forEach(campo => {
const valor = data[campo];
if (valor !== undefined && valor !== null) {
const valorNum = parseFloat(valor.toString());
if (isNaN(valorNum) || valorNum < 0) {
errors.push(`${campo} inválido: ${valor}`);
sanitized[campo] = 0;
} else {
sanitized[campo] = valorNum;
}
} else {
sanitized[campo] = 0;
}
});
// Campos opcionais com valores padrão
sanitized.acumulado = Boolean(data.acumulado);
sanitized.localSorteio = this.sanitizeString(data.localSorteio) || 'ESPAÇO DA SORTE';
const isValid = errors.length === 0;
if (isValid) {
console.log('✅ Dados da API validados com sucesso');
} else {
console.warn('⚠️ Dados da API sanitizados:', errors);
}
return { isValid, sanitized, errors };
} catch (error) {
errors.push(`Erro durante validação: ${error}`);
return {
isValid: false,
sanitized: this.getDefaultResult(),
errors
};
}
}
private static generateDefaultNumbers(): number[] {
const numbers = new Set<number>();
while (numbers.size < 20) {
numbers.add(Math.floor(Math.random() * 100));
}
return Array.from(numbers).sort((a, b) => a - b);
}
private static sanitizeString(input: any): string {
if (typeof input !== 'string') return '';
return input.replace(/[<>\"'&]/g, '').trim().substring(0, 100);
}
private static getDefaultResult(): any {
return {
numero: 1,
listaDezenas: this.generateDefaultNumbers(),
dataApuracao: new Date().toLocaleDateString('pt-BR'),
valorArrecadado: 0,
valorAcumuladoProximoConcurso: 0,
valorEstimadoProximoConcurso: 0,
acumulado: false,
localSorteio: 'ESPAÇO DA SORTE'
};
}
private static isValidDate(dateString: string): boolean {
try {
const date = new Date(dateString);
return date instanceof Date && !isNaN(date.getTime());
} catch {
return false;
}
}
}
// Proteção contra ataques de timing
export class TimingProtection {
private static readonly SAFE_DELAY = 100; // ms
// Adicionar delay aleatório para mascarar tempo de processamento
static async safeDelay(): Promise<void> {
const delay = Math.random() * this.SAFE_DELAY;
return new Promise(resolve => setTimeout(resolve, delay));
}
// Executar função com timing consistente
static async withConstantTiming<T>(
fn: () => Promise<T>,
targetTime: number = 1000
): Promise<T> {
const start = Date.now();
try {
const result = await fn();
const elapsed = Date.now() - start;
if (elapsed < targetTime) {
await new Promise(resolve => setTimeout(resolve, targetTime - elapsed));
}
return result;
} catch (error) {
const elapsed = Date.now() - start;
if (elapsed < targetTime) {
await new Promise(resolve => setTimeout(resolve, targetTime - elapsed));
}
throw error;
}
}
}
// Monitor de integridade da aplicação
export class IntegrityMonitor {
private checksums: Map<string, string> = new Map();
private violations: string[] = [];
// Calcular checksum simples de uma string
private calculateChecksum(data: string): string {
let hash = 0;
for (let i = 0; i < data.length; i++) {
const char = data.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Converter para 32bit integer
}
return hash.toString(16);
}
// Registrar checksum de dados importantes
registerChecksum(key: string, data: string): void {
const checksum = this.calculateChecksum(data);
this.checksums.set(key, checksum);
}
// Verificar integridade
verifyIntegrity(key: string, data: string): boolean {
const expectedChecksum = this.checksums.get(key);
if (!expectedChecksum) {
console.warn('Checksum não encontrado para:', key);
return false;
}
const actualChecksum = this.calculateChecksum(data);
if (actualChecksum !== expectedChecksum) {
const violation = `Violação de integridade detectada para ${key}`;
this.violations.push(violation);
console.error(violation);
return false;
}
return true;
}
getViolations(): string[] {
return [...this.violations];
}
clearViolations(): void {
this.violations = [];
}
}
// Instâncias globais
export const inputSanitizer = new InputSanitizer();
export const rateLimiter = new RateLimiter();
export const integrityMonitor = new IntegrityMonitor();
// Configuração de segurança padrão
export function setupSecurityDefaults(): void {
// Configurar rate limits
rateLimiter.setLimit('api-calls', 10, 60000); // 10 calls per minute
rateLimiter.setLimit('user-input', 50, 60000); // 50 inputs per minute
// Configurar CSP
if (process.env.NODE_ENV === 'production') {
CSPHelper.setCSPHeader();
}
// Registrar checksums críticos
integrityMonitor.registerChecksum('app-version', process.env.REACT_APP_VERSION || '1.0.0');
}
// Hook para uso em React
export function useSecurityUtils() {
return {
sanitizeHTML: InputSanitizer.sanitizeHTML,
sanitizeURL: InputSanitizer.sanitizeURL,
validateNumber: InputSanitizer.validateNumber,
validateString: InputSanitizer.validateString,
isRateLimited: (key: string) => !rateLimiter.isAllowed(key),
getRemainingCalls: (key: string) => rateLimiter.getRemainingCalls(key),
validateAPIData: APIDataValidator.validateLotomaniaResult
};
}