Método mais rápido para escapar de tags HTML como entidades HTML?

98

Estou escrevendo uma extensão do Chrome que envolve muitas das seguintes tarefas: higienizar strings que podem conter tags HTML, convertendo <, >e &em &lt;, &gt;e &amp;, respectivamente.

(Em outras palavras, o mesmo que o do PHP htmlspecialchars(str, ENT_NOQUOTES)- não acho que haja qualquer necessidade real de converter caracteres de aspas duplas.)

Esta é a função mais rápida que encontrei até agora:

function safe_tags(str) {
    return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') ;
}

Mas ainda há um grande atraso quando eu tenho que passar alguns milhares de cordas por ele de uma vez.

Alguém pode melhorar isso? É principalmente para strings entre 10 e 150 caracteres, se isso fizer diferença.

(Uma ideia que tive foi não me incomodar em codificar o sinal de maior - haveria algum perigo real nisso?)

callum
fonte
2
Por quê? Na maioria dos casos em que você deseja fazer isso, deseja inserir os dados no DOM; nesse caso, você deve esquecer de escapar dele e apenas fazer um textNode a partir dele.
Quentin,
1
@David Dorward: talvez ele quisesse higienizar os dados do POST e o servidor não fez o roundtrip dos dados corretamente.
Lie Ryan,
4
@Lie - em caso afirmativo, a solução é "Pelo amor de Deus, conserte o servidor, pois você tem um grande buraco de XSS"
Quentin,
2
@David Dorward: é possível que ele não tenha controle sobre o servidor. Eu estive recentemente em uma situação em que estava escrevendo um script greasemonkey para contornar algumas coisas de que não gosto no site da minha universidade; Tive que fazer um POST em um servidor que não tenho controle e higienizar os dados do POST usando javascript (uma vez que os dados brutos vêm de uma caixa de texto rica e, portanto, tem montes de tags html que não fazem o percurso completo no servidor) . O administrador da Web estava ignorando minha solicitação para consertar o site, então não tive outra escolha.
Lie Ryan,
1
Eu tenho um caso de uso em que preciso exibir uma mensagem de erro em um div. A mensagem de erro pode conter HTML e novas linhas. Quero escapar do HTML e substituir as novas linhas por <br>. Em seguida, coloque o resultado em uma div para exibição.
mozey

Respostas:

83

Você pode tentar passar uma função de retorno de chamada para realizar a substituição:

var tagsToReplace = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;'
};

function replaceTag(tag) {
    return tagsToReplace[tag] || tag;
}

function safe_tags_replace(str) {
    return str.replace(/[&<>]/g, replaceTag);
}

Aqui está um teste de desempenho: http://jsperf.com/encode-html-entities para comparar com a chamada da replacefunção repetidamente e usando o método DOM proposto por Dmitrij.

Seu caminho parece ser mais rápido ...

Mas por que você precisa disso?

Martijn
fonte
2
Não há necessidade de escapar >.
6
Na verdade, se você colocar o valor de escape no atributo de um elemento html, você precisa escapar do símbolo>. Caso contrário, quebraria a tag desse elemento html.
Zlatin Zlatev
1
No texto normal, os caracteres de escape são raros. É melhor ligar para substituir apenas quando necessário, se você se preocupa com a velocidade máxima:if (/[<>&"]/.test(str) { ... }
Vitaly
3
@callum: Não. Não estou interessado em enumerar casos em que acho que "algo pode dar errado" (até porque são os casos inesperados / esquecidos que vão te machucar, e quando você menos espera). Estou interessado em codificar de acordo com os padrões (portanto, os casos inesperados / esquecidos não podem prejudicá-lo por definição ). Eu não posso enfatizar o quão importante isso é. >é um caractere especial em HTML, portanto, escape-o. Simples assim. :)
Lightness Races in Orbit
4
@LightnessRacesinOrbit É relevante porque a questão é qual é o método mais rápido possível. Se for possível pular a >substituição, isso a tornará mais rápida.
callum de
104

Esta é uma maneira de fazer isso:

var escape = document.createElement('textarea');
function escapeHTML(html) {
    escape.textContent = html;
    return escape.innerHTML;
}

function unescapeHTML(html) {
    escape.innerHTML = html;
    return escape.textContent;
}

Aqui está uma demonstração.

Web designer
fonte
Redesenhei a demonstração. Esta é uma versão em tela cheia: jsfiddle.net/Daniel_Hug/qPUEX/show/light
Web_Designer
13
Não sei como / o quê / por quê - mas isso é genial.
rob_james
4
Parece que ele está aproveitando o código existente do elemento TextArea para escapar do texto literal. Muito bom, acho que este pequeno truque vai encontrar outra casa.
Ajax
3
@jazkat Não estou usando essa função. A variável de escape que uso, eu me defino no exemplo.
Web_Designer
2
mas isso perde espaço em branco etc.
Andrew
31

Método de Martijn como função de protótipo:

String.prototype.escape = function() {
    var tagsToReplace = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;'
    };
    return this.replace(/[&<>]/g, function(tag) {
        return tagsToReplace[tag] || tag;
    });
};

