Czy w przeglądarce można uruchomić JavaScript w piaskownicy?

142

Zastanawiam się, czy możliwe jest piaskownica JavaScript działająca w przeglądarce, aby uniemożliwić dostęp do funkcji, które są normalnie dostępne dla kodu JavaScript działającego na stronie HTML.

Na przykład, powiedzmy, że chcę zapewnić użytkownikom końcowym JavaScript API, aby umożliwić im zdefiniowanie programów obsługi zdarzeń, które mają być uruchamiane, gdy wystąpią „interesujące zdarzenia”, ale nie chcę, aby ci użytkownicy mieli dostęp do właściwości i funkcji windowobiektu. Czy jestem w stanie to zrobić?

W najprostszym przypadku powiedzmy, że chcę uniemożliwić użytkownikom dzwonienie alert. Oto kilka podejść, które przychodzą mi do głowy:

  • window.alertGlobalnie przedefiniuj . Nie sądzę, żeby to było właściwe podejście, ponieważ inny kod działający na stronie (tj. Rzeczy, które nie zostały utworzone przez użytkowników w ich programach obsługi zdarzeń) mogą chcieć użyć alert.
  • Wyślij kod obsługi zdarzeń do serwera w celu przetworzenia. Nie jestem pewien, czy wysłanie kodu do serwera w celu przetworzenia jest właściwym podejściem, ponieważ programy obsługi zdarzeń muszą działać w kontekście strony.

Być może rozwiązanie, w którym serwer przetwarza funkcję zdefiniowaną przez użytkownika, a następnie generuje wywołanie zwrotne do wykonania na kliencie, zadziała? Nawet jeśli to podejście działa, czy istnieją lepsze sposoby rozwiązania tego problemu?

Walter Rumsby
źródło

Odpowiedzi:

54

Google Caja jest tłumaczem typu „source-to-source”, który „umożliwia umieszczanie na stronie niezaufanego kodu HTML i JavaScript innej firmy, a jednocześnie zapewnia bezpieczeństwo”.

Darius Bacon
źródło
5
Szybki test pokazuje, że Caja nie jest w stanie chronić przeglądarki przed atakami procesora, takimi jak while (1) {}--- po prostu się zawiesza. Podobnie a=[]; while (1) { a=[a,a]; }.
David Given
5
Tak, odmowa usługi jest poza zakresem: code.google.com/p/google-caja/issues/detail?id=1406
Darius Bacon
32

Spójrz na ADsafe Douglasa Crockforda :

ADsafe umożliwia bezpieczne umieszczanie kodu gościa (takiego jak reklamy skryptowe lub widżety stron trzecich) na dowolnej stronie internetowej. ADsafe definiuje podzbiór JavaScript, który jest wystarczająco potężny, aby umożliwić kodowi gościa wykonywanie wartościowych interakcji, jednocześnie zapobiegając złośliwym lub przypadkowym uszkodzeniom lub włamaniom. Podzbiór ADsafe można zweryfikować mechanicznie za pomocą narzędzi takich jak JSLint, dzięki czemu nie jest konieczna inspekcja przez człowieka w celu sprawdzenia kodu gościa pod kątem bezpieczeństwa. Podzbiór ADsafe wymusza również dobre praktyki kodowania, zwiększając prawdopodobieństwo, że kod gościa będzie działał poprawnie.

Możesz zobaczyć przykład korzystania z ADsafe, przeglądając pliki template.htmli template.jsw repozytorium GitHub projektu .

Simon Lieschke
źródło
Na ich stronie nie widzę możliwości korzystania z ADsafe. Nie ma sposobu, aby go pobrać, nie ma linku do kodu, nic. Jak możesz wypróbować ADsafe?
BT,
2
Ponadto zapobiega żadnego dostępu do this, który jest całkowicie nie do przyjęcia. Nie możesz napisać dobrego javascript bez użycia this.
BT,
4
@BT Napisałem całe projekty bez użycia this. Nie jest trudno uniknąć źle nazwanego parametru.
soundly_typed
2
@BT Głupio byłoby powiedzieć, że kończenie projektów w świecie rzeczywistym jest niedopuszczalne. Ale żałuję rozpoczęcia tej dyskusji i muszę się wycofać; to nie jest miejsce na omawianie takich rzeczy (przepraszam). Jestem na Twitterze, jeśli chcesz dalej dyskutować.
soundly_typed
1
@BT (będę kontynuował, ponieważ jest to istotne dla pytania) Za każdym razem, gdy uruchamiasz kod w czyimś środowisku, napotkasz reguły i ograniczenia. Nie nazwałbym tego niedopuszczalnym. Może "wrzód na tyłku". Ale nie niedopuszczalne. W końcu dla każdego użycia thisistnieje równy, równoważny thissposób, aby to zrobić (w końcu jest to tylko parametr).
soundly_typed
24

