¿Es posible ejecutar Sandbox JavaScript en el navegador?

142

Me pregunto si es posible crear un sandbox de JavaScript en el navegador para evitar el acceso a funciones que normalmente están disponibles para el código JavaScript que se ejecuta en una página HTML.

Por ejemplo, supongamos que quiero proporcionar una API de JavaScript para que los usuarios finales les permitan definir manejadores de eventos que se ejecutarán cuando ocurran "eventos interesantes", pero no quiero que esos usuarios accedan a las propiedades y funciones del windowobjeto. ¿Soy capaz de hacer esto?

En el caso más simple, digamos que quiero evitar que los usuarios llamen alert. Un par de enfoques que puedo pensar son:

  • Redefinir window.alertglobalmente. No creo que este sea un enfoque válido porque podría querer usar otro código que se ejecute en la página (es decir, material no creado por los usuarios en sus controladores de eventos) alert.
  • Envíe el código del controlador de eventos al servidor para procesar. No estoy seguro de que enviar el código al servidor para procesar sea el enfoque correcto porque los controladores de eventos deben ejecutarse en el contexto de la página.

¿Quizás una solución en la que el servidor procese la función definida por el usuario y luego genere una devolución de llamada que se ejecutará en el cliente funcionaría? Incluso si ese enfoque funciona, ¿hay mejores maneras de resolver este problema?

Walter Rumsby
fuente

Respuestas:

54

Google Caja es un traductor de fuente a fuente que "le permite poner HTML y JavaScript de terceros no confiables en línea en su página y aún así estar seguro".

Darius Bacon
fuente
55
Una prueba rápida muestra que Caja no puede proteger el navegador de ataques de CPU como while (1) {}--- simplemente se cuelga. Del mismo modo a=[]; while (1) { a=[a,a]; }.
David dado el
55
Sí, la denegación de servicio está fuera del alcance: code.google.com/p/google-caja/issues/detail?id=1406
Darius Bacon
32

Echa un vistazo a ADsafe de Douglas Crockford :

ADsafe hace que sea seguro poner código de invitado (como publicidad con guiones de terceros o widgets) en cualquier página web. ADsafe define un subconjunto de JavaScript que es lo suficientemente potente como para permitir que el código de invitado realice interacciones valiosas, al mismo tiempo que evita daños o intrusiones maliciosas o accidentales. El subconjunto ADsafe puede verificarse mecánicamente mediante herramientas como JSLint para que no sea necesaria una inspección humana para revisar el código de invitado por seguridad. El subconjunto ADsafe también aplica buenas prácticas de codificación, lo que aumenta la probabilidad de que el código de invitado se ejecute correctamente.

Puede ver un ejemplo de cómo usar ADsafe mirando los archivos template.htmly template.jsen el repositorio GitHub del proyecto .

Simon Lieschke
fuente
En su sitio, no veo forma de usar ADsafe. No hay forma de descargarlo, ningún enlace al código, nada. ¿Cómo puedes probar ADsafe?
BT
2
Además, evita cualquier acceso a this, lo cual es completamente inaceptable. No puedes escribir un buen JavaScript sin usarlo this.
BT
44
@BT He escrito proyectos enteros sin usar this. No es difícil evitar el parámetro mal nombrado.
soundly_typed
2
@BT Sería tonto decir que completar proyectos del mundo real es inaceptable. Pero lamento comenzar esta discusión, y debo retirarme; Este no es el lugar para discutir tales cosas (lo siento). Estoy en Twitter si quieres discutir más.
soundly_typed
1
@BT (continuaré porque es relevante para la pregunta) Cada vez que ejecute código en el entorno de otra persona, se encontrará con reglas y restricciones. No lo llamaría inaceptable. Un "dolor en el culo", tal vez. Pero no es inaceptable. Después de todo, para cada uso de this, hay una no- thismanera igual y equivalente de hacerlo (es solo un parámetro, después de todo).
soundly_typed
24

Creé una biblioteca de sandboxing llamada jsandbox que usa trabajadores web para sandbox el código evaluado. También tiene un método de entrada para proporcionar explícitamente datos de código de espacio aislado que de otro modo no podría obtener.

El siguiente es un ejemplo de la API:

