Desinfectar / reescribir HTML en el lado del cliente

82

Necesito mostrar los recursos externos cargados a través de solicitudes entre dominios y asegurarme de mostrar solo contenido " seguro ".

Podría usar String # stripScripts de Prototype para eliminar bloques de script. Pero los manipuladores como onclicko onerrortodavía están allí.

¿Hay alguna biblioteca que pueda al menos

  • bloques de secuencia de comandos de tira,
  • matar a los controladores DOM,
  • eliminar las etiquetas de la lista negra (por ejemplo: embedo object).

Entonces, ¿existen enlaces y ejemplos relacionados con JavaScript?

aemkei
fuente
12
No confíe en las respuestas que podría hacer esto mediante expresiones regulares stackoverflow.com/questions/1732348/...
Mikko Ohtamaa
¿Cómo es esto seguro? ¿Los usuarios no pueden editar el javascript de una página?
Daniel dice Reincorporar a Monica
sí, no es "seguro" a menos que simplemente intente evitar errores de usuarios de confianza.
Scott

Respuestas:

112

Actualización 2016: ahora hay un paquete de cierre de Google basado en el desinfectante Caja.

Tiene una API más limpia, se reescribió para tener en cuenta las API disponibles en los navegadores modernos e interactúa mejor con Closure Compiler.


Enchufe desvergonzado: consulte caja / plugin / html-sanitizer.js para obtener un desinfectante html del lado del cliente que ha sido revisado a fondo.

Está en la lista blanca, no en la lista negra, pero las listas blancas se pueden configurar según CajaWhitelists


Si desea eliminar todas las etiquetas, haga lo siguiente:

var tagBody = '(?:[^"\'>]|"[^"]*"|\'[^\']*\')*';

var tagOrComment = new RegExp(
    '<(?:'
    // Comment body.
    + '!--(?:(?:-*[^->])*--+|-?)'
    // Special "raw text" elements whose content should be elided.
    + '|script\\b' + tagBody + '>[\\s\\S]*?</script\\s*'
    + '|style\\b' + tagBody + '>[\\s\\S]*?</style\\s*'
    // Regular name
    + '|/?[a-z]'
    + tagBody
    + ')>',
    'gi');
function removeTags(html) {
  var oldHtml;
  do {
    oldHtml = html;
    html = html.replace(tagOrComment, '');
  } while (html !== oldHtml);
  return html.replace(/</g, '&lt;');
}

La gente le dirá que puede crear un elemento y asignar innerHTMLy luego obtener la innerTexto textContent, y luego escapar de las entidades en eso. No hagas eso. Es vulnerable a la inyección XSS, ya <img src=bogus onerror=alert(1337)>que ejecutará el onerrorcontrolador incluso si el nodo nunca está adjunto al DOM.

Mike Samuel
fuente
5
Genial, parece que hay una pequeña documentación aquí: code.google.com/p/google-caja/wiki/JsHtmlSanitizer
tmcw
3
El código de desinfección de Caja HTML se ve muy bien, pero requiere un código de pegamento (vecino cssparser.js, pero más importante, el html4objeto). Además, contamina la windowpropiedad global . ¿Existe una versión para la web de este código? Si no es así, ¿ve una mejor manera de producir y mantener uno que crear un proyecto separado para él?
Phihag
1
@phihag, pregunte en google-caja- discus y es posible que le indiquen uno empaquetado. Creo que la contaminación del objeto de la ventana es por compatibilidad con versiones anteriores, por lo que es posible que cualquier nueva versión del paquete no lo necesite.
Mike Samuel
1
Resulta que ya existe un paquete para navegadores web.
Phihag
2
@phihag Ese paquete es para nodejs, no para navegadores.
Jeffery al
40

El desinfectante de HTML de Google Caja se puede hacer "listo para la web" insertándolo en un trabajador web . Cualquier variable global introducida por el desinfectante estará contenida dentro del trabajador, además el procesamiento tiene lugar en su propio hilo.

Para los navegadores que no son compatibles con Web Workers, podemos usar un iframe como un entorno separado para que funcione el desinfectante. Timothy Chien tiene un polyfill que hace exactamente esto, usando iframes para simular Web Workers, por lo que esa parte está hecha por nosotros.

El proyecto Caja tiene una página wiki sobre cómo utilizar Caja como un desinfectante independiente del lado del cliente :

  • Verifique la fuente, luego compile ejecutando ant
  • Incluir html-sanitizer-minified.jso html-css-sanitizer-minified.jsen su página
  • Llamada html_sanitize(...)