Stworzyłem bibliotekę piaskownicy o nazwie jsandbox, która wykorzystuje pracowników sieci do oceny kodu w piaskownicy. Posiada również metodę wejściową do jawnego podawania danych kodu w trybie piaskownicy, których w innym przypadku nie byłby w stanie uzyskać.

Poniżej znajduje się przykład interfejsu 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 Grey
źródło
+1: To wygląda naprawdę fajnie. Jak bezpieczne jest wykonanie kodu użytkownika w ten sposób?
Konstantin Tarkus
1
Bardzo bezpieczny. Sprawdź zaktualizowaną bibliotekę na github .
Eli Gray
1
czy ten projekt jest nadal utrzymywany? Widzę, że nie był aktualizowany od ponad 2 lat ...
Yanick Rochon
Podoba mi się to, z wyjątkiem tego, że jeśli chcesz korzystać z piaskownicy, ale nadal zezwalasz kodowi na dostęp do jQuery, to się nie powiedzie, ponieważ pracownicy sieciowi nie pozwolą na manipulację DOM.
Rahly
Cześć Eli - dzięki za wspaniałą libankę, czy planujesz ją utrzymać? Mam prośbę o zmianę dotyczącą dodania funkcji debugowania - co powinno być możliwe po szybkim przejrzeniu kodu. Proszę daj mi znać co myślisz?
user1514042
8

Myślę, że warto tu wspomnieć o js.js. To interpreter JavaScript napisany w JavaScript.

Jest około 200 razy wolniejszy niż natywny JS, ale jego natura sprawia, że ​​jest idealnym środowiskiem piaskownicy. Inną wadą jest jego rozmiar - prawie 600 kb, co w niektórych przypadkach może być akceptowalne dla komputerów stacjonarnych, ale nie dla urządzeń mobilnych.

gronostaj
źródło
7

Jak wspomniano w innych odpowiedziach, wystarczy uwięzić kod w piaskownicy iframe (bez wysyłania go na serwer) i komunikować się z wiadomościami. Proponuję przyjrzeć się małej bibliotece, którą stworzyłem głównie ze względu na potrzebę udostępnienia jakiegoś API do niezaufanego kodu, tak jak opisano w pytaniu: istnieje możliwość wyeksportowania określonego zestawu funkcji bezpośrednio do piaskownicy, gdzie niezaufany kod zostanie uruchomiony. Jest też demo, które wykonuje kod przesłany przez użytkownika w piaskownicy:

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

asvd
źródło
4

Wszyscy dostawcy przeglądarek i specyfikacja HTML5 pracują nad rzeczywistą właściwością piaskownicy, aby umożliwić korzystanie z elementów iframe w piaskownicy - ale nadal jest to ograniczone do szczegółowości elementów iframe.

Ogólnie rzecz biorąc, żaden poziom wyrażeń regularnych itp. Nie może bezpiecznie oczyścić dowolnego JavaScript dostarczonego przez użytkownika, ponieważ degeneruje się on do problemu zatrzymania: - /

olliej
źródło
2
Czy możesz wyjaśnić, jak to degeneruje się do problemu zatrzymania?
hdgarrood
2
Teoretyczna niemożność rozwiązania problemu zatrzymania dotyczy tak naprawdę tylko statycznej analizy kodu. Piaskownice mogą na przykład wymuszać ograniczenia czasowe, aby poradzić sobie z problemem zatrzymania.
Aviendha
4

Ulepszona wersja kodu piaskownicy dla pracowników sieci @ RyanOHara, w jednym pliku (nie eval.jsjest potrzebny żaden dodatkowy plik).

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

Sprawdź to:

https://jsfiddle.net/kp0cq6yw/

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

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