jsandbox
    .eval({
      code    : "x=1;Math.round(Math.pow(input, ++x))",
      input   : 36.565010597564445,
      callback: function(n) {
          console.log("number: ", n); // number: 1337
      }
  }).eval({
      code   : "][];.]\\ (*# ($(! ~",
      onerror: function(ex) {
          console.log("syntax error: ", ex); // syntax error: [error object]
      }
  }).eval({
      code    : '"foo"+input',
      input   : "bar",
      callback: function(str) {
          console.log("string: ", str); // string: foobar
      }
  }).eval({
      code    : "({q:1, w:2})",
      callback: function(obj) {
          console.log("object: ", obj); // object: object q=1 w=2
      }
  }).eval({
      code    : "[1, 2, 3].concat(input)",
      input   : [4, 5, 6],
      callback: function(arr) {
          console.log("array: ", arr); // array: [1, 2, 3, 4, 5, 6]
      }
  }).eval({
      code    : "function x(z){this.y=z;};new x(input)",
      input   : 4,
      callback: function(x) {
          console.log("new x: ", x); // new x: object y=4
      }
  });
Eli Gray
fuente
+1: Esto se ve realmente genial. ¿Qué tan seguro es ejecutar el código del usuario de esta manera?
Konstantin Tarkus
1
Muy seguro. Echa un vistazo a la biblioteca actualizada en github .
Eli Gray
1
¿Se mantiene este proyecto? Veo que no se ha actualizado desde hace más de 2 años ...
Yanick Rochon
Me gusta esto, excepto que si desea sandbox pero aún permite que el código de acceso diga jQuery, esto fallará ya que los trabajadores web no permiten la manipulación DOM.
Rahly
Hola Eli: gracias por una gran lib, ¿estás planeando mantenerla? Tengo una solicitud de cambio para agregar la funcionalidad de depuración, que al mirar rápidamente el código debería ser posible. ¿Por favor dejame saber lo que tu piensas?
user1514042
8

Creo que vale la pena mencionar js.js aquí. Es un intérprete de JavaScript escrito en JavaScript.

Es aproximadamente 200 veces más lento que el JS nativo, pero su naturaleza lo convierte en un entorno sandbox perfecto. Otro inconveniente es su tamaño: casi 600 kb, que puede ser aceptable para computadoras de escritorio en algunos casos, pero no para dispositivos móviles.

gronostaj
fuente
7

Como se mencionó en otras respuestas, es suficiente encarcelar el código en iframe de espacio aislado (sin enviarlo al lado del servidor) y comunicarse con mensajes. Sugeriría echar un vistazo a una pequeña biblioteca que creé principalmente debido a la necesidad de proporcionar alguna API al código no confiable, tal como se describe en la pregunta: existe la oportunidad de exportar el conjunto particular de funciones directamente al sandbox donde se ejecuta el código no confiable. Y también hay una demostración que ejecuta el código enviado por un usuario en un sandbox:

http://asvd.github.io/jailed/demos/web/console/

asvd
fuente
4

Todos los proveedores de navegadores y la especificación HTML5 están trabajando hacia una propiedad de espacio aislado real para permitir iframes de espacio aislado, pero aún se limita a la granularidad de iframe.

En general, ningún grado de expresiones regulares, etc. puede desinfectar de forma segura JavaScript arbitrario proporcionado por el usuario, ya que degenera en el problema de detención: - /

olliej
fuente
2
¿Puedes explicar cómo se degenera en el problema de detención?
hdgarrood
2
La imposibilidad teórica de resolver el problema de detención solo se aplica realmente al análisis de código estático. Los sandboxes pueden hacer cosas como imponer límites de tiempo para lidiar con el problema de detención.
Aviendha
4

Una versión mejorada del código sandbox de los trabajadores web de @ RyanOHara, en un solo archivo (no eval.jsse necesita ningún archivo adicional ).

function safeEval(untrustedCode)
    {
    return new Promise(function (resolve, reject)
    {

    var blobURL = URL.createObjectURL(new Blob([
        "(",
        function ()
            {
            var _postMessage = postMessage;
            var _addEventListener = addEventListener;

            (function (obj)
                {
                "use strict";

                var current = obj;
                var keepProperties = [
                    // required
                    'Object', 'Function', 'Infinity', 'NaN', 'undefined', 'caches', 'TEMPORARY', 'PERSISTENT', 
                    // optional, but trivial to get back
                    'Array', 'Boolean', 'Number', 'String', 'Symbol',
                    // optional
                    'Map', 'Math', 'Set',
                ];

                do {
                    Object.getOwnPropertyNames(current).forEach(function (name) {
                        if (keepProperties.indexOf(name) === -1) {
                            delete current[name];
                        }
                    });

                    current = Object.getPrototypeOf(current);
                }
                while (current !== Object.prototype);
                })(this);

            _addEventListener("message", function (e)
            {
            var f = new Function("", "return (" + e.data + "\n);");
            _postMessage(f());
            });
            }.toString(),
        ")()"], {type: "application/javascript"}));

    var worker = new Worker(blobURL);

    URL.revokeObjectURL(blobURL);

    worker.onmessage = function (evt)
        {
        worker.terminate();
        resolve(evt.data);
        };

    worker.onerror = function (evt)
        {
        reject(new Error(evt.message));
        };

    worker.postMessage(untrustedCode);

    setTimeout(function () {
        worker.terminate();
        reject(new Error('The worker timed out.'));
        }, 1000);
    });
    }

