Tutorial

Práticas recomendadas de validação de carimbo de data/hora: um guia completo

Introdução

A validação de carimbos de data/hora é crucial para a integridade dos dados, segurança e confiabilidade dos aplicativos. Carimbos de data e hora inválidos podem levar a erros de banco de dados, cálculos incorretos, vulnerabilidades de segurança e experiência do usuário insatisfatória. Este guia abrangente ensinará as melhores práticas para validação de carimbo de data/hora em diferentes cenários.

Por que validar carimbos de data/hora?

Problemas comuns com carimbos de data/hora inválidos

  1. Corrupção de dados: carimbos de data/hora inválidos podem corromper dados históricos e análises
  2. Riscos de segurança: a manipulação do carimbo de data/hora pode ignorar a autenticação e autorização
  3. Erros de lógica de negócios: carimbos de data/hora incorretos podem afetar o faturamento, a programação e os relatórios
  4. Falhas de integração: formatos inválidos podem interromper integrações de API e pipelines de dados
  5. Problemas de desempenho: validação deficiente pode causar ineficiências nas consultas ao banco de dados

Impacto no mundo real

// Without validation - vulnerable to attacks
const expiryTime = req.body.expiryTime; // Could be: 9999999999999
if (Date.now() < expiryTime) {
  grantAccess(); // Attacker gets permanent access!
}

// With validation - secure
const expiryTime = validateTimestamp(req.body.expiryTime, {
  min: Date.now(),
  max: Date.now() + 86400000 // 24 hours max
});
if (expiryTime && Date.now() < expiryTime) {
  grantAccess(); // Safe access control
}

Técnicas Básicas de Validação

1. Validação de intervalo

Sempre valide se os carimbos de data/hora estão dentro dos limites aceitáveis.

Exemplo de JavaScript

function validateTimestampRange(timestamp, options = {}) {
  const {
    min = 0, // Unix epoch start (1970-01-01)
    max = 253402300799000, // Year 9999 in milliseconds
    precision = 'milliseconds'
  } = options;

  // Normalize to milliseconds
  let ts = timestamp;
  if (precision === 'seconds' && String(timestamp).length === 10) {
    ts = timestamp * 1000;
  } else if (precision === 'microseconds' && String(timestamp).length === 16) {
    ts = Math.floor(timestamp / 1000);
  }

  // Type check
  if (typeof ts !== 'number' || isNaN(ts)) {
    return { valid: false, error: 'Timestamp must be a valid number' };
  }

  // Finite check
  if (!Number.isFinite(ts)) {
    return { valid: false, error: 'Timestamp must be finite' };
  }

  // Range check
  if (ts < min) {
    return { valid: false, error: `Timestamp ${ts} is before minimum ${min}` };
  }

  if (ts > max) {
    return { valid: false, error: `Timestamp ${ts} exceeds maximum ${max}` };
  }

  return { valid: true, normalized: ts };
}

// Usage
const result = validateTimestampRange(1704067200000, {
  min: Date.parse('2024-01-01'),
  max: Date.parse('2024-12-31')
});

console.log(result);
// { valid: true, normalized: 1704067200000 }

Exemplo de Python

from datetime import datetime, timezone

def validate_timestamp_range(timestamp, min_ts=0, max_ts=253402300799):
    """
    Validate timestamp is within acceptable range.

    Args:
        timestamp: Unix timestamp (seconds or milliseconds)
        min_ts: Minimum allowed timestamp (default: Unix epoch)
        max_ts: Maximum allowed timestamp (default: year 9999)

    Returns:
        dict: Validation result with 'valid' boolean and optional 'error'
    """
    # Type validation
    if not isinstance(timestamp, (int, float)):
        return {'valid': False, 'error': 'Timestamp must be a number'}

    # Detect precision and normalize to seconds
    if timestamp > 10000000000:  # Likely milliseconds
        ts = timestamp / 1000
    else:
        ts = timestamp

    # Finite check
    if not (-float('inf') < ts < float('inf')):
        return {'valid': False, 'error': 'Timestamp must be finite'}

    # Range validation
    if ts < min_ts:
        return {
            'valid': False,
            'error': f'Timestamp {ts} is before minimum {min_ts}'
        }

    if ts > max_ts:
        return {
            'valid': False,
            'error': f'Timestamp {ts} exceeds maximum {max_ts}'
        }

    return {'valid': True, 'normalized': ts}

# Usage
result = validate_timestamp_range(
    1704067200,
    min_ts=datetime(2024, 1, 1, tzinfo=timezone.utc).timestamp(),
    max_ts=datetime(2024, 12, 31, tzinfo=timezone.utc).timestamp()
)
print(result)
# {'valid': True, 'normalized': 1704067200.0}