var a = "<abc>";
var b = a.escape(); // "&lt;abc&gt;"
Aram Kocharyan
fonte
12
Adicione Stringassim ele deve ser escapeHtml, pois não é um escape para uma String em geral. Isso é String.escapeHtmlcorreto, mas String.escapelevanta a questão, "escapar para quê?"
Lawrence Dol
3
Sim, boa ideia. Eu deixei de estender o protótipo atualmente para evitar conflitos.
Aram Kocharyan
1
Se o seu navegador tiver suporte para Symbol, você pode usá-lo para evitar poluir o namespace string-key. var escape = new Symbol ("escape"); String.protótipo [escape] = função () {...}; "texto" [escape] ();
Ajax
12

Uma solução ainda mais rápida / curta é:

escaped = new Option(html).innerHTML

Isso está relacionado a algum vestígio estranho de JavaScript pelo qual o elemento Option retém um construtor que faz esse tipo de escape automaticamente.

Crédito para https://github.com/jasonmoo/t.js/blob/master/t.js

Todd
fonte
1
Uma linha simples, mas o método mais lento depois do regex. Além disso, o texto aqui pode ter o espaço em branco removido, de acordo com a especificação
ShortFuse
Observe que o link "método mais lento" de @ ShortFuse faz meu sistema ficar sem RAM (com ~ 6 GB livres) e o firefox parece parar de alocar pouco antes de ficar sem memória, então em vez de encerrar o processo ofensivo, o linux vai sentar lá e deixar você fazer um hard power desligado.
Luc
11

O código-fonte do AngularJS também tem uma versão dentro do angular-sanitize.js .

var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
    // Match everything outside of normal chars and " (quote character)
    NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
/**
 * Escapes all potentially dangerous characters, so that the
 * resulting string can be safely inserted into attribute or
 * element text.
 * @param value
 * @returns {string} escaped text
 */
function encodeEntities(value) {
  return value.
    replace(/&/g, '&amp;').
    replace(SURROGATE_PAIR_REGEXP, function(value) {
      var hi = value.charCodeAt(0);
      var low = value.charCodeAt(1);
      return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
    }).
    replace(NON_ALPHANUMERIC_REGEXP, function(value) {
      return '&#' + value.charCodeAt(0) + ';';
    }).
    replace(/</g, '&lt;').
    replace(/>/g, '&gt;');
}
Kevin Hakanson
fonte
1
Uau, essa regex não alfanumérica é intensa. Eu não acho que o | na expressão é necessário.
Ajax
11

O método mais rápido é:

function escapeHTML(html) {
    return document.createElement('div').appendChild(document.createTextNode(html)).parentNode.innerHTML;
}

Este método é cerca de duas vezes mais rápido do que os métodos baseados em 'substituir', consulte http://jsperf.com/htmlencoderegex/35 .

Fonte: https://stackoverflow.com/a/17546215/698168

Julien Kronegg
fonte
9

Script tudo-em-um:

// HTML entities Encode/Decode

