É possível sandbox JavaScript em execução no navegador?

142

Gostaria de saber se é possível sandbox JavaScript em execução no navegador para impedir o acesso a recursos que normalmente estão disponíveis para o código JavaScript em execução em uma página HTML.

Por exemplo, digamos que desejo fornecer uma API JavaScript para os usuários finais, para que eles definam manipuladores de eventos a serem executados quando "eventos interessantes" acontecerem, mas não quero que esses usuários acessem as propriedades e funções do windowobjeto. Eu sou capaz de fazer isso?

No caso mais simples, digamos que eu queira impedir que os usuários liguem alert. Algumas abordagens em que consigo pensar são:

  • Redefina window.alertglobalmente. Eu não acho que isso seria uma abordagem válida, porque outro código em execução na página (ou seja, coisas não criadas pelos usuários em seus manipuladores de eventos) pode querer usar alert.
  • Envie o código do manipulador de eventos ao servidor para processar. Não tenho certeza de que enviar o código ao servidor para processar seja a abordagem correta, porque os manipuladores de eventos precisam ser executados no contexto da página.

Talvez uma solução em que o servidor processe a função definida pelo usuário e gere um retorno de chamada a ser executado no cliente funcionaria? Mesmo que essa abordagem funcione, existem maneiras melhores de resolver esse problema?

Walter Rumsby
fonte

Respostas:

54

O Google Caja é um tradutor fonte a fonte que "permite inserir HTML e JavaScript de terceiros não confiáveis ​​na sua página e ainda assim estar seguro".

Darius Bacon
fonte
5
Um teste rápido mostra que o Caja é incapaz de proteger o navegador de ataques da CPU, como while (1) {}--- simplesmente trava. Da mesma forma a=[]; while (1) { a=[a,a]; }.
1811 David David
5
Sim, a negação de serviço está fora do escopo: code.google.com/p/google-caja/issues/detail?id=1406
Darius Bacon
32

Dê uma olhada no ADsafe de Douglas Crockford :

O ADsafe torna seguro colocar código de convidado (como anúncios ou widgets com script de terceiros) em qualquer página da web. O ADsafe define um subconjunto de JavaScript que é poderoso o suficiente para permitir que o código de convidado execute interações valiosas, ao mesmo tempo em que evita danos ou invasões maliciosas ou acidentais. O subconjunto ADsafe pode ser verificado mecanicamente por ferramentas como JSLint, para que nenhuma inspeção humana seja necessária para revisar o código do hóspede por segurança. O subconjunto ADsafe também aplica boas práticas de codificação, aumentando a probabilidade de o código convidado ser executado corretamente.

Você pode ver um exemplo de como usar o ADsafe observando os arquivos template.htmle template.jsno repositório GitHub do projeto .

Simon Lieschke
fonte
No site deles, não vejo como usar o ADsafe. Não há como fazer o download, não há link para o código, nada. Como você pode experimentar o ADsafe?
BT
2
Além disso, impede qualquer acesso this, o que é totalmente inaceitável. Você não pode escrever um bom javascript sem usar this.
BT
4
@ BT Eu escrevi projetos inteiros sem usar this. Não é difícil evitar o parâmetro com nomes inadequados.
soundly_typed
2
@BT Seria tolo dizer que a conclusão de projetos do mundo real é inaceitável. Mas lamento iniciar esta discussão e devo me retirar; este não é o lugar para discutir essas coisas (desculpe). Estou no twitter se você quiser discutir mais.
soundly_typed
1
@BT (continuarei, pois é relevante para a pergunta) Sempre que você executar código no ambiente de outra pessoa, encontrará regras e restrições. Eu não chamaria isso de inaceitável. Um "pé no saco", talvez. Mas não é inaceitável. Afinal, para todo uso de this, existe uma maneira igual e equivalente thisde fazê-lo (afinal, é apenas um parâmetro).
soundly_typed
24

Criei uma biblioteca de sandbox chamada jsandbox que usa web workers para sandbox o código avaliado. Ele também possui um método de entrada para fornecer explicitamente dados de código em área restrita que de outra forma não seria possível obter.

A seguir, um exemplo da 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
fonte
+1: isso parece muito legal. Quão seguro é executar o código do usuário dessa maneira?
Konstantin Tarkus
1
Muito seguro. Confira a biblioteca atualizada no github .
Eli Gray
1
este projeto ainda é mantido? Vejo que não foi atualizado desde mais de 2 anos ...
Yanick Rochon
Eu gosto disso, exceto que, se você quiser colocar uma caixa de areia, mas ainda permitir que o código acesse jQuery, isso falhará, pois os trabalhadores da Web não permitem a manipulação do DOM.
Rahly
Oi Eli - obrigado por uma ótima lib, você está planejando mantê-la? Eu tenho uma solicitação de alteração para adicionar a funcionalidade de depuração - que, olhando rapidamente para o código, deve ser possível. Por favor, deixe-me saber o que você pensa?
User1514042
8