2. Validação de formato

Validar o formato do carimbo de data/hora corresponde ao padrão esperado.

Padrões Regex

// Timestamp format validators
const timestampFormats = {
  // Unix timestamp - seconds (10 digits)
  unixSeconds: /^[0-9]{10}$/,

  // Unix timestamp - milliseconds (13 digits)
  unixMilliseconds: /^[0-9]{13}$/,

  // ISO 8601 basic format
  iso8601: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/,

  // ISO 8601 with timezone
  iso8601Tz: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?([+-]\d{2}:\d{2}|Z)$/,

  // RFC 3339
  rfc3339: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/,
};

function validateTimestampFormat(timestamp, format = 'unixMilliseconds') {
  const pattern = timestampFormats[format];

  if (!pattern) {
    return { valid: false, error: `Unknown format: ${format}` };
  }

  const timestampStr = String(timestamp);

  if (!pattern.test(timestampStr)) {
    return {
      valid: false,
      error: `Timestamp "${timestampStr}" does not match ${format} format`
    };
  }

  return { valid: true };
}

// Usage examples
console.log(validateTimestampFormat(1704067200000, 'unixMilliseconds'));
// { valid: true }

console.log(validateTimestampFormat('2024-01-01T00:00:00Z', 'iso8601'));
// { valid: true }

console.log(validateTimestampFormat(170406720, 'unixMilliseconds'));
// { valid: false, error: 'Timestamp "170406720" does not match...' }

3. Detecção de precisão

Detecte e valide automaticamente a precisão do carimbo de data/hora.

function detectTimestampPrecision(timestamp) {
  const tsStr = String(timestamp);
  const length = tsStr.length;

  // Precision mapping
  const precisionMap = {
    10: { precision: 'seconds', multiplier: 1000 },
    13: { precision: 'milliseconds', multiplier: 1 },
    16: { precision: 'microseconds', multiplier: 0.001 },
    19: { precision: 'nanoseconds', multiplier: 0.000001 }
  };

  const detected = precisionMap[length];

  if (!detected) {
    return {
      valid: false,
      error: `Unable to detect precision for ${length}-digit timestamp`
    };
  }

  // Convert to milliseconds
  const normalized = timestamp * detected.multiplier;

  // Sanity check - should be between 1970 and year 3000
  const minDate = 0; // 1970-01-01
  const maxDate = 32503680000000; // 3000-01-01

  if (normalized < minDate || normalized > maxDate) {
    return {
      valid: false,
      error: `Timestamp ${timestamp} with ${detected.precision} precision is out of valid range`
    };
  }

  return {
    valid: true,
    precision: detected.precision,
    normalized: Math.floor(normalized),
    original: timestamp
  };
}

// Usage
console.log(detectTimestampPrecision(1704067200));
// { valid: true, precision: 'seconds', normalized: 1704067200000, original: 1704067200 }

console.log(detectTimestampPrecision(1704067200000));
// { valid: true, precision: 'milliseconds', normalized: 1704067200000, original: 1704067200000 }

4. Validação de fuso horário

Validar se as informações de fuso horário estão presentes e são válidas.

function validateTimezone(dateString) {
  // Check if timezone designator is present
  const hasTimezone = /Z|[+-]\d{2}:\d{2}$/.test(dateString);

  if (!hasTimezone) {
    return {
      valid: false,
      warning: 'No timezone information - timestamp is ambiguous'
    };
  }

  // Extract timezone
  const tzMatch = dateString.match(/(Z|[+-]\d{2}:\d{2})$/);
  const timezone = tzMatch ? tzMatch[1] : null;

  // Validate timezone offset range
  if (timezone && timezone !== 'Z') {
    const [sign, hours, minutes] = timezone.match(/([+-])(\d{2}):(\d{2})/).slice(1);
    const offset = parseInt(sign + hours) * 60 + parseInt(sign + minutes);

    // Valid timezone range: -12:00 to +14:00
    if (offset < -720 || offset > 840) {
      return {
        valid: false,
        error: `Invalid timezone offset: ${timezone}`
      };
    }
  }

  return {
    valid: true,
    timezone: timezone === 'Z' ? 'UTC' : timezone
  };
}

// Usage
console.log(validateTimezone('2024-01-01T00:00:00Z'));
// { valid: true, timezone: 'UTC' }

console.log(validateTimezone('2024-01-01T00:00:00+05:30'));
// { valid: true, timezone: '+05:30' }

console.log(validateTimezone('2024-01-01T00:00:00'));
// { valid: false, warning: 'No timezone information - timestamp is ambiguous' }

Considerações de segurança

1. Prevenir ataques baseados no tempo

