Codificação de URL explicada: o sinal de percentagem e onde morde
Publicado a 07/07/2026 · 17 min de leitura · Ferramentas para programadores
Daniel Okonkwo — Programador front-end e redator de Tecnologia na Allin
Desempenho web · Formatos de ficheiro
Verificado a partir de 6 fontes
A codificação por percentagem substitui um byte por um sinal de percentagem e dois dígitos hexadecimais. Que bytes precisam de substituição depende da parte da URL em que está, e é por isso que o assunto parece incoerente. A RFC 3986 define carateres não reservados que nunca precisam de codificação — A-Z, a-z, 0-9, hífen, ponto, sublinhado e til — e carateres reservados com significado estrutural: os gen-delims : / ? # [ ] @ e os sub-delims ! $ & ' ( ) * + , ; = . Um carácter reservado tem de ser codificado quando aparece como dado e não como estrutura. Assim, / é perfeitamente legal dentro de um caminho e tem de passar a %2F dentro de um valor de consulta, porque ali seria lido como parte do caminho. O JavaScript dá-lhe três funções que discordam nisto. Corra-as sobre a b/c?d=café+e&f#g~h*i(j): encodeURI devolve a%20b/c?d=caf%C3%A9+e&f#g~h*i(j), encodeURIComponent devolve a%20b%2Fc%3Fd%3Dcaf%C3%A9%2Be%26f%23g~h*i(j), e a obsoleta escape devolve a%20b/c%3Fd%3Dcaf%E9+e%26f%23g%7Eh*i%28j%29. Use encodeURIComponent para cada valor individual, encodeURI só para uma URL inteira em que já confia, e escape nunca — emite Latin-1, pelo que é sai %E9 em vez do correto UTF-8 %C3%A9.
A codificação por percentagem decide-se componente a componente, e é daí que vem toda a confusão. Uma barra é legal num caminho e tem de ser escapada num valor de consulta; um espaço é %20 num caminho e pode ser + num corpo de formulário. Eis os conjuntos exatos da RFC 3986, as três funções de JavaScript que discordam e as armadilhas.
A regra é por componente, não por URL
Uma URL não é uma única cadeia, é uma sequência de partes etiquetadas: esquema, anfitrião, caminho, consulta, fragmento. Cada parte tem a sua própria ideia do que é estrutura e do que é dado, e a codificação por percentagem existe para as distinguir. Uma barra dentro de um caminho é estrutura — separa segmentos — logo fica como está. A mesma barra dentro de um valor de consulta é dado, e tem de se escrever %2F, senão um analisador que leia a consulta não tem como saber que queria um carácter literal e não um pedaço de caminho que foi parar ao sítio errado.
A consequência aparece assim que constrói uma URL por concatenação. Suponha que um nome de ficheiro é 2026/08 report.pdf e tem de caber num segmento de caminho. Codifique o valor e obtém /files/2026%2F08%20report.pdf, um só segmento como pretendia. Salte a codificação e obtém /files/2026/08 report.pdf, três segmentos e um espaço, a apontar para algo que não existe. A mesma assimetria atinge os valores de consulta: ?note=rock&roll analisa-se como dois parâmetros, note com valor rock e um roll vazio, ao passo que ?note=rock%26roll se analisa como o valor único que queria.
Reservados e não reservados, exatamente como a RFC 3986 os define
O conjunto não reservado é pequeno e vale a pena decorá-lo: as letras de A a Z em ambas as caixas, os dígitos de 0 a 9 e exatamente quatro sinais de pontuação — hífen, ponto, sublinhado e til. Esses nunca precisam de codificação em parte alguma de uma URL, e codificá-los à mesma é legal mas inútil, já que %41 e A denotam o mesmo carácter e um analisador conforme trata-os de forma idêntica.
O conjunto reservado divide-se em dois. Os gen-delims são os carateres que separam os componentes principais: dois pontos, barra, ponto de interrogação, cardinal, parênteses retos de abertura e fecho, e arroba. Os sub-delims estruturam o interior de um componente: ponto de exclamação, cifrão, e comercial, apóstrofo, parênteses de abertura e fecho, asterisco, mais, vírgula, ponto e vírgula e igual. Tudo o que não esteja no conjunto não reservado nem no reservado — carateres de controlo, espaço, aspas retas, sinais de menor e maior, barra invertida, circunflexo, acento grave, chavetas, barra vertical e todo o byte acima de 127 — tem de ser sempre codificado.
Um pormenor apanha as pessoas. O encodeURIComponent deixa intactos o ponto de exclamação, o apóstrofo, os parênteses e o asterisco, e os quatro são sub-delims segundo a RFC 3986. Esses carateres são legais onde a função é normalmente usada, pelo que é inócuo nos casos correntes; mas se estiver a produzir um valor para um sistema que segue a RFC 3986 estritamente — alguns esquemas de assinatura e canonizações ao estilo OAuth fazem-no —, tem de os escapar por si mesmo a seguir. Note ainda que os parênteses retos são sempre escapados por ambos os codificadores de JavaScript, dando %5B e %5D, porque foram acrescentados ao conjunto reservado para literais IPv6 depois de as funções terem sido especificadas.
Três funções de JavaScript sobre a mesma cadeia, e a exceção do sinal de mais
Pegue em a b/c?d=café+e&f#g~h*i(j) e corra as três. O encodeURI produz a%20b/c?d=caf%C3%A9+e&f#g~h*i(j): o espaço e a letra acentuada são codificados, tudo o que é estrutural fica intacto. O encodeURIComponent produz a%20b%2Fc%3Fd%3Dcaf%C3%A9%2Be%26f%23g~h*i(j): a barra, o ponto de interrogação, o igual, o mais, o e comercial e o cardinal são todos escapados, porque no mundo desta função a cadeia inteira é um único valor. O escape produz a%20b/c%3Fd%3Dcaf%E9+e%26f%23g%7Eh*i%28j%29, diferente dos dois.
Enumere a gama ASCII e a diferença torna-se precisa. As duas funções modernas discordam em exatamente onze carateres: # $ & + , / : ; = ? @ são deixados intactos pelo encodeURI e escapados pelo encodeURIComponent. Essa lista é o conjunto reservado, o que lhe diz para que servem as duas funções. O encodeURI assume que a cadeia já é uma URL completa cujos delimitadores têm de sobreviver; o encodeURIComponent assume que a cadeia é um valor único ao qual não se pode permitir introduzir delimitador nenhum.
O escape é outro bicho e nunca deveria ser usado. É anterior às especificações modernas e codifica para Latin-1 em vez de UTF-8: é sai %E9 em vez do correto %C3%A9, e tudo acima de U+00FF sai como uma sequência não normalizada %uXXXX, pelo que o sinal de euro sai como %u20AC. Deixa também por escapar o mais, a arroba e a barra, todos perigosos dentro de um valor de consulta, enquanto escapa desnecessariamente o til e os parênteses. Sobrevive na linguagem só por compatibilidade retroativa, no anexo reservado às funcionalidades que existem mas em que não se deve confiar.
A codificação por percentagem tal como a RFC 3986 a define tem exatamente uma representação do espaço: %20. Funciona em todo o lado — caminho, consulta, fragmento. O sinal de mais como espaço pertence a outro mecanismo, mais antigo: a serialização application/x-www-form-urlencoded usada pelos formulários HTML, em que os espaços passam a sinais de mais e um mais literal tem de passar a %2B. Os navegadores usam essa forma para a cadeia de consulta de uma submissão de formulário por GET, e é por isso que vê ambas as convenções em cadeias de consulta reais.
A consequência é um erro de descodificação fácil de escrever e difícil de ver. decodeURIComponent("a+b") devolve a+b, com o mais intacto, porque o decodeURIComponent implementa a RFC 3986 e nada sabe de codificação de formulários. Dê a mesma cadeia a um analisador consciente dos formulários e obtém a b. Portanto o descodificador certo depende de como a cadeia foi produzida. Na prática o gesto fiável é parar de improvisar dos dois lados: construa as cadeias de consulta com URLSearchParams, que serializa um espaço como + e escapa um mais literal como %2B, e leia-as com URLSearchParams, que inverte exatamente as mesmas regras.
O não ASCII passa primeiro por UTF-8
A codificação por percentagem opera sobre bytes, não sobre carateres, pelo que um carácter não ASCII tem de ser convertido em bytes antes de poder ser escapado. A regra moderna é UTF-8 e depois um escape por byte. é é um único carácter codificado como os dois bytes c3 a9, logo fica %C3%A9. O sinal de euro são três bytes, e2 82 ac, logo fica %E2%82%AC — nove carateres para um símbolo. Um emoji como U+1F600 são quatro bytes e fica %F0%9F%98%80, doze carateres.
É aqui que o escape o trai, já que manda esse mesmo é para o único byte %E9, o seu ponto de código Latin-1. Um servidor que descodifica como UTF-8 vê uma sequência de bytes inválida e ou lança um erro ou produz um carácter de substituição, e a falha só aparece nas linhas acentuadas dos seus dados. A RFC 3986 não impõe, ela própria, uma codificação de carateres — é anterior à adoção universal do UTF-8 e limita-se a recomendá-lo para esquemas novos — mas a norma URL do WHATWG, que é o que os navegadores implementam de facto, especifica UTF-8 em todo o lado. Trate o UTF-8 como a única resposta correta.
A dupla codificação e como aparece %2520
O sinal de percentagem é ele próprio um carácter reservado, pelo que codificar uma cadeia já codificada escapa os escapes. Comece com a b. Codifique uma vez: a%20b. Codifique isso: a%2520b, porque a percentagem passou a %25. Codifique outra vez: a%252520b. Cada volta acrescenta três carateres e mais uma passagem de descodificação obrigatória, e a cadeia cresce sem nunca lançar um erro.
Em sistemas reais isto acontece quando um valor atravessa várias camadas que, prestáveis, codificam cada uma o que lhes foi entregue: um cliente codifica, uma gateway volta a codificar, uma framework codifica à entrada de um modelo. O sintoma é um %20 literal a aparecer numa página ou num nome de ficheiro onde devia estar um espaço, ou um 404 num caminho que parece correto. A cura é uma disciplina, não um truque: decida exatamente um lugar do pipeline dono da codificação, codifique lá e passe valores em bruto em todos os outros sítios. Se tiver de descodificar defensivamente, descodifique uma vez e verifique se o resultado ainda contém uma percentagem seguida de dois dígitos hexadecimais antes de decidir descodificar outra vez — e saiba que descodificar repetidamente às cegas é em si um problema de segurança, porque pode transformar %252e%252e em .. e reabrir um percurso de caminhos que uma única descodificação tinha fechado.
O anfitrião é outro mecanismo: IDN e punycode
A codificação por percentagem não se aplica ao nome de domínio. As etiquetas DNS limitam-se a letras, dígitos e hífenes, pelo que os domínios internacionalizados usam uma transformação completamente à parte: o Punycode, definido na RFC 3492 e envolvido pelas especificações IDNA. münchen.de passa a xn--mnchen-3ya.de, bücher.example passa a xn--bcher-kva.example, e um domínio japonês como os dois carateres que designam o Japão seguidos de .jp passa a xn--wgv71a.jp. O prefixo xn-- marca a etiqueta como codificada; a seguir vêm os carateres ASCII por ordem, depois um separador e depois instruções para reinserir os não ASCII.
Pode ver os dois mecanismos a trabalhar lado a lado. Analise https://münchen.de/straße?q=über alles com um analisador de URL padrão e o resultado é https://xn--mnchen-3ya.de/stra%C3%9Fe?q=%C3%BCber%20alles: o anfitrião passou pelo Punycode, o caminho e a consulta passaram pela codificação UTF-8 com percentagem, e nenhum pisou o terreno do outro. Esta separação não é cosmética. É por isso que uma sequência com percentagem num nome de anfitrião não é descodificada como se poderia esperar, e por isso que os ataques por homógrafo — registar um domínio cujos carateres Unicode se parecem com os de outro — são um problema da camada Punycode, que os navegadores tratam com regras de apresentação e não com codificação.
Quatro regras que evitam a maior parte dos problemas
Primeiro, nunca monte uma URL por concatenação de cadeias quando existe um construtor de URL. new URL() e URLSearchParams sabem em que componente estão e codificam em conformidade, que é exatamente o conhecimento que falta a um literal de modelo. Segundo, codifique valores, não URL: aplique encodeURIComponent a cada segmento de caminho e a cada valor de consulta, e reserve o encodeURI para uma URL acabada que tenha construído. Terceiro, codifique uma só vez, numa única camada dona, e passe valores em bruto em todos os outros sítios — só isso elimina toda a classe de erros em %2520. Quarto, apague o escape da sua base de código; codifica em Latin-1 e deixa passar carateres perigosos, e não há situação em que seja a resposta certa.
| Codificador | Saída sobre a b/c?d=café+e&f#g~h*i(j) | Deixa intactos (além de letras e dígitos) | Não ASCII | Use-o para |
|---|---|---|---|---|
| encodeURIComponent | a%20b%2Fc%3Fd%3Dcaf%C3%A9%2Be%26f%23g~h*i(j) | - . _ ~ ! ' ( ) * | UTF-8 e depois percentagem | Cada valor individual: um segmento de caminho, um valor de consulta, um fragmento |
| encodeURI | a%20b/c?d=caf%C3%A9+e&f#g~h*i(j) | Tudo o que o codificador de componente conserva, mais # $ & + , / : ; = ? @ | UTF-8 e depois percentagem | Uma URL inteira que montou e em que já confia |
| escape (obsoleta) | a%20b/c%3Fd%3Dcaf%E9+e%26f%23g%7Eh*i%28j%29 | * + - . / @ _ — e escapa ~ ( ) que os outros conservam | Latin-1 até U+00FF, depois o não normalizado %uXXXX | Nada. Está na norma apenas por compatibilidade retroativa |
| URLSearchParams (codificação de formulário) | q=a+b para o valor a b; a/b+c passa a a%2Fb%2Bc | Mesmo conjunto não reservado, mas um espaço passa a + e não a %20 | UTF-8 e depois percentagem | Construir uma cadeia de consulta ou um corpo x-www-form-urlencoded |
Perguntas frequentes
- Qual é a diferença entre encodeURI e encodeURIComponent?
- Exatamente onze carateres. O encodeURI deixa intactos # $ & + , / : ; = ? @; o encodeURIComponent escapa-os todos. Essa lista é o conjunto reservado da RFC 3986, e diz-lhe o que cada função assume. O encodeURI julga que lhe entregou uma URL completa cujos delimitadores têm de continuar a funcionar, pelo que só escapa aquilo que nunca poderia ser estrutura — espaços, não ASCII, carateres de controlo. O encodeURIComponent julga que lhe entregou um valor único que não pode introduzir delimitador nenhum, pelo que escapa tudo o que é reservado. Sobre a cadeia a b/c?d=café+e&f#g~h*i(j) o primeiro devolve a%20b/c?d=caf%C3%A9+e&f#g~h*i(j) e o segundo devolve a%20b%2Fc%3Fd%3Dcaf%C3%A9%2Be%26f%23g~h*i(j). Use o encodeURIComponent para cada segmento de caminho e cada valor de consulta, que é quase sempre o que quer. Use o encodeURI só sobre uma URL inteira que montou e em que já confia — aplicá-lo a entrada de utilizador não torna essa entrada segura, porque preserva de propósito os carateres de que um valor injetado precisaria.
- Um espaço deve ser %20 ou um sinal de mais?
- %20 é sempre correto; o sinal de mais só o é num contexto específico. A codificação por percentagem da RFC 3986 tem uma única representação do espaço, %20, válida no caminho, na consulta e no fragmento por igual. O mais como espaço vem do application/x-www-form-urlencoded, a serialização mais antiga que os formulários HTML usam, onde os espaços passam a mais e um mais literal tem de se escrever %2B. Os navegadores aplicam-no ao submeter um formulário por GET, e é por isso que as cadeias de consulta reais contêm ambas as convenções. O perigo prático está do lado da descodificação: decodeURIComponent("a+b") devolve a+b com o mais intacto, porque implementa a RFC 3986 e nada sabe de formulários, ao passo que um analisador consciente dos formulários devolve a b. Não decida caso a caso. Construa as cadeias de consulta com URLSearchParams e leia-as com URLSearchParams, para que as mesmas regras valham nos dois sentidos. Num segmento de caminho use sempre %20 — um mais ali é um carácter mais literal e nada mais.
- Porque vejo %2520 nos meus URL?
- Porque algo codificou uma cadeia que já estava codificada. O sinal de percentagem é ele próprio reservado, pelo que passa a %25 quando escapado. Pegue em a b, codifique para a%20b e depois codifique esse resultado: a percentagem passa a %25 e sai a%2520b. Faça-o mais uma vez e sai a%252520b. Nada falha, a cadeia apenas cresce três carateres por volta e precisa de uma passagem de descodificação extra. Em sistemas reais isto acontece quando várias camadas codificam educadamente cada uma o que lhes foi entregue — um cliente, depois uma gateway, depois uma framework a renderizar num modelo. O sintoma é um %20 literal onde devia estar um espaço, ou um 404 num caminho que parece certo. A correção é arquitetónica: nomeie exatamente uma camada dona da codificação, codifique lá e faça circular valores em bruto em todos os outros sítios. Evite descodificar em ciclo até não restarem percentagens, porque isso é uma vulnerabilidade em si — a descodificação repetida pode transformar %252e%252e em .. e reabrir um percurso de caminhos que uma única descodificação tinha contido.
- É preciso codificar carateres não ingleses num URL?
- Sim, e a codificação passa primeiro por UTF-8. A codificação por percentagem trabalha sobre bytes, pelo que um carácter tem de se tornar bytes antes de ser escapado, e a regra moderna é codificar como UTF-8 e depois escrever um escape por byte. é são dois bytes, c3 a9, e fica %C3%A9. O sinal de euro são três bytes e fica %E2%82%AC, nove carateres para um símbolo. Um emoji em U+1F600 são quatro bytes e fica %F0%9F%98%80. Isto importa para os tamanhos de armazenamento e para qualquer limite de comprimento que imponha, já que um carácter pode custar doze. Explica também porque a função obsoleta escape corrompe dados: emite Latin-1, transformando é no único byte %E9, que um descodificador UTF-8 rejeita como inválido — e a avaria só aparece nos seus registos acentuados. O anfitrião é a exceção: os nomes de domínio não são codificados por percentagem mas convertidos com Punycode, pelo que münchen.de passa a xn--mnchen-3ya.de enquanto o caminho e a consulta ao lado usam a codificação UTF-8 corrente.
- A codificação por percentagem basta para tornar segura a entrada do utilizador?
- Não, porque a codificação é contextual e uma URL é apenas um dos contextos por que um valor passa. O encodeURIComponent impede um valor de sair do seu componente de URL: uma barra passa a %2F e não pode abrir um segmento de caminho, um e comercial passa a %26 e não pode abrir um parâmetro. Isso é real e importante. Nada faz pelo salto seguinte. O mesmo valor colocado em HTML precisa de escape HTML, colocado numa instrução SQL precisa de consulta parametrizada, colocado num comando de shell precisa de aspas ao nível do argumento, e colocado num literal de cadeia de JavaScript precisa do seu próprio escape. Codificar para o contexto errado não é proteção parcial: é ausência de proteção. Mais dois avisos: o encodeURI não é um sanitizador de entrada, pois preserva de propósito os carateres reservados que um atacante usaria, portanto nunca o aplique a dados não fiáveis como medida de segurança; e descodificar repetidamente até não restar percentagem pode reconstruir sequências que uma única descodificação tinha neutralizado, nomeadamente devolver %252e%252e a um percurso de caminhos.
- Como encaixam aqui os nomes de domínio internacionalizados?
- Não usam codificação por percentagem de todo. As etiquetas DNS limitam-se a letras, dígitos e hífenes, pelo que um domínio com outra coisa é convertido com Punycode, definido na RFC 3492 e regido pelas especificações IDNA. münchen.de passa a xn--mnchen-3ya.de, bücher.example passa a xn--bcher-kva.example, e um domínio japonês como os dois carateres que designam o Japão seguidos de .jp passa a xn--wgv71a.jp. O prefixo xn-- assinala a etiqueta como codificada, e o resto contém os carateres ASCII seguidos de instruções para repor os outros. Pode ver os dois sistemas ao mesmo tempo analisando uma URL como https://münchen.de/straße?q=über alles: o resultado é https://xn--mnchen-3ya.de/stra%C3%9Fe?q=%C3%BCber%20alles, com Punycode no anfitrião e codificação UTF-8 com percentagem em tudo o que se segue. Na prática, nunca codifique por percentagem um nome de anfitrião e compare os nomes de anfitrião na sua forma Punycode. É também por isso que os ataques por homógrafo são tratados pela política de apresentação do navegador e não pela codificação — os dois nomes são mesmo etiquetas distintas que apenas se parecem.
Artigos que podem interessar-lhe
Todos os guias →Ferramentas relacionadas
Fontes
- IETF — RFC 3986, Uniform Resource Identifier (URI): Generic Syntax
- WHATWG — URL Standard
- WHATWG — HTML Standard, URL-encoded form data
- IETF — RFC 3492, Punycode: A Bootstring encoding of Unicode for IDNA
- IETF — RFC 5890, Internationalized Domain Names for Applications (IDNA): Definitions and Document Framework
- MDN Web Docs — encodeURIComponent()
Detetaste um erro neste artigo?