Est-il possible d'exécuter JavaScript Sandbox dans le navigateur?

142

Je me demande s'il est possible de sandbox JavaScript s'exécutant dans le navigateur pour empêcher l'accès aux fonctionnalités qui sont normalement disponibles pour le code JavaScript s'exécutant dans une page HTML.

Par exemple, disons que je souhaite fournir une API JavaScript aux utilisateurs finaux pour leur permettre de définir des gestionnaires d'événements à exécuter lorsque des "événements intéressants" se produisent, mais je ne veux pas que ces utilisateurs accèdent aux propriétés et aux fonctions de l' windowobjet. Suis-je capable de faire ça?

Dans le cas le plus simple, disons que je veux empêcher les utilisateurs d'appeler alert. Quelques approches auxquelles je peux penser sont:

  • Redéfinissez window.alertglobalement. Je ne pense pas que ce serait une approche valide car d'autres codes exécutés dans la page (c'est-à-dire des éléments non créés par les utilisateurs dans leurs gestionnaires d'événements) pourraient vouloir utiliser alert.
  • Envoyez le code du gestionnaire d'événements au serveur à traiter. Je ne suis pas sûr que l'envoi du code au serveur à traiter soit la bonne approche car les gestionnaires d'événements doivent s'exécuter dans le contexte de la page.

Peut-être qu'une solution où le serveur traite la fonction définie par l'utilisateur puis génère un rappel à exécuter sur le client fonctionnerait? Même si cette approche fonctionne, existe-t-il de meilleures façons de résoudre ce problème?

Walter Rumsby
la source

Réponses:

54

Google Caja est un traducteur de source à source qui "vous permet de mettre en ligne du HTML et du JavaScript tiers non approuvés dans votre page tout en restant sécurisé".

Darius Bacon
la source
5
Un test rapide montre que Caja est incapable de protéger le navigateur contre les attaques du processeur comme while (1) {}--- il se bloque juste. De même a=[]; while (1) { a=[a,a]; }.
David donné le
5
Oui, le déni de service est hors de portée: code.google.com/p/google-caja/issues/detail?id=1406
Darius Bacon
32

Jetez un œil à ADsafe de Douglas Crockford :

ADsafe permet de mettre en toute sécurité du code invité (comme des publicités ou des widgets tiers) sur n'importe quelle page Web. ADsafe définit un sous-ensemble de JavaScript qui est suffisamment puissant pour permettre au code invité d'effectuer des interactions précieuses, tout en empêchant en même temps les dommages ou intrusions malveillants ou accidentels. Le sous-ensemble ADsafe peut être vérifié mécaniquement par des outils tels que JSLint afin qu'aucune inspection humaine ne soit nécessaire pour examiner le code client pour des raisons de sécurité. Le sous-ensemble ADsafe applique également de bonnes pratiques de codage, ce qui augmente la probabilité que le code invité s'exécute correctement.

Vous pouvez voir un exemple d'utilisation d'ADsafe en examinant les fichiers template.htmlet template.jsdans le référentiel GitHub du projet .

Simon Lieschke
la source
Sur leur site, je ne vois aucun moyen d'utiliser ADsafe. Il n'y a aucun moyen de le télécharger, pas de lien vers le code, rien. Comment pouvez-vous essayer ADsafe?
BT
2
En outre, il empêche tout accès à this, ce qui est totalement inacceptable. Vous ne pouvez pas écrire du bon javascript sans utiliser this.
BT
4
@BT J'ai écrit des projets entiers sans utiliser this. Il n'est pas difficile d'éviter le paramètre mal nommé.
soundly_typed
2
@BT Il serait ridicule de dire que la réalisation de projets réels est inacceptable. Mais je regrette d'avoir entamé cette discussion et je dois me retirer; ce n'est pas le lieu de discuter de telles choses (désolé). Je suis sur Twitter si vous souhaitez en discuter davantage.
soundly_typed
1
@BT (je vais continuer car c'est pertinent pour la question) Chaque fois que vous exécutez du code dans l'environnement de quelqu'un d'autre, vous rencontrerez des règles et des restrictions. Je n'appellerais pas cela inacceptable. Une "douleur dans le cul", peut-être. Mais pas inacceptable. Après tout, pour chaque utilisation de this, il existe un non- thismoyen égal et équivalent de le faire (c'est juste un paramètre, après tout).
soundly_typed le
24

