Je me demande s'il est possible d'exécuter JavaScript dans le navigateur en sandbox pour empêcher l'accès aux fonctionnalités normalement disponibles pour le code JavaScript s'exécutant dans une page HTML.
Par exemple, supposons que je souhaite fournir une API JavaScript aux utilisateurs finaux pour leur permettre de définir les gestionnaires d'événements à exécuter lorsque des "événements intéressants" se produisent, mais je ne souhaite pas que ces utilisateurs accèdent aux propriétés et fonctions de l'objet window
. Suis-je capable de faire ça?
Dans le cas le plus simple, supposons que je souhaite empêcher les utilisateurs d'appeler alert
. Voici quelques approches auxquelles je peux penser:
window.alert
globalement. Je ne pense pas que cette approche soit valide car d’autres codes exécutés dans la page (des éléments dont les utilisateurs ne disposent pas dans leurs gestionnaires d’événements) pourraient vouloir utiliser alert
.Peut-être qu'une solution dans laquelle 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 meilleurs moyens de résoudre ce problème?
Google Caja est un traducteur source à source qui "vous permet de mettre du code HTML et JavaScript tiers non approuvé en ligne dans votre page tout en maintenant la sécurité".
Regardez ADsafe de Douglas Crockford :
ADsafe sécurise l’insertion d’un code client (tel que des publicités scriptées ou des widgets tiers) sur n’importe quelle page Web. ADsafe définit un sous-ensemble de JavaScript suffisamment puissant pour permettre au code invité d'effectuer des interactions utiles, tout en évitant les dommages ou les intrusions malveillants ou accidentels. Le sous-ensemble ADsafe peut être vérifié mécaniquement à l'aide d'outils tels que JSLint, de sorte qu'aucune inspection humaine ne soit nécessaire pour examiner le code invité pour des raisons de sécurité. Le sous-ensemble ADsafe applique également de bonnes pratiques de codage, augmentant la probabilité que le code invité s'exécute correctement.
Vous pouvez voir un exemple d'utilisation d'ADSafe en consultant les fichiers template.html
et template.js
dans le référentiel GitHub du projet .
J'ai créé une bibliothèque de sandbox appelée jsandbox qui utilise des travailleurs Web pour du code évalué en sandbox. Il possède également une méthode de saisie pour donner explicitement des données de code en mode bac à sable qu’il ne pourrait pas obtenir autrement.
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
}
});
EDIT: Bien que je ne sache pas comment échapper à la liste blanche ci-dessous, je lancerais aussi le travailleur depuis un <iframe>
en bac à sable, juste au cas où, et j'appuierais la recommandation js.js de @ gronostaj si l'épuisement de ma mémoire est une préoccupation.
Les travailleurs Web constituent un moyen pratique de créer un autre contexte de script, qui peut ensuite être agité de manière agressive en sandbox sans affecter le parent.
Une implémentation utilisant des promesses:
function safeEval(untrustedCode) {
return new Promise(function (resolve, reject) {
var worker = new Worker('eval.js');
worker.onmessage = function (e) {
worker.terminate();
resolve(e.data);
};
worker.onerror = function (e) {
reject(new Error(e.message));
};
worker.postMessage(untrustedCode);
setTimeout(function () {
worker.terminate();
reject(new Error('The worker timed out.'));
}, 1000);
});
}
eval.js
(vous souhaiterez probablement développer la liste blanche):
(function (global) {
'use strict';
var _postMessage = postMessage;
var _addEventListener = addEventListener;
(function () {
var current = global;
var keepProperties = [
// required
'Object', 'Function', 'Infinity', 'NaN', 'undefined',
// 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);
})();
_addEventListener('message', function (e) {
var f = new Function('', 'return (' + e.data + '\n);');
_postMessage(f());
});
})(this);
Aucun accès à l’interface utilisateur à partir de travailleurs, ne peut être terminé séparément, ne peut pas lier l’interface utilisateur ni le script en cours d’exécution, et est standard.
Je pense que js.js mérite d'être mentionné ici. C'est un interprète JavaScript écrit en JavaScript.
Il est environ 200 fois plus lent que le JS natif, mais sa nature en fait un environnement de bac à sable parfait. Un autre inconvénient est sa taille - presque 600 Ko, ce qui peut être acceptable pour les ordinateurs de bureau dans certains cas, mais pas pour les appareils mobiles.
Comme mentionné dans d'autres réponses, il suffit de jailer le code dans un ifbox tramé (sans l'envoyer au serveur) et de communiquer avec les messages. Je suggérerais de jeter un coup d'oeil à une petite bibliothèque que j'ai créée principalement à cause de la nécessité de fournir une API au code non fiable, comme décrit dans la question: il existe une opportunité d'exporter l'ensemble de fonctions de droite 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:
Tous les éditeurs de navigateurs et la spécification HTML5 s'emploient actuellement à créer une propriété de bac à sable permettant d'autoriser les iframes en bac à sable - mais cela reste limité à la granularité de l'iframe.
En général, aucun degré d'expressions régulières, etc., ne peut effacer en toute sécurité le code JavaScript fourni par l'utilisateur arbitraire, car celui-ci dégénère en problème bloquant: - /
C'est une façon laide, mais peut-être que cela fonctionne pour vous. J'ai pris tous les globals et les redéfinis dans la portée du bac à sable. J'ai également ajouté le mode strict afin qu'ils ne puissent pas obtenir l'objet global à l'aide d'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"}
Une version améliorée du code de bac à sable des travailleurs Web de @ RyanOHara, dans un fichier unique _ (aucun fichier eval.js
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 générer 6
(testé dans Chrome et Firefox).
Depuis 2019, vm2 semble être la solution la plus populaire et la plus mise à jour régulièrement pour résoudre ce problème.
Un interpréteur Javascript indépendant est plus susceptible de générer un sandbox robuste qu'une version en cage de l'implémentation de navigateur intégrée. Ryan a déjà mentionnéjs.js , mais un projet plus récent est JS-Interpreter . Les docs expliquent comment exposer diverses fonctions à l’interprète, mais sa portée est par ailleurs très limitée.