Eu acho que vale a pena mencionar o js.js aqui. É um intérprete de JavaScript escrito em JavaScript.

É cerca de 200 vezes mais lento que o JS nativo, mas sua natureza o torna um ambiente de sandbox perfeito. Outra desvantagem é seu tamanho - quase 600 kb, o que pode ser aceitável para desktops em alguns casos, mas não para dispositivos móveis.

gronostaj
fonte
7

Conforme mencionado em outras respostas, basta prender o código no iframe em área restrita (sem enviá-lo para o servidor) e se comunicar com as mensagens. Sugiro dar uma olhada em uma pequena biblioteca que criei principalmente devido à necessidade de fornecer alguma API para o código não confiável, assim como descrito na pergunta: há uma oportunidade de exportar o conjunto específico de funções diretamente para a sandbox onde o código não confiável é executado. E também há uma demonstração que executa o código enviado por um usuário em uma sandbox:

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

asvd
fonte
4

Todos os fornecedores de navegadores e a especificação HTML5 estão trabalhando em direção a uma propriedade de área restrita real para permitir iframes em área restrita - mas ainda é limitado à granularidade de iframe.

Em geral, nenhum grau de expressões regulares etc. pode limpar com segurança o usuário arbitrário, desde que o JavaScript se degenere no problema de interrupção: - /

olliej
fonte
2
Você pode explicar como isso se degenera no problema da parada?
hdgarrood
2
A impossibilidade teórica de resolver o problema de parada só se aplica realmente à análise de código estático. As caixas de areia podem fazer coisas como impor limites de tempo para lidar com o problema de interrupção.
Aviendha
4

Uma versão aprimorada do código da caixa de areia dos trabalhadores da Web do @ RyanOHara, em um único arquivo (nenhum eval.jsarquivo extra é necessário).

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

Teste-o:

https://jsfiddle.net/kp0cq6yw/

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

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

Deve sair 6(testado no Chrome e Firefox).

MarcG
fonte
2

De uma maneira feia, mas talvez isso funcione para você, peguei todas as globais e as redefinii no escopo da sandbox e também adicionei o modo estrito para que elas não possam obter o objeto global usando uma função 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
fonte
3
Trivial para windowvoltar disso. sandboxcode('console.log((0,eval)("this"))')
Ry-
Eu vou ter que descobrir como evitar que
alejandro
@alejandro Você encontrou uma maneira de evitar isso?
Wilt
1
Minha implementação apenas acrescenta:function sbx(s,p) {e = eval; eval = function(t){console.log("GOT GOOD")}; sandboxcode(s,p); eval =e}
YoniXw
2
@YoniXw: Espero que você não tenha usado para nada. Nenhuma abordagem como essa funcionará. (_=>_).constructor('return this')()
Ry-
1

É provável que um intérprete Javascript independente produza uma sandbox robusta do que uma versão em gaiola da implementação do navegador interno. Ryan já mencionou js.js , mas um projeto mais atualizado é o JS-Interpreter . Os documentos abordam como expor várias funções ao intérprete, mas seu escopo é muito limitado.

David Fraser
fonte
1

A partir de 2019, o vm2 parecerá a solução mais popular e a mais atualizada regularmente para esse problema.

Bret Cameron
fonte
O vm2 não suporta o tempo de execução no navegador. No entanto, deve funcionar se você estiver procurando o código da área restrita em um aplicativo nodejs.
kevin.groat 13/06
0

Com o NISP, você poderá fazer uma avaliação em área restrita. Embora a expressão que você escreve não seja exatamente um JS, você escreverá expressões-s. Ideal para DSLs simples que não exigem programação extensa.

Kannan Ramamoorthy
fonte
-3

1) Suponha que você tenha um código para executar:

var sCode = "alert(document)";

Agora, suponha que você queira executá-lo em uma sandbox:

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

Essas duas linhas quando executadas falharão, porque a função "alert" não está disponível na "sandbox"

2) E agora você deseja expor um membro do objeto de janela com a sua funcionalidade:

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

Na verdade, você pode adicionar aspas escapando e fazer outro polimento, mas acho que a ideia é clara.