J'ai créé une bibliothèque de sandboxing appelée jsandbox qui utilise des web workers pour sandbox évalué le code. Il dispose également d'une méthode d'entrée pour donner explicitement des données de code sandbox qu'il ne pourrait autrement pas obtenir.

Voici un exemple de l'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
la source
+1: Cela a l'air vraiment cool. Dans quelle mesure est-il sûr d'exécuter le code utilisateur de cette manière?
Konstantin Tarkus
1
Très sûr. Découvrez la bibliothèque mise à jour sur github .
Eli Gray
1
ce projet est-il toujours maintenu? Je vois qu'elle n'a pas été mise à jour depuis plus de 2 ans ...
Yanick Rochon du
J'aime ça, sauf que si vous voulez faire du bac à sable tout en autorisant l'accès au code pour dire jQuery, cela échouerait car les travailleurs Web n'autorisent pas la manipulation du DOM.
Rahly
Salut Eli - merci pour une excellente bibliothèque, envisagez-vous de la maintenir? J'ai une demande de modification pour ajouter une fonctionnalité de débogage - ce qui devrait être possible en regardant rapidement le code. S'il vous plait, faite moi part de votre avis?
user1514042
8

Je pense que js.js mérite d'être mentionné ici. C'est un interpréteur JavaScript écrit en JavaScript.

Il est environ 200 fois plus lent que le JS natif, mais sa nature en fait un environnement sandbox parfait. Un autre inconvénient est sa taille - près de 600 Ko, ce qui peut être acceptable pour les ordinateurs de bureau dans certains cas, mais pas pour les appareils mobiles.

Gronostaj
la source
7

Comme mentionné dans d'autres réponses, il suffit de mettre en prison le code dans une iframe en bac à sable (sans l'envoyer côté serveur) et de communiquer avec des messages. Je suggérerais de jeter un coup d'œil à une petite bibliothèque que j'ai créée principalement en raison de la nécessité de fournir une API au code non approuvé, comme décrit dans la question: il est possible d'exporter l'ensemble de fonctions particulier directement dans le bac à sable où le code non approuvé s'exécute. Et il y a aussi une démo qui exécute le code soumis par un utilisateur dans un bac à sable:

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

asvd
la source
4

Tous les fournisseurs de navigateurs et la spécification HTML5 travaillent sur une propriété sandbox réelle pour autoriser les iframes en sandbox - mais cela reste limité à la granularité iframe.

En général, aucun degré d'expressions régulières, etc. ne peut nettoyer en toute sécurité le JavaScript fourni par l'utilisateur arbitraire car il dégénère en problème d'arrêt: - /

olliej
la source
2
Pouvez-vous expliquer comment cela dégénère en problème d'arrêt?
hdgarrood
2
L'impossibilité théorique de résoudre le problème de l'arrêt ne s'applique réellement qu'à l'analyse de code statique. Les bacs à sable peuvent faire des choses comme imposer des limites de temps pour traiter le problème en suspens.
Aviendha
4

Une version améliorée du code sandbox des web workers de @ RyanOHara, dans un seul fichier (aucun eval.jsfichier supplémentaire n'est nécessaire).

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);
    });
    }

Essaye-le:

https://jsfiddle.net/kp0cq6yw/

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

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

Il devrait sortir 6(testé dans Chrome et Firefox).

MarcG
la source
2

