Erros comuns ao validar CPF e 12 casos de teste
Os bugs mais comuns em validadores de CPF, como sequências repetidas, resto 0 e 1 e zeros perdidos, e 12 casos de teste com o resultado esperado.
Validar CPF parece resolvido: a conta é conhecida, existem centenas de exemplos prontos. Mesmo assim, validadores com defeito continuam aparecendo em projetos reais, e quase sempre pelos mesmos motivos. Alguns recusam CPFs legítimos, o que bloqueia clientes; outros aceitam números impossíveis, o que suja a base. Este guia percorre os erros mais frequentes, mostra o código com defeito e a correção, e termina com uma tabela de 12 casos que todo validador deveria passar.
Aceitar sequências repetidas
O erro mais conhecido, e ainda assim comum, é aceitar 111.111.111-11. Ele não é um bug na conta: a conta está certa. Para qualquer dígito repetido nove vezes, as duas rodadas do módulo 11 devolvem o próprio dígito como verificador. Com 1, a primeira soma é 54, o resto é 10 e o dígito é 1; a segunda soma é 65, o resto é 10 e o dígito é 1 de novo. O mesmo vale de 000.000.000-00 a 999.999.999-99.
Essas sequências não são aceitas por convenção, e são exatamente o que alguém digita para pular um campo obrigatório. A correção é uma checagem antes da conta:
// com defeito: só a conta
if (dv1 === Number(cpf[9]) && dv2 === Number(cpf[10])) return true;
// corrigido: recusa sequências antes de calcular
if (/^(\d)\1{10}$/.test(cpf)) return false;A expressão captura o primeiro dígito e exige que ele se repita mais dez vezes. O detalhe é aplicá-la depois de remover a pontuação: /^(\d)\1{10}$/ não casa com “111.111.111-11”.
Esquecer o resto 0 e 1
Pela regra do CPF, quando o resto da divisão por 11 é menor que 2, o dígito verificador é 0. Uma implementação que faz só 11 - resto devolve 11 para resto 0 e 10 para resto 1, valores que nunca são iguais a um único dígito. O efeito é recusar CPFs legítimos, e só alguns deles, o que torna o bug difícil de perceber em testes manuais.
// com defeito
const dv = 11 - (soma % 11);
// corrigido
const resto = soma % 11;
const dv = resto < 2 ? 0 : 11 - resto;Três números cobrem esses casos. Em 529.982.248-06, a primeira soma é 297, múltiplo de 11, e o resto 0 gera o primeiro verificador 0. Em 012.345.678-90, a segunda soma é 210, com resto 1, que gera o segundo verificador 0. E em 529.982.243-00 os dois restos são 1. Se o seu validador recusa algum dos três, este é o defeito.
Há uma variante que calcula (soma * 10) % 11 e transforma 10 em 0. Ela é equivalente, desde que a transformação não seja esquecida.
Perder zeros à esquerda ao converter para número
CPFs podem começar com zero, como 012.345.678-90. Qualquer conversão para número descarta esse zero: Number('01234567890') resulta em 1234567890, com 10 dígitos. O mesmo acontece quando o CPF passa por uma coluna BIGINT, por um campo numérico de JSON ou por uma planilha que abre o CSV e formata a coluna como número.
O sintoma é sempre o mesmo: o validador recebe 10 dígitos e recusa, ou, pior, alguém “corrige” o validador para aceitar 10 dígitos.
// com defeito: o CPF virou número em algum ponto
const cpf = String(Number(entrada)); // '1234567890'
// corrigido: CPF é sempre texto
const cpf = String(entrada).replace(/[.\-\s]/g, ''); // '01234567890'A regra é tratar o CPF como texto do início ao fim: no formulário (type="text", não type="number"), no JSON (string), no banco (CHAR(11)) e nas planilhas (coluna formatada como texto antes de importar). O guia sobre validação no banco de dados mostra como recuperar com lpad zeros já perdidos, e por que isso só é seguro quando você sabe que a coluna era numérica.
Validar só o formato
Uma expressão regular confere se a entrada tem a forma de um CPF, mas não se os dígitos verificadores batem. ^\d{3}\.\d{3}\.\d{3}-\d{2}$ aceita 529.982.247-26, que tem o segundo verificador errado, e aceita 123.456.789-00. O guia sobre regex de CPF mostra o que a expressão consegue fazer, e a resposta é: filtrar o formato, nada mais.
O formato é a primeira etapa da validação, não a validação inteira. Depois dele vem a conta dos verificadores, e a função completa está no guia sobre o algoritmo de módulo 11.
Rejeitar CPF sem pontuação
O erro inverso também é comum: exigir a máscara. Um validador que só aceita “529.982.247-25” recusa “52998224725”, que é como o CPF chega de outros sistemas, de planilhas e muitas vezes do próprio formulário depois de remover a máscara. Também recusa “ 529 982 247 25 “, que aparece quando alguém copia o número de um PDF.
A abordagem mais robusta é aceitar pontos, hífen e espaços como separadores, removê-los e exigir 11 dígitos. Qualquer outro caractere deve ser recusado, e não descartado:
// tolerante demais: '529.982.247/25' e 'cpf 52998224725' viram válidos
const limpo = entrada.replace(/\D/g, '');
// estrito: separadores são aceitos, o resto invalida
const semSeparadores = entrada.replace(/[.\-\s]/g, '');
if (/\D/.test(semSeparadores)) return false; // motivo: caracteres
if (semSeparadores.length !== 11) return false; // motivo: tamanhoDescartar tudo que não é dígito é útil em uma máscara de input, que formata enquanto a pessoa digita. Na validação, porém, um caractere estranho indica que a entrada pode estar errada, e aceitá-la em silêncio esconde o problema. O validador de CPF e a API deste site seguem a regra estrita e devolvem o motivo caracteres nesses casos.
Validar no front e esquecer o back
A validação no navegador melhora a experiência, mas não protege nada. Qualquer requisição pode ser montada fora do formulário, com curl, com uma ferramenta de API ou com o próprio console do navegador:
curl -X POST https://seu-sistema.example/api/clientes \
-H 'Content-Type: application/json' \
-d '{"nome": "Teste", "cpf": "111.111.111-11"}'Se o back-end não valida, esse cadastro entra. A regra é validar nos dois lados com a mesma lógica: no front para dar a mensagem na hora, no back porque é ele que grava. Quando várias aplicações escrevem no mesmo banco, vale uma terceira barreira, uma restrição na própria tabela. Para validar de forma pontual sem implementar nada, a API responde em GET /api/v1/cpf/validar/:cpf com valido, cpf, formatado, uf e, quando o número é recusado, motivo, sem chave e com limite de 60 requisições por minuto por IP.
Casos de teste que todo validador deve passar
A tabela abaixo reúne os casos que pegam os erros descritos neste guia. O resultado esperado segue a implementação de referência deste site: separadores aceitos, outros caracteres recusados, sequências repetidas recusadas.
| # | Entrada | Esperado | Por quê |
|---|---|---|---|
| 1 | 529.982.247-25 |
válido | caso básico, formatado |
| 2 | 529.982.248-06 |
válido | primeiro verificador vem do resto 0 |
| 3 | 111.444.777-35 |
válido | outro número válido |
| 4 | 111.111.111-11 |
inválido (repetido) |
passa na conta, recusado por convenção |
| 5 | 000.000.000-00 |
inválido (repetido) |
sequência de zeros |
| 6 | 529.982.247-26 |
inválido (dv2) |
segundo verificador errado (esperado 5) |
| 7 | 529.982.247-35 |
inválido (dv1) |
primeiro verificador errado (esperado 2) |
| 8 | "5299822472" |
inválido (tamanho) |
10 dígitos |
| 9 | "052998224725" |
inválido (tamanho) |
12 dígitos, zero a mais à esquerda |
| 10 | 529.982.247/25 |
inválido (caracteres) |
barra não é separador aceito |
| 11 | "" |
inválido (tamanho) |
entrada vazia |
| 12 | " 529 982 247 25 " |
válido | espaços tolerados |
O caso 10 depende da política adotada: uma função que descarta tudo que não é dígito o aceita. Se essa for a sua escolha, mantenha o caso na tabela com o resultado oposto, para que a decisão fique registrada no teste. O caso 9 é o inverso do problema dos zeros perdidos: completar ou cortar dígitos para chegar a 11 nunca é papel do validador.
Em Vitest ou Jest, a tabela vira um teste parametrizado:
import { describe, it, expect } from 'vitest';
import { analisarCpf } from '../src/cpf';
const casos: [string, boolean, string?][] = [
['529.982.247-25', true],
['529.982.248-06', true],
['111.444.777-35', true],
['111.111.111-11', false, 'repetido'],
['000.000.000-00', false, 'repetido'],
['529.982.247-26', false, 'dv2'],
['529.982.247-35', false, 'dv1'],
['5299822472', false, 'tamanho'],
['052998224725', false, 'tamanho'],
['529.982.247/25', false, 'caracteres'],
['', false, 'tamanho'],
[' 529 982 247 25 ', true],
];
describe('analisarCpf', () => {
it.each(casos)('%j -> valido=%s motivo=%s', (entrada, valido, motivo) => {
const r = analisarCpf(entrada);
expect(r.valido).toBe(valido);
expect(r.motivo).toBe(motivo);
});
});Se a sua função devolve só true ou false, remova a terceira coluna. Para conferir um caso isolado, cole o número no validador de CPF, que mostra o motivo da recusa, ou use a calculadora de dígitos para ver as somas e os restos. Para gerar números válidos para outros testes, use o gerador de CPF; eles podem coincidir com um CPF real por acaso, então ficam restritos a ambientes de teste.
Perguntas frequentes
Por que 111.111.111-11 passa na conta do módulo 11?
Porque, para qualquer dígito repetido, as duas somas deixam um resto que devolve o próprio dígito como verificador. As dez sequências, de 000.000.000-00 a 999.999.999-99, fecham a conta e precisam ser recusadas explicitamente.
O validador deve aceitar CPF com espaços?
É uma boa prática aceitar espaços, pontos e hífen como separadores, porque eles aparecem ao copiar e colar. Letras, barras e outros caracteres devem ser recusados, em vez de descartados em silêncio.
Se o meu validador passar nos 12 casos, ele está correto?
Os 12 casos cobrem os erros mais frequentes, mas não provam a correção. Para mais segurança, compare o seu validador com uma implementação de referência sobre milhares de números gerados, válidos e com um dígito alterado.