Powinien się wyświetlać 6(testowane w Chrome i Firefox).

MarcG
źródło
2

Brzydki sposób, ale może to działa dla ciebie, wziąłem wszystkie globale i przedefiniowałem je w zakresie piaskownicy, a także dodałem tryb ścisły, aby nie mogli uzyskać obiektu globalnego za pomocą funkcji anonimowej.

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
źródło
3
Trywialne, żeby windowz tego wrócić. sandboxcode('console.log((0,eval)("this"))')
Ry-
Muszę wymyślić, jak temu zapobiec
alejandro
@alejandro Czy znalazłeś sposób, aby temu zapobiec?
Wilt
1
Moja realizacja po prostu dodaje:function sbx(s,p) {e = eval; eval = function(t){console.log("GOT GOOD")}; sandboxcode(s,p); eval =e}
YoniXw
2
@YoniXw: Mam nadzieję, że nie wykorzystałeś go do niczego. Żadne takie podejście nigdy nie zadziała. (_=>_).constructor('return this')()
Ry-
1

Niezależny interpreter JavaScript z większym prawdopodobieństwem zapewni solidną piaskownicę niż wersję implementacji wbudowanej przeglądarki w klatce. Ryan już wspomniał o js.js , ale bardziej aktualnym projektem jest JS-Interpreter . W docs pokrycie jak wystawiać różne funkcje do tłumacza, ale jego zakres jest inaczej bardzo ograniczony.

David Fraser
źródło
1

Od 2019 roku vm2 wygląda na najpopularniejsze i najczęściej aktualizowane rozwiązanie tego problemu.

Bret Cameron
źródło
vm2 nie obsługuje środowiska uruchomieniowego w przeglądarce. Powinno jednak działać, jeśli szukasz kodu piaskownicy w aplikacji nodejs.
kevin.groat
0

Dzięki NISP będziesz mógł przeprowadzić ocenę w piaskownicy. Chociaż napisane wyrażenie nie jest dokładnie JS, zamiast tego napiszesz s-wyrażenia. Idealny do prostych DSL, które nie wymagają rozbudowanego programowania.

Kannan Ramamoorthy
źródło
-3

1) Załóżmy, że masz kod do wykonania:

var sCode = "alert(document)";

Teraz przypuśćmy, że chcesz wykonać to w piaskownicy:

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

Te dwa wiersze po wykonaniu zakończą się niepowodzeniem, ponieważ funkcja „alert” nie jest dostępna z poziomu „piaskownicy”

2) A teraz chcesz udostępnić element członkowski obiektu window ze swoją funkcjonalnością:

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

Rzeczywiście, możesz dodać cudzysłowy i zrobić inne polerowanie, ale myślę, że pomysł jest jasny.

Sergey Ilinsky
źródło
7
Czy nie ma niezliczonych innych sposobów dotarcia do obiektu globalnego? Na przykład w funkcji o nazwie using func.apply (null) „this” będzie obiektem okna.
mbarkhau
5
Pierwszy przykład nie zawodzi, jest to bardzo nieprawidłowy przykład piaskownicy.
Andy E
1
var sCode = "this.alert ('FAIL')";
Leonard Pauli
-4

Skąd pochodzi JavaScript użytkownika?