Une manière moche mais peut-être que cela fonctionne pour vous, j'ai pris tous les globaux et les ai redéfinis dans la portée du bac à sable, ainsi que j'ai ajouté le mode strict afin qu'ils ne puissent pas obtenir l'objet global en utilisant une fonction anonyme.

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
la source
3
Trivial pour windowrevenir de ça. sandboxcode('console.log((0,eval)("this"))')
Ry-
Je vais devoir trouver comment éviter cela
alejandro
@alejandro Avez-vous trouvé un moyen d'éviter cela?
Wilt
1
Ma mise en œuvre ajoute simplement:function sbx(s,p) {e = eval; eval = function(t){console.log("GOT GOOD")}; sandboxcode(s,p); eval =e}
YoniXw
2
@YoniXw: J'espère que vous ne l'avez pas utilisé pour quoi que ce soit. Aucune approche comme celle-ci ne fonctionnera jamais. (_=>_).constructor('return this')()
Ry-
1

Un interpréteur Javascript indépendant est plus susceptible de produire un bac à sable robuste qu'une version en cage de l'implémentation du navigateur intégré. Ryan a déjà mentionné js.js , mais un projet plus à jour est JS-Interpreter . La documentation explique comment exposer diverses fonctions à l'interpréteur, mais sa portée est par ailleurs très limitée.

David Fraser
la source
1

Depuis 2019, vm2 semble être la solution la plus populaire et la plus régulièrement mise à jour à ce problème.

Bret Cameron
la source
vm2 ne prend pas en charge le runtime dans le navigateur. Cela devrait cependant fonctionner si vous recherchez du code sandbox dans une application nodejs.
kevin.groat le
0

Avec NISP, vous pourrez faire une évaluation en bac à sable. Bien que l'expression que vous écrivez ne soit pas exactement un JS, vous écrirez plutôt des expressions s. Idéal pour les DSL simples qui ne nécessitent pas une programmation étendue.

Kannan Ramamoorthy
la source
-3

1) Supposons que vous ayez un code à exécuter:

var sCode = "alert(document)";

Maintenant, supposons que vous souhaitiez l'exécuter dans un bac à sable:

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

Ces deux lignes une fois exécutées échoueront, car la fonction "alerte" n'est pas disponible depuis le "sandbox"

2) Et maintenant, vous souhaitez exposer un membre de l'objet window avec votre fonctionnalité:

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

En effet vous pouvez ajouter des citations s'échappant et faire d'autres polissage, mais je suppose que l'idée est claire.

Sergey Ilinsky
la source
7
N'y a-t-il pas une myriade d'autres façons d'atteindre l'objet global? Par exemple, dans une fonction appelée à l'aide de func.apply (null) "this" sera l'objet window.
mbarkhau le
5
Le premier exemple n'échoue pas, c'est un exemple très invalide de sandboxing.
Andy E
1
var sCode = "this.alert ('FAIL')";
Leonard Pauli
-4

D'où vient ce JavaScript utilisateur?

Il n'y a pas grand-chose que vous puissiez faire pour qu'un utilisateur intègre du code dans votre page puis l'appelle depuis son navigateur (voir Greasemonkey, http://www.greasespot.net/ ). C'est juste quelque chose que font les navigateurs.

Cependant, si vous stockez le script dans une base de données, puis le récupérez et l'évaluez (), vous pouvez nettoyer le script avant qu'il ne soit exécuté.

Exemples de code qui supprime toutes les fenêtres. et document. références:

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

Cela tente d'empêcher l'exécution de ce qui suit (non testé):

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

Il existe de nombreuses limitations que vous devrez appliquer au script utilisateur non sécurisé. Malheureusement, il n'y a pas de «conteneur sandbox» disponible pour JavaScript.