// BAD: Vulnerable to timing attacks
function validateToken(token, expiryTimestamp) {
  return Date.now() < expiryTimestamp;
}

// GOOD: Secure validation with constant-time comparison
function validateTokenSecure(token, expiryTimestamp) {
  // Validate timestamp first
  const validation = validateTimestampRange(expiryTimestamp, {
    min: Date.now(),
    max: Date.now() + 86400000 // Max 24 hours in future
  });

  if (!validation.valid) {
    return false;
  }

  // Use constant-time comparison for sensitive checks
  const now = Date.now();
  const isValid = now < validation.normalized;

  // Add jitter to prevent timing analysis
  const jitter = Math.floor(Math.random() * 10);

  return isValid;
}

2. Evitar estouro de número inteiro

function safeTimestampAddition(timestamp, milliseconds) {
  // Check for potential overflow before adding
  if (timestamp > Number.MAX_SAFE_INTEGER - milliseconds) {
    throw new Error('Timestamp addition would cause overflow');
  }

  const result = timestamp + milliseconds;

  // Verify result is within safe integer range
  if (!Number.isSafeInteger(result)) {
    throw new Error('Result timestamp exceeds safe integer range');
  }

  return result;
}

// Usage
try {
  const future = safeTimestampAddition(1704067200000, 86400000);
  console.log(future); // 1704153600000
} catch (error) {
  console.error(error.message);
}

3. Prevenção de injeção de SQL

// BAD: Vulnerable to SQL injection
function getUserActivityUnsafe(userId, startTime) {
  const query = `SELECT * FROM activities WHERE user_id = ${userId}
                 AND created_at > ${startTime}`;
  return db.query(query); // DANGEROUS!
}

// GOOD: Using parameterized queries with validation
function getUserActivitySafe(userId, startTime) {
  // Validate timestamp first
  const validation = validateTimestampRange(startTime, {
    min: 0,
    max: Date.now()
  });

  if (!validation.valid) {
    throw new Error(`Invalid timestamp: ${validation.error}`);
  }

  // Use parameterized query
  const query = 'SELECT * FROM activities WHERE user_id = ? AND created_at > ?';
  return db.query(query, [userId, new Date(validation.normalized)]);
}

Função de validação completa

Aqui está um validador de carimbo de data/hora pronto para produção que combina todas as práticas recomendadas:

class TimestampValidator {
  constructor(options = {}) {
    this.minDate = options.minDate || new Date('1970-01-01');
    this.maxDate = options.maxDate || new Date('2099-12-31');
    this.allowedFormats = options.formats || ['unix', 'iso8601'];
    this.requireTimezone = options.requireTimezone || false;
  }

  validate(timestamp) {
    const errors = [];
    const warnings = [];

    // Step 1: Type validation
    if (timestamp === null || timestamp === undefined) {
      errors.push('Timestamp is required');
      return { valid: false, errors };
    }

    // Step 2: Format detection and parsing
    let parsedDate;
    let detectedFormat;

    if (typeof timestamp === 'number') {
      detectedFormat = 'unix';
      const precision = detectTimestampPrecision(timestamp);

      if (!precision.valid) {
        errors.push(precision.error);
        return { valid: false, errors };
      }

      parsedDate = new Date(precision.normalized);
    } else if (typeof timestamp === 'string') {
      // Try ISO 8601 parsing
      parsedDate = new Date(timestamp);
      detectedFormat = 'iso8601';

      // Validate timezone if required
      if (this.requireTimezone) {
        const tzValidation = validateTimezone(timestamp);
        if (!tzValidation.valid) {
          if (tzValidation.error) {
            errors.push(tzValidation.error);
          } else if (tzValidation.warning) {
            warnings.push(tzValidation.warning);
          }
        }
      }
    } else {
      errors.push(`Invalid timestamp type: ${typeof timestamp}`);
      return { valid: false, errors };
    }

    // Step 3: Check if format is allowed
    if (!this.allowedFormats.includes(detectedFormat)) {
      errors.push(`Format ${detectedFormat} is not allowed. Allowed: ${this.allowedFormats.join(', ')}`);
    }

    // Step 4: Validate parsed date is valid
    if (isNaN(parsedDate.getTime())) {
      errors.push('Timestamp could not be parsed to a valid date');
      return { valid: false, errors };
    }

    // Step 5: Range validation
    if (parsedDate < this.minDate) {
      errors.push(`Timestamp ${parsedDate.toISOString()} is before minimum ${this.minDate.toISOString()}`);
    }

    if (parsedDate > this.maxDate) {
      errors.push(`Timestamp ${parsedDate.toISOString()} exceeds maximum ${this.maxDate.toISOString()}`);
    }

    // Step 6: Additional checks
    const timestamp_ms = parsedDate.getTime();

    // Check for JavaScript Date edge cases
    if (timestamp_ms === -62135596800000) {
      warnings.push('Timestamp represents year 0 which may have parsing issues');
    }

    return {
      valid: errors.length === 0,
      errors: errors.length > 0 ? errors : undefined,
      warnings: warnings.length > 0 ? warnings : undefined,
      parsed: parsedDate,
      format: detectedFormat,
      timestamp: timestamp_ms
    };
  }
}