Pruébalo:

https://jsfiddle.net/kp0cq6yw/

var promise = safeEval("1+2+3");

promise.then(function (result) {
      alert(result);
      });

Debería salir 6(probado en Chrome y Firefox).

MarcG
fuente
2

De una manera fea, pero tal vez esto funcione para usted, tomé todos los globales y los redefiní en el ámbito de la caja de arena, y también agregué el modo estricto para que no puedan obtener el objeto global utilizando una función anónima.

function construct(constructor, args) {
  function F() {
      return constructor.apply(this, args);
  }
  F.prototype = constructor.prototype;
  return new F();
}
// Sanboxer 
function sandboxcode(string, inject) {
  "use strict";
  var globals = [];
  for (var i in window) {
    // <--REMOVE THIS CONDITION
    if (i != "console")
    // REMOVE THIS CONDITION -->
    globals.push(i);
  }
  globals.push('"use strict";\n'+string);
  return construct(Function, globals).apply(inject ? inject : {});
}
sandboxcode('console.log( this, window, top , self, parent, this["jQuery"], (function(){return this;}()));'); 
// => Object {} undefined undefined undefined undefined undefined undefined 
console.log("return of this", sandboxcode('return this;', {window:"sanboxed code"})); 
// => Object {window: "sanboxed code"}

https://gist.github.com/alejandrolechuga/9381781

alejandro
fuente
3
Trivial para windowvolver de eso. sandboxcode('console.log((0,eval)("this"))')
Ry-
Tendré que descubrir cómo prevenir eso
alejandro
@alejandro ¿Encontraste una manera de evitar eso?
marchita el
1
Mi implementación solo agrega:function sbx(s,p) {e = eval; eval = function(t){console.log("GOT GOOD")}; sandboxcode(s,p); eval =e}
YoniXw
2
@YoniXw: Espero que no termines usándolo para nada. Ningún enfoque como este funcionará jamás. (_=>_).constructor('return this')()
Ry-
1

Es más probable que un intérprete de Javascript independiente produzca una caja de arena robusta que una versión enjaulada de la implementación del navegador incorporado. Ryan ya ha mencionado js.js , pero un proyecto más actualizado es JS-Interpreter . Los documentos cubren cómo exponer varias funciones al intérprete, pero su alcance es muy limitado.

David Fraser
fuente
1

A partir de 2019, vm2 parece la solución más popular y más actualizada regularmente para este problema.

Bret Cameron
fuente
vm2 no admite tiempo de ejecución en el navegador. Sin embargo, debería funcionar si está buscando código sandbox en una aplicación de nodejs.
kevin.groat
0

Con NISP podrás hacer una evaluación de espacio aislado. Aunque la expresión que escribe no es exactamente un JS, en su lugar, escribirá expresiones-s. Ideal para DSL simples que no requieren una programación extensa.

Kannan Ramamoorthy
fuente
-3

1) Suponga que tiene un código para ejecutar:

var sCode = "alert(document)";

Ahora, suponga que desea ejecutarlo en un sandbox:

new Function("window", "with(window){" + sCode + "}")({});

Estas dos líneas cuando se ejecuten fallarán, porque la función "alerta" no está disponible desde el "entorno limitado"

2) Y ahora desea exponer un miembro de un objeto de ventana con su funcionalidad:

new Function("window", "with(window){" + sCode + "}")({
    'alert':function(sString){document.title = sString}
});

De hecho, puede agregar citas de escape y hacer otro pulido, pero supongo que la idea es clara.

Sergey Ilinsky
fuente
77
¿No hay una miríada de otras formas de llegar al objeto global? Por ejemplo, dentro de una función llamada usando func.apply (nulo) "este" será el objeto de ventana.
mbarkhau
55
El primer ejemplo no falla, este es un ejemplo muy inválido de sandboxing.
Andy E
1
var sCode = "this.alert ('FAIL')";
Leonard Pauli
-4

¿De dónde viene este usuario JavaScript?

No hay mucho que pueda hacer para que un usuario incruste código en su página y luego lo llame desde su navegador (consulte Greasemonkey, http://www.greasespot.net/ ). Es solo algo que hacen los navegadores.

Sin embargo, si almacena el script en una base de datos, luego lo recupera y lo evalúa (), entonces puede limpiar el script antes de ejecutarlo.

Ejemplos de código que elimina todas las ventanas. y documento. referencias:

 eval(
  unsafeUserScript
    .replace(/\/\/.+\n|\/\*.*\*\/, '') // Clear all comments
    .replace(/\s(window|document)\s*[\;\)\.]/, '') // removes window. or window; or window)
 )