Sergey Ilinsky
fonte
7
Não existem inúmeras outras maneiras de chegar ao objeto global? Por exemplo, dentro de uma função chamada using func.apply (null) "this" será o objeto da janela.
mbarkhau 02/09/11
5
O primeiro exemplo não falha, este é um exemplo muito inválido de sandbox.
Andy E
1
var sCode = "this.alert ('FAIL')";
21413 Leonard Pauli
-4

De onde vem esse JavaScript do usuário?

Não há muito o que fazer sobre um usuário incorporar código à sua página e chamá-lo pelo navegador (consulte Greasemonkey, http://www.greasespot.net/ ). É apenas algo que os navegadores fazem.

No entanto, se você armazenar o script em um banco de dados, recuperá-lo e eval (), poderá limpar o script antes de executá-lo.

Exemplos de código que remove todas as janelas. e documento. referências:

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

Isso tenta impedir que o seguinte seja executado (não testado):

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

Existem muitas limitações que você teria que aplicar ao script de usuário não seguro. Infelizmente, não há 'container sandbox' disponível para JavaScript.

Dimitry
fonte
2
Se alguém está tentando fazer algo malicioso, um regex simples simplesmente não pode fazê-lo - take (function () {this ["loca" + "tion"] = " example.com ";}) () Em geral, se você não pode confiar nos seus usuários (como é o caso de qualquer site no qual pessoas arbitrárias possam adicionar conteúdo), é necessário bloquear todos os js.
12138 olliej
Eu usei algo semelhante no passado. Não é perfeito, mas leva você a maior parte do caminho até lá.
Sugendran 12/10/08
olliej, você está certo sobre as limitações de tal técnica. Que tal substituir variáveis ​​globais como <code> var window = null, document = null, this = {}; </code>?
Dimitry
Dimitry Z, a substituição dessas variáveis ​​não é permitida [em alguns navegadores]. Também verifique minha solução na lista de respostas - funciona.
Sergey Ilinsky
-5

Eu tenho trabalhado em uma sandbox js simplista para permitir que os usuários criem applets para o meu site. Embora eu ainda enfrente alguns desafios ao permitir o acesso ao DOM (parentNode simplesmente não me deixa manter as coisas seguras = /), minha abordagem foi apenas redefinir o objeto de janela com alguns de seus membros úteis / inofensivos e depois avaliar () o usuário código com essa janela redefinida como o escopo padrão.

Meu código "principal" é assim ... (não estou mostrando isso totalmente;)

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

Então, eu posso instalar uma Sandbox e usar sua execute () para executar o código. Além disso, todas as novas variáveis ​​declaradas no código eval'd acabam vinculadas ao escopo execute (), portanto, não haverá nomes conflitantes nem interferindo no código existente.

Embora os objetos globais ainda sejam acessíveis, aqueles que devem permanecer desconhecidos no código da área restrita devem ser definidos como proxies no objeto Sandbox :: scope.

Espero que funcione para voce.


fonte
8
Isso não sandbox nada. O código evaled pode excluir membros e chegar ao escopo global dessa forma, ou agarrar uma referência a ghe escopo global fazendo (function () {return este;}) ()
Mike Samuel
-6

Você pode agrupar o código do usuário em uma função que redefine objetos proibidos como parâmetros - estes seriam undefinedchamados quando:

(function (alert) {

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

}) ();

Obviamente, invasores inteligentes podem contornar isso inspecionando o DOM Javascript e localizando um objeto não substituído que contém uma referência à janela.


Outra idéia é escanear o código do usuário usando uma ferramenta como jslint . Certifique-se de que ele esteja definido para não ter variáveis ​​predefinidas (ou: apenas variáveis ​​desejadas) e, se quaisquer globais estiverem configurados ou acessados, não permita que o script do usuário seja usado. Novamente, pode ser vulnerável a andar pelo DOM - objetos que o usuário pode construir usando literais podem ter referências implícitas ao objeto de janela que pode ser acessado para escapar da sandbox.

John Millikin
fonte
2
Se o usuário inserisse window.alert em vez de alerta simples, eles ignorariam esse limite.
Quentin
@ Dorward: sim, daí "objetos proibidos". O wrunsby deve decidir quais objetos o usuário não tem permissão para acessar e colocá-los na lista de parâmetros.
John Millikin
Existe apenas um objeto - janela. Se você não bloquear o acesso, tudo estará disponível através dele. Se você o bloquear, o script não poderá acessar nada de suas propriedades (já que dizer alerta em vez de window.alert implica apenas a janela).
Quentin
@ Doward: esse não é o caso em que você bloquearia o window.alert, mas o alerta ainda funcionaria, tente. Isso ocorre porque a janela também é o objeto global. É necessário bloquear a janela e qualquer propriedade ou método da janela que você não queira que o código do usuário acesse.
AnthonyWJones