// Usage examples
const validator = new TimestampValidator({
  minDate: new Date('2024-01-01'),
  maxDate: new Date('2024-12-31'),
  requireTimezone: true,
  formats: ['unix', 'iso8601']
});

// Valid timestamp
console.log(validator.validate(1704067200000));
// {
//   valid: true,
//   parsed: Date,
//   format: 'unix',
//   timestamp: 1704067200000
// }

// Invalid - out of range
console.log(validator.validate(1735689600000)); // 2025-01-01
// {
//   valid: false,
//   errors: ['Timestamp exceeds maximum...']
// }

// Invalid - missing timezone
console.log(validator.validate('2024-06-15T10:30:00'));
// {
//   valid: false,
//   errors: ['No timezone information - timestamp is ambiguous']
// }

Práticas recomendadas para tratamento de erros

1. Limpar mensagens de erro

function formatValidationError(validation) {
  if (validation.valid) {
    return null;
  }

  return {
    message: 'Timestamp validation failed',
    details: validation.errors,
    suggestions: [
      'Ensure timestamp is in correct format (Unix milliseconds or ISO 8601)',
      'Check timestamp is within valid range (1970-2099)',
      'Include timezone information (Z or +HH:MM)',
      'Use 13-digit Unix timestamp for millisecond precision'
    ]
  };
}

2. Degradação graciosa

function parseTimestampWithFallback(timestamp, fallback = null) {
  const validation = validator.validate(timestamp);

  if (validation.valid) {
    return validation.parsed;
  }

  // Log error for monitoring
  console.warn('Timestamp validation failed:', validation.errors);

  // Return fallback value
  return fallback ? new Date(fallback) : new Date();
}

Testando seus validadores

// Test cases for timestamp validation
const testCases = [
  // Valid cases
  { input: 1704067200000, expected: true, description: 'Unix milliseconds' },
  { input: '2024-01-01T00:00:00Z', expected: true, description: 'ISO 8601 UTC' },

  // Invalid cases
  { input: null, expected: false, description: 'Null timestamp' },
  { input: 'invalid', expected: false, description: 'Invalid string' },
  { input: -1, expected: false, description: 'Negative timestamp' },
  { input: Infinity, expected: false, description: 'Infinity' },
  { input: NaN, expected: false, description: 'NaN' },

  // Edge cases
  { input: 0, expected: true, description: 'Unix epoch' },
  { input: 253402300799000, expected: true, description: 'Max timestamp (year 9999)' },
];

testCases.forEach(test => {
  const result = validator.validate(test.input);
  const passed = result.valid === test.expected;
  console.log(`${passed ? '✓' : '✗'} ${test.description}`);
  if (!passed) {
    console.log('  Expected:', test.expected, 'Got:', result.valid);
  }
});

Armadilhas comuns a serem evitadas

  1. Não validando tipo de entrada - Sempre verifique typeof antes de processar
  2. Ignorar informações de fuso horário - Pode levar a erros fora do horário comercial
  3. Usando igualdade flexível (==) - Use igualdade estrita (===) para comparações
  4. Não trata de casos extremos - Teste com 0, valores negativos, números muito grandes
  5. Esquecendo diferenças de precisão - Segundos vs milissegundos vs microssegundos
  6. Confiar em carimbos de data/hora do cliente - Sempre valide no lado do servidor
  7. Não registrar falhas de validação - Dificulta a depuração

Conclusão

A validação adequada do carimbo de data/hora é essencial para a construção de aplicativos robustos. Seguindo estas práticas recomendadas:

  • Sempre valide o intervalo - Certifique-se de que os carimbos de data e hora estejam dentro dos limites aceitáveis
  • Verifique o formato explicitamente - Não assuma o formato de entrada
  • Trate dos fusos horários com cuidado - Exija informações de fuso horário quando necessário
  • Considere as implicações de segurança - Evite manipulação e ataques
  • Forneça mensagens de erro claras - Ajude usuários e desenvolvedores a depurar problemas
  • Teste completamente - Cubra casos extremos e entradas inválidas

Use essas técnicas de validação para proteger seu aplicativo contra dados inválidos, vulnerabilidades de segurança e erros lógicos.

Recursos relacionados