Dimitry
la source
2
Si quelqu'un essaie de faire quelque chose de malveillant, une simple regex ne peut tout simplement pas le faire - prenez (function () {this ["loca" + "tion"] = " example.com ";}) () En général si vous ne peut pas faire confiance à vos utilisateurs (ce qui est le cas de tout site sur lequel des personnes arbitraires peuvent ajouter du contenu) le blocage de tous les js est nécessaire.
olliej
J'ai utilisé quelque chose de similaire dans le passé. Ce n'est pas parfait, mais cela vous permet de faire la plupart du temps.
Sugendran
olliej, vous avez raison sur les limites d'une telle technique. Que diriez-vous d'écraser des variables globales telles que <code> var window = null, document = null, this = {}; </code>?
Dimitry
Dimitry Z, l'écrasement de ces variables n'est pas autorisé [dans certains navigateurs]. Vérifiez également ma solution dans la liste des réponses - cela fonctionne.
Sergey Ilinsky
-5

J'ai travaillé sur un bac à sable js simpliste pour permettre aux utilisateurs de créer des applets pour mon site. Bien que je fasse encore face à des défis pour autoriser l'accès au DOM (parentNode ne me laissera tout simplement pas garder les choses en sécurité = /), mon approche consistait simplement à redéfinir l'objet window avec certains de ses membres utiles / inoffensifs, puis à eval () l'utilisateur code avec cette fenêtre redéfinie comme portée par défaut.

Mon code "de base" va comme ça ... (je ne le montre pas entièrement;)

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);
            }
        }
    };
}

Donc, je peux créer une instance d'un Sandbox et utiliser son execute () pour faire fonctionner le code. De plus, toutes les nouvelles variables déclarées dans le code évalué seront finalement liées à la portée execute (), il n'y aura donc pas de conflits de noms ou de problèmes avec le code existant.

Bien que les objets globaux soient toujours accessibles, ceux qui doivent rester inconnus du code sandbox doivent être définis comme des proxys dans l'objet Sandbox :: scope.

J'espère que cela fonctionne pour toi.


la source
8
Cela ne fait rien de bac à sable. Le code évalué peut supprimer des membres et accéder à la portée globale de cette façon, ou saisir une référence à la portée globale en faisant (function () {return this;}) ()
Mike Samuel
-6

Vous pouvez envelopper le code de l'utilisateur dans une fonction qui redéfinit les objets interdits en tant que paramètres - ceux-ci seraient alors undefinedappelés:

(function (alert) {

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

}) ();

Bien sûr, les attaquants intelligents peuvent contourner cela en inspectant le DOM Javascript et en trouvant un objet non surchargé qui contient une référence à la fenêtre.


Une autre idée consiste à scanner le code de l'utilisateur à l'aide d'un outil tel que jslint . Assurez-vous qu'il est défini pour ne pas avoir de variables prédéfinies (ou: uniquement les variables que vous voulez), puis si des globaux sont définis ou accédés, ne laissez pas le script de l'utilisateur être utilisé. Encore une fois, il peut être vulnérable à la marche dans le DOM - les objets que l'utilisateur peut construire à l'aide de littéraux peuvent avoir des références implicites à l'objet de fenêtre auquel il est possible d'accéder pour sortir du bac à sable.

John Millikin
la source
2
Si l'utilisateur saisissait window.alert au lieu de plain alert, il contournerait cette limite.
Quentin
@Dorward: oui, d'où "objets interdits". wrunsby doit décider des objets auxquels l'utilisateur n'est pas autorisé à accéder et les placer dans la liste des paramètres.
John Millikin
Il n'y a qu'un seul objet - fenêtre. Si vous ne bloquez pas l'accès, alors tout est disponible via celui-ci. Si vous le bloquez, alors le script ne peut accéder à rien de ses propriétés (puisque dire alert au lieu de window.alert implique simplement la fenêtre.).
Quentin
@Doward: ce n'est pas le cas où vous bloquez window.alert mais alert fonctionnerait toujours, essayez-le. En effet, window est également l'objet global. Il faudrait bloquer la fenêtre et toute propriété ou méthode de fenêtre à laquelle vous ne voulez pas que le code utilisateur accède.
AnthonyWJones