Esto intenta evitar que se ejecute lo siguiente (no probado):

window.location = 'http://mydomain.com';
var w = window  ;

Hay muchas limitaciones que deberías aplicar al script de usuario inseguro. Desafortunadamente, no hay un 'contenedor de espacio aislado' disponible para JavaScript.

Dimitry
fuente
2
Si alguien está tratando de hacer algo malicioso, una simple expresión regular simplemente no puede hacerlo: tome (function () {this ["loca" + "tion"] = " example.com ";}) () En general si no puede confiar en sus usuarios (que es el caso de cualquier sitio en el que personas arbitrarias puedan agregar contenido) es necesario bloquear todos los js.
olliej
He usado algo similar en el pasado. No es perfecto, pero te lleva a la mayor parte del camino.
Sugendran
olliej, tienes razón sobre las limitaciones de tal técnica. ¿Qué tal sobreescribir variables globales como <code> var window = null, document = null, this = {}; </code>?
Dimitry
Dimitry Z, no se permite sobrescribir estas variables [en algunos navegadores]. También verifique mi solución en la lista de respuestas: funciona.
Sergey Ilinsky
-5

He estado trabajando en un sandbox js simplista para permitir a los usuarios crear applets para mi sitio. Aunque todavía enfrento algunos desafíos al permitir el acceso DOM (parentNode simplemente no me permite mantener las cosas seguras = /), mi enfoque fue simplemente redefinir el objeto de ventana con algunos de sus miembros útiles / inofensivos, y luego evaluar () al usuario código con esta ventana redefinida como el alcance predeterminado.

Mi código "central" es así ... (no lo estoy mostrando por completo;)

function Sandbox(parent){

    this.scope = {
        window: {
            alert: function(str){
                alert("Overriden Alert: " + str);
            },
            prompt: function(message, defaultValue){
                return prompt("Overriden Prompt:" + message, defaultValue);
            },
            document: null,
            .
            .
            .
            .
        }
    };

    this.execute = function(codestring){

        // here some code sanitizing, please

        with (this.scope) {
            with (window) {
                eval(codestring);
            }
        }
    };
}

Por lo tanto, puedo instanciar un Sandbox y usar execute () para ejecutar el código. Además, todas las nuevas variables declaradas dentro del código evaluado finalmente se vincularán con el alcance execute (), por lo que no habrá conflictos de nombres ni alteraciones con el código existente.

Aunque los objetos globales seguirán siendo accesibles, aquellos que deben permanecer desconocidos para el código de espacio aislado deben definirse como proxies en el objeto Sandbox :: scope.

Espero que esto funcione para usted.


fuente
8
Esto no sandbox nada. El código evadido puede eliminar miembros y llegar al alcance global de esa manera, o tomar una referencia al alcance global haciendo (function () {return this;}) ()
Mike Samuel
-6

Puede ajustar el código del usuario en una función que redefine los objetos prohibidos como parámetros; estos serían undefinedcuando se llama:

(function (alert) {

alert ("uh oh!"); // User code

}) ();

Por supuesto, los atacantes inteligentes pueden evitar esto inspeccionando el DOM de Javascript y encontrando un objeto no anulado que contenga una referencia a la ventana.


Otra idea es escanear el código del usuario utilizando una herramienta como jslint . Asegúrese de que esté configurado para que no tenga variables preestablecidas (o: solo las variables que desee), y luego, si se configuran o acceden algunos globales, no permita que se use el script del usuario. Una vez más, podría ser vulnerable a recorrer el DOM: los objetos que el usuario puede construir utilizando literales pueden tener referencias implícitas al objeto de ventana al que se puede acceder para escapar del entorno limitado.

John Millikin
fuente
2
Si el usuario ingresó window.alert en lugar de una alerta simple, omitiría ese límite.
Quentin
@Dorward: sí, por lo tanto, "objetos prohibidos". wrunsby debería decidir a qué objetos no puede acceder el usuario y colocarlos en la lista de parámetros.
John Millikin
Solo hay un objeto: ventana. Si no bloquea el acceso a él, entonces todo está disponible a través de él. Si lo bloquea, el script no puede acceder a ninguna de sus propiedades (ya que decir alert en lugar de window.alert solo implica la ventana).
Quentin
@Doward: ese no es el caso en el que bloquearías window.alert pero la alerta aún funcionaría, pruébalo. Esto se debe a que la ventana también es el objeto global. Uno necesitaría bloquear la ventana y cualquier propiedad o método de la ventana a la que no desea que acceda el código de usuario.
AnthonyWJones