function htmlspecialchars(str) {
    var map = {
        "&": "&amp;",
        "<": "&lt;",
        ">": "&gt;",
        "\"": "&quot;",
        "'": "&#39;" // ' -> &apos; for XML only
    };
    return str.replace(/[&<>"']/g, function(m) { return map[m]; });
}
function htmlspecialchars_decode(str) {
    var map = {
        "&amp;": "&",
        "&lt;": "<",
        "&gt;": ">",
        "&quot;": "\"",
        "&#39;": "'"
    };
    return str.replace(/(&amp;|&lt;|&gt;|&quot;|&#39;)/g, function(m) { return map[m]; });
}
function htmlentities(str) {
    var textarea = document.createElement("textarea");
    textarea.innerHTML = str;
    return textarea.innerHTML;
}
function htmlentities_decode(str) {
    var textarea = document.createElement("textarea");
    textarea.innerHTML = str;
    return textarea.value;
}

http://pastebin.com/JGCVs0Ts

baptx
fonte
Eu não fiz downvote, mas todas as substituições de estilo regex falharão ao codificar Unicode ... Então, qualquer um que use um idioma estrangeiro ficará desapontado. O truque <textarea> mencionado acima é muito legal e lida com tudo de forma rápida e segura.
Ajax
O regex funciona bem para mim com vários caracteres Unicode não latinos. Eu não esperaria mais nada. Como você acha que isso não funcionaria? Você está pensando em páginas de código de byte único que requerem entidades HTML? É para isso que servem a 3ª e a 4ª funções, e não explicitamente a 1ª e a segunda. Gosto da diferenciação.
dia
@LonelyPixel Acho que ele não verá seu comentário se você não mencioná-lo ("Apenas um usuário adicional pode ser notificado; o proprietário da postagem sempre será notificado")
baptx
Eu não sabia que existiam notificações direcionadas. @Ajax por favor, veja meu comentário acima.
ygoe 01 de
@LonelyPixel eu vejo agora. Por algum motivo, não achei que houvesse uma substituição de estilo textarea nesta resposta. Eu estava, de fato, pensando em grandes valores unicode de código duplo, como o mandarim. Quer dizer, seria possível fazer uma regex inteligente o suficiente, mas quando você olha os atalhos que os fornecedores de navegadores podem usar, eu me sentiria bem apostando que textarea será muito mais rápido (do que uma regex completamente competente). Alguém postou um benchmark sobre esta resposta? Jurei que tinha visto um.
Ajax
2

function encode(r) {
  return r.replace(/[\x26\x0A\x3c\x3e\x22\x27]/g, function(r) {
	return "&#" + r.charCodeAt(0) + ";";
  });
}

test.value=encode('How to encode\nonly html tags &<>\'" nice & fast!');

/*
 \x26 is &ampersand (it has to be first),
 \x0A is newline,
 \x22 is ",
 \x27 is ',
 \x3c is <,
 \x3e is >
*/
<textarea id=test rows=11 cols=55>www.WHAK.com</textarea>

Dave Brown
fonte
1

Não estou totalmente certo sobre velocidade, mas se você está procurando simplicidade, sugiro usar a função de escape lodash / sublinhado .

Gilmatic
fonte
0

Método de Martijn como função única com manipulação da marca " ( usando em javascript ):

function escapeHTML(html) {
    var fn=function(tag) {
        var charsToReplace = {
            '&': '&amp;',
            '<': '&lt;',
            '>': '&gt;',
            '"': '&#34;'
        };
        return charsToReplace[tag] || tag;
    }
    return html.replace(/[&<>"]/g, fn);
}
iman
fonte
0

Vou adicionar XMLSerializerà pilha. Ele fornece o resultado mais rápido sem usar nenhum cache de objeto (nem no serializador, nem no nó Texto).

function serializeTextNode(text) {
  return new XMLSerializer().serializeToString(document.createTextNode(text));
}

O bônus adicional é que ele suporta atributos que são serializados de forma diferente dos nós de texto:

function serializeAttributeValue(value) {
  const attr = document.createAttribute('a');
  attr.value = value;
  return new XMLSerializer().serializeToString(attr);
}

Você pode ver o que está realmente substituindo verificando a especificação, tanto para nós de texto quanto para valores de atributo . A documentação completa tem mais tipos de nós, mas o conceito é o mesmo.

Quanto ao desempenho, é o mais rápido quando não está em cache. Quando você permite o armazenamento em cache, innerHTMLé mais rápido chamar um HTMLElement com um nó de texto filho. Regex seria o mais lento (como comprovado por outros comentários). Claro, XMLSerializer poderia ser mais rápido em outros navegadores, mas em meus testes (limitados), a innerHTMLé mais rápido.


Linha única mais rápida:

new XMLSerializer().serializeToString(document.createTextNode(text));

Mais rápido com cache:

const cachedElementParent = document.createElement('div');
const cachedChildTextNode = document.createTextNode('');
cachedElementParent.appendChild(cachedChildTextNode);

function serializeTextNode(text) {
  cachedChildTextNode.nodeValue = text;
  return cachedElementParent.innerHTML;
}

https://jsperf.com/htmlentityencode/1

Pavio curto
fonte
-3

Um pouco tarde para o show, mas o que há de errado em usar encodeURIComponent () e decodeURIComponent () ?

suncat100
fonte
1
Esses fazem algo completamente não relacionado
callum
1
Talvez o maior abuso da palavra "completamente" que já ouvi. Por exemplo, em relação à questão do tópico principal, ele poderia ser usado para decodificar uma string html (obviamente por algum motivo de armazenamento), independentemente das tags html, e então facilmente codificá-la de volta para html quando e se necessário.
suncat100,