El script del trabajador solo necesita seguir esas instrucciones:

importScripts('html-css-sanitizer-minified.js'); // or 'html-sanitizer-minified.js'

var urlTransformer, nameIdClassTransformer;

// customize if you need to filter URLs and/or ids/names/classes
urlTransformer = nameIdClassTransformer = function(s) { return s; };

// when we receive some HTML
self.onmessage = function(event) {
    // sanitize, then send the result back
    postMessage(html_sanitize(event.data, urlTransformer, nameIdClassTransformer));
};

(Se necesita un poco más de código para que funcione la biblioteca simworker, pero no es importante para esta discusión).

Demostración: https://dl.dropbox.com/u/291406/html-sanitize/demo.html

Jeffery a
fuente
Gran respuesta. Jeffrey, ¿puede explicar por qué la desinfección debe realizarla un trabajador web?
Austin Wang
Los trabajadores web de @AustinWang no son estrictamente necesarios, pero dado que la desinfección puede ser potencialmente costosa desde el punto de vista computacional y no requiere la interacción del usuario, es muy adecuada para la tarea. (También mencioné que contiene variables globales en la respuesta principal.)
Jeffery To
No puedo encontrar documentación decente para esta biblioteca. ¿Dónde / cómo especifico mi lista blanca de elementos y atributos?
AsGoodAsItGets
@AsGoodAsItGets Como se describe en un comentario en la versión actual , nameIdClassTransformerse llama para cada nombre HTML, ID de elemento y lista de clases; regresar nulleliminará el atributo. Al editar los archivos JSON en src / com / google / caja / lang / html , también puede personalizar qué elementos y atributos están incluidos en la lista blanca.
Jeffery To
@JefferyTo Lo siento, tal vez soy demasiado tonto, pero no lo entiendo. Los archivos JSON a los que hace referencia no se utilizan en su ejemplo y demostración anteriores. Quiero usar la biblioteca en un navegador, así que miré su demostración. Se puede modificar la nameIdClassTranformerfunción anterior, por ejemplo, para rechazar todas las <script>etiquetas y aceptar <b>y <i>etiquetas?
AsGoodAsItGets
20

Nunca confíes en el cliente. Si está escribiendo una aplicación de servidor, asuma que el cliente siempre enviará datos malintencionados e insalubres. Es una regla general que lo mantendrá fuera de problemas. Si puede, le recomendaría hacer toda la validación y el saneamiento en el código del servidor, que sabe (en un grado razonable) no se manipulará. ¿Quizás podría usar una aplicación web del lado del servidor como proxy para su código del lado del cliente, que obtiene de un tercero y realiza el saneamiento antes de enviarlo al cliente mismo?

[editar] Lo siento, entendí mal la pregunta. Sin embargo, sigo mi consejo. Sus usuarios probablemente estarán más seguros si desinfecta en el servidor antes de enviárselo.

Sean Edwards
fuente
19
En realidad, con el aumento de la popularidad de node.js, una solución javascript también podría ser una solución del lado del servidor. Así es como terminé aquí al menos. Aún así, este es un excelente consejo para vivir.
Nicholas Flynt
15

Ahora que todos los navegadores principales admiten iframes de espacio aislado, hay una forma mucho más sencilla de que creo que puede ser segura. Me encantaría que esta respuesta pudiera ser revisada por personas más familiarizadas con este tipo de problema de seguridad.

NOTA: Este método definitivamente no funcionará en IE 9 y versiones anteriores. Consulte esta tabla para conocer las versiones del navegador que admiten la zona de pruebas. (Nota: la tabla parece indicar que no funcionará en Opera Mini, pero lo intenté y funcionó).

La idea es crear un iframe oculto con JavaScript desactivado, pegar su HTML que no es de confianza en él y dejar que lo analice. Luego, puede recorrer el árbol DOM y copiar las etiquetas y atributos que se consideran seguros.

Las listas blancas que se muestran aquí son solo ejemplos. Lo mejor para incluir en la lista blanca dependería de la aplicación. Si necesita una política más sofisticada que solo listas blancas de etiquetas y atributos, puede acomodarla con este método, aunque no con este código de ejemplo.

var tagWhitelist_ = {
  'A': true,
  'B': true,
  'BODY': true,
  'BR': true,
  'DIV': true,
  'EM': true,
  'HR': true,
  'I': true,
  'IMG': true,
  'P': true,
  'SPAN': true,
  'STRONG': true
};