Niewiele można zrobić, aby użytkownik osadził kod na swojej stronie, a następnie wywołał go z przeglądarki (patrz Greasemonkey, http://www.greasespot.net/ ). Robią to tylko przeglądarki.

Jeśli jednak przechowujesz skrypt w bazie danych, a następnie pobierzesz go i przeprowadzisz eval (), możesz wyczyścić skrypt przed jego uruchomieniem.

Przykłady kodu, który usuwa wszystkie okna. i dokument. Bibliografia:

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

Ma to na celu zapobieżenie wykonaniu następujących czynności (nie przetestowano):

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

Istnieje wiele ograniczeń, które musiałbyś zastosować do niebezpiecznego skryptu użytkownika. Niestety dla JavaScript nie jest dostępny żaden „kontener piaskownicy”.

Dimitry
źródło
2
Jeśli ktoś próbuje zrobić coś złośliwego, proste wyrażenie regularne po prostu nie może tego zrobić - weź (function () {this ["loca" + "ation "] =" example.com ";}) () Ogólnie, jeśli nie możesz ufać swoim użytkownikom (tak jest w przypadku każdej witryny, w której dowolne osoby mogą dodawać zawartość), blokowanie wszystkich plików js jest konieczne.
olliej
W przeszłości użyłem czegoś podobnego. Nie jest idealny, ale prowadzi tam większość drogi.
Sugendran
olliej, masz rację co do ograniczeń takiej techniki. Co powiesz na nadpisywanie zmiennych globalnych, takich jak <code> var window = null, document = null, this = {}; </code>?
Dimitry,
Dimitry Z, nadpisywanie tych zmiennych jest niedozwolone [w niektórych przeglądarkach]. Sprawdź też moje rozwiązanie na liście odpowiedzi - działa.
Sergey Ilinsky
-5

Pracowałem nad uproszczoną piaskownicą js, aby umożliwić użytkownikom tworzenie apletów dla mojej witryny. Chociaż nadal napotykam pewne wyzwania związane z zezwoleniem na dostęp do DOM (parentNode po prostu nie pozwala mi zachować bezpieczeństwa = /), moim podejściem było po prostu przedefiniowanie obiektu okna z niektórymi jego użytecznymi / nieszkodliwymi składnikami, a następnie eval () użytkownika kod z tym ponownie zdefiniowanym oknem jako domyślnym zakresem.

Mój „podstawowy” kod wygląda tak ... (nie pokazuję go całkowicie;)

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

Mogę więc utworzyć instancję Sandbox i użyć jej funkcji execute (), aby uruchomić kod. Ponadto wszystkie nowe zadeklarowane zmienne w kodzie eval'd ostatecznie będą powiązane z zakresem execute (), więc nie będzie kolidujących nazw ani bałaganu z istniejącym kodem.

Chociaż obiekty globalne będą nadal dostępne, te, które powinny pozostać nieznane w kodzie piaskownicy, muszą być zdefiniowane jako proxy w obiekcie Sandbox :: scope.

Mam nadzieję, że to działa dla Ciebie.


źródło
8
To nic nie piaskownicy. Oceniany kod może usunąć członków i dostać się w ten sposób do zasięgu globalnego lub pobrać odniesienie do zakresu globalnego, wykonując (function () {return this;}) ()
Mike Samuel
-6

Możesz opakować kod użytkownika w funkcję, która na nowo definiuje zabronione obiekty jako parametry - byłyby wtedy undefinedwywołane:

(function (alert) {

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

}) ();

Oczywiście sprytni napastnicy mogą obejść ten problem, sprawdzając Javascript DOM i znajdując niezamknięty obiekt, który zawiera odniesienie do okna.


Innym pomysłem jest skanowanie kodu użytkownika za pomocą narzędzia takiego jak jslint . Upewnij się, że nie ma predefiniowanych zmiennych (lub: tylko zmienne, które chcesz), a następnie, jeśli są ustawione lub dostępne jakiekolwiek zmienne globalne, nie pozwól na użycie skryptu użytkownika. Ponownie, może być podatny na chodzenie po DOM - obiekty, które użytkownik może skonstruować za pomocą literałów, mogą mieć niejawne odniesienia do obiektu okna, do którego można uzyskać dostęp, aby uciec z piaskownicy.

John Millikin
źródło
2
Jeśli użytkownik wprowadziłby window.alert zamiast zwykłego alertu, omijałby ten limit.
Quentin
@Dorward: tak, stąd „zakazane obiekty”. wrunsby powinien zdecydować, do jakich obiektów użytkownik nie ma dostępu, i umieścić je na liście parametrów.
John Millikin,
Jest tylko jeden obiekt - okno. Jeśli nie zablokujesz dostępu do niego, wszystko jest przez niego dostępne. Jeśli go zablokujesz, skrypt nie będzie mógł uzyskać dostępu do żadnych właściwości (ponieważ powiedzenie alertu zamiast window.alert oznacza tylko okno.).
Quentin
@Doward: tak nie jest, gdybyś zablokował window.alert, ale alert nadal działałby, spróbuj. Dzieje się tak, ponieważ okno jest również obiektem globalnym. Należałoby zablokować okno i dowolną właściwość lub metodę okna, do których nie chcesz, aby kod użytkownika miał dostęp.
AnthonyWJones