var attributeWhitelist_ = {
  'href': true,
  'src': true
};

function sanitizeHtml(input) {
  var iframe = document.createElement('iframe');
  if (iframe['sandbox'] === undefined) {
    alert('Your browser does not support sandboxed iframes. Please upgrade to a modern browser.');
    return '';
  }
  iframe['sandbox'] = 'allow-same-origin';
  iframe.style.display = 'none';
  document.body.appendChild(iframe); // necessary so the iframe contains a document
  iframe.contentDocument.body.innerHTML = input;

  function makeSanitizedCopy(node) {
    if (node.nodeType == Node.TEXT_NODE) {
      var newNode = node.cloneNode(true);
    } else if (node.nodeType == Node.ELEMENT_NODE && tagWhitelist_[node.tagName]) {
      newNode = iframe.contentDocument.createElement(node.tagName);
      for (var i = 0; i < node.attributes.length; i++) {
        var attr = node.attributes[i];
        if (attributeWhitelist_[attr.name]) {
          newNode.setAttribute(attr.name, attr.value);
        }
      }
      for (i = 0; i < node.childNodes.length; i++) {
        var subCopy = makeSanitizedCopy(node.childNodes[i]);
        newNode.appendChild(subCopy, false);
      }
    } else {
      newNode = document.createDocumentFragment();
    }
    return newNode;
  };

  var resultElement = makeSanitizedCopy(iframe.contentDocument.body);
  document.body.removeChild(iframe);
  return resultElement.innerHTML;
};

Puedes probarlo aquí .

Tenga en cuenta que estoy rechazando los atributos y etiquetas de estilo en este ejemplo. Si los permitiste, probablemente querrás analizar el CSS y asegurarte de que sea seguro para tus propósitos.

Probé esto en varios navegadores modernos (Chrome 40, Firefox 36 Beta, IE 11, Chrome para Android) y en uno antiguo (IE 8) para asegurarme de que funcionara antes de ejecutar cualquier script. Me interesaría saber si hay navegadores que tengan problemas con él, o casos extremos que estoy pasando por alto.

aldel
fuente
10
Esta publicación merece la atención de los expertos, ya que parece ser la solución más obvia y simple. ¿Es realmente seguro?
ruega
¿Cómo se puede crear mediante programación un iframe oculto "con JavaScript desactivado"? Que yo sepa, esto es imposible. En el momento en que lo haga iframe.contentDocument.body.innerHTML = input, se ejecutarán las etiquetas de script que contenga.
AsGoodAsItGets
@AsGoodAsItGets: busque el atributo sandbox en iframes.
aldel
1
@aldel De hecho, no lo sabía. Para nosotros sigue siendo un fracaso debido a la falta de soporte en IE9. Supongo que su solución podría funcionar, pero creo que debería aclarar en su respuesta que depende del sandboxatributo.
AsGoodAsItGets
Lo siento, pensé que estaba claro desde mi apertura "Ahora que todos los navegadores principales admiten iframes en espacio aislado". Agregaré una nota menos sutil.
aldel
12

No puede anticipar todos los posibles tipos extraños de marcado mal formado con el que algún navegador en algún lugar podría tropezar para escapar de la lista negra, así que no lo haga. Hay muchas más estructuras que podría necesitar eliminar además de script / incrustación / objeto y controladores.

En su lugar, intente analizar el HTML en elementos y atributos en una jerarquía, luego ejecute todos los nombres de elementos y atributos en una lista blanca lo más mínima posible. También verifique cualquier atributo de URL que deje pasar en una lista blanca (recuerde que hay protocolos más peligrosos que solo javascript :).

Si la entrada es XHTML bien formado, la primera parte de lo anterior es mucho más fácil.

Como siempre con el saneamiento de HTML, si puede encontrar alguna otra forma de evitar hacerlo, hágalo en su lugar. Hay muchos, muchos agujeros potenciales. Si los principales servicios de correo web siguen encontrando vulnerabilidades después de tantos años, ¿qué le hace pensar que puede hacerlo mejor?

bobince
fuente
11

Entonces, es 2016, y creo que muchos de nosotros estamos usando npmmódulos en nuestro código ahora. sanitize-htmlparece ser la opción principal en npm, aunque hay otras .

Otras respuestas a esta pregunta brindan una excelente información sobre cómo desarrollar la suya propia, pero este es un problema lo suficientemente complicado como para que las soluciones comunitarias bien probadas sean probablemente la mejor respuesta.

Ejecute esto en la línea de comando para instalar: npm install --save sanitize-html

ES5: var sanitizeHtml = require('sanitize-html'); // ... var sanitized = sanitizeHtml(htmlInput);

ES6: import sanitizeHtml from 'sanitize-html'; // ... let sanitized = sanitizeHtml(htmlInput);

ericsoco
fuente
10
2018 aquí, esto es demasiado pesado (medio megabyte de dependencias)
user1464581
2020 aquí, sanitize-html es para Node y todavía no hay una buena opción para los navegadores, por lo que puedo decir
Mick
3

[Descargo de responsabilidad: soy uno de los autores]

Escribimos una biblioteca de código abierto "solo web" (es decir, "requiere un navegador") para esto, https://github.com/jitbit/HtmlSanitizer que elimina todos tags/attributes/stylesexcepto los "incluidos en la lista blanca".

Uso:

var input = HtmlSanitizer.SanitizeHtml("<script> Alert('xss!'); </scr"+"ipt>");

PS Funciona mucho más rápido que una solución de "JavaScript puro", ya que utiliza el navegador para analizar y manipular DOM. Si está interesado en una solución de "JS puro", pruebe https://github.com/punkave/sanitize-html (no afiliado)

Alex
fuente
2

La biblioteca de Google Caja sugerida anteriormente era demasiado compleja para configurar e incluir en mi proyecto para una aplicación web (por lo tanto, ejecutándose en el navegador). Lo que recurrí en su lugar, dado que ya usamos el componente CKEditor, es usar su función incorporada de desinfección y lista blanca de HTML, que es mucho más fácil de configurar. Entonces, puede cargar una instancia de CKEditor en un iframe oculto y hacer algo como:

CKEDITOR.instances['myCKEInstance'].dataProcessor.toHtml(myHTMLstring)

Ahora, por supuesto, si no está utilizando CKEditor en su proyecto, esto puede ser un poco exagerado, ya que el componente en sí tiene alrededor de medio megabyte (minimizado), pero si tiene las fuentes, tal vez pueda aislar el código haciendo la lista blanca ( CKEDITOR.htmlParser?) y hacerla mucho más corta.

http://docs.ckeditor.com/#!/api

http://docs.ckeditor.com/#!/api/CKEDITOR.htmlDataProcessor

Mejor imposible
fuente
0

Recomiendo eliminar los marcos de tu vida, te facilitaría mucho las cosas a largo plazo.

cloneNode: La clonación de un nodo copias de todos sus atributos y sus valores, pero no NO copiar los detectores de eventos .

https://developer.mozilla.org/en/DOM/Node.cloneNode

Lo siguiente no está probado, aunque he estado usando treewalkers desde hace algún tiempo y son una de las partes más infravaloradas de JavaScript. Aquí hay una lista de los tipos de nodos que puede rastrear, normalmente yo uso SHOW_ELEMENT o SHOW_TEXT .

http://www.w3.org/TR/DOM-Level-2-Traversal-Range/traversal.html#Traversal-NodeFilter

function xhtml_cleaner(id)
{
 var e = document.getElementById(id);
 var f = document.createDocumentFragment();
 f.appendChild(e.cloneNode(true));

 var walker = document.createTreeWalker(f,NodeFilter.SHOW_ELEMENT,null,false);

 while (walker.nextNode())
 {
  var c = walker.currentNode;
  if (c.hasAttribute('contentEditable')) {c.removeAttribute('contentEditable');}
  if (c.hasAttribute('style')) {c.removeAttribute('style');}

  if (c.nodeName.toLowerCase()=='script') {element_del(c);}
 }

 alert(new XMLSerializer().serializeToString(f));
 return f;
}


function element_del(element_id)
{
 if (document.getElementById(element_id))
 {
  document.getElementById(element_id).parentNode.removeChild(document.getElementById(element_id));
 }
 else if (element_id)
 {
  element_id.parentNode.removeChild(element_id);
 }
 else
 {
  alert('Error: the object or element \'' + element_id + '\' was not found and therefore could not be deleted.');
 }
}
John
fuente
5
Este código asume que la entrada para limpiar ya ha sido analizada e incluso insertada en el árbol del documento. Si ese es el caso, los scripts maliciosos ya se han ejecutado. La entrada debe ser una cadena.
phihag
Luego, envíele un fragmento de DOM, solo porque esté en el DOM en una forma o formato dado, no implica que se haya ejecutado. Suponiendo que lo está cargando a través de AJAX, puede usarlo junto con importNode.
Juan