Código mais seguro com Shadow Realms no JavaScript
O JavaScript sempre foi e continua sendo uma linguagem bastante dinâmica, por conta disso estou iniciando uma nova série de artigos onde vou falar cada vez mais sobre as novas propostas e as possíveis funcionalidades desse ecossistema incrível!
A escolhida de hoje é uma proposta que está sendo levada adiante por ninguém mais ninguém menos que nosso grande representante no TC39, Leo Balter junto com mais um monte de pessoas incríveis e ela se chama ShadowRealm.
Um pouco de contexto
Quando estamos falando de web, sempre temos que ter em mente que ela é como se fosse uma folha em branco, ou seja, temos muito espaço pra criar e experimentar com quase tudo.
Uma das coisas mais comuns que temos por aí, são aplicações extensíveis, por exemplo, daquelas que você pode criar seu próprio código para estender a funcionalidade já existente, como plugins.
O grande problema desse tipo de aplicação é que temos que executar o código da própria aplicação, chamado de core, junto com o código do usuário ou do plugin. E, no JavaScript, isso compartilha do mesmo objeto global chamado Window, ou seja, virtualmente, todos os códigos estão rodando no mesmo lugar, e não há nada que impede de o plugin acessar informações sensíveis do usuário, por exemplo.
Por outro lado, esse tipo de comportamento é o que torna possível aplicações como o jQuery, porque estar em um ambiente global, permite que criemos objetos compartilhados e possamos também estender funcionalidades padrões, como o $
que o jQuery injetava no objeto global, ou então modificar o método Array.prototype.pop
estão entre as coisas mais comuns que essas antigas libs faziam.
Isso parece um problema de segurança, não?
Entra o ShadowRealm
Realm, em inglês, é a palavra que define um "reino". Hoje em dia não temos muitos reinos por aí, mas imagina que isso sejam países. E, assim como países tem seus próprios problemas, fronteiras, leis e etc, os realms também tem seu próprio "mundo".
Um ShadowRealm
cria um outro contexto de execução, ou seja, um novo local dentro do mesmo código com seu próprio objeto global e seus próprio objetos internos (como seu próprio Array.prototype.pop
), isso significa que podemos executar códigos dentro desse local sem interferir com o código externo. É como se isolássemos o código em um local separado.
Essa funcionalidade sempre vai executar o código de maneira síncrona, o que permite uma virtualização de todas as APIs do DOM que executam dentro dele:
const shadowRealm = new ShadowRealm()
shadowRealm.evaluate('globalThis.x. = "Um novo lugar"')
globalThis.x = "root"
const shadowRealmEval = shadowRealm.evaluate('globalThis.x')
shadowRealmEval // Um novo lugar
x // root
Nesse código estamos criando uma propriedade x
tanto no ShadowRealm quanto fora dele, com dois valores diferentes, e podemos ver que esses valores são de fato isolados um do outro.
É importante notar que uma instancia do ShadowRealm só pode trafegar dados primitivos: String, Number, BigInt, Symbol, Boolean, undefined e null. Qualquer outro tipo de dado – como objetos – não são permitidos. E isso é bastante importante para manter a coesão e a separação dos ambientes, já que objetos carregam as referências do local onde foram criados, ou seja, passar um objeto para dentro do ShadowRealm poderia vazar um escopo superior para um escopo interno.
Porém, um ShadowRealm pode compartilhar funções e valores retornados por essas funções, e isso permite uma comunicação bastante robusta entre as duas partes:
const sr = new ShadowRealm()
const srFn = sr.evaluate('(x) => globalThis.value = x')
srFn(42)
globalThis.value // undefined
sr.evaluate('globalThis.value') // 42
Existem outros exemplos bem legais do uso do ShadowRealms de forma mais básica no blog post original dos autores que é bem legal!
Injeção externa de valores
Os ShadowRealms permitem que a gente execute funções e códigos arbitrários com o comando evaluate
, que recebe uma string como parâmetro e funciona como uma versão um pouco mais segura do eval
, mas ele ainda está sujeito a Content Security Policies (CSP) no browser, então uma CSP de unsafe-eval
iria desabilitar essa funcionalidade.
Para injetar um código direto dentro do ShadowRealm, ele também possui o método importValue
, que basicamente funciona como um import()
dentro do código para carregar um módulo e capturar um valor exportado.
const sr = new ShadowRealm()
const specifier = './spec-file.js'
const name = 'sum'
const shadowSum = await sr.importValue(specifier, name)
shadowSum(1) // Executa a operação e captura o resultado
Basicamente, await sr.importValue
é uma promise que será resolvida com o valor name
importado de specifier
, então se o specifier for:
//spec-file.js
const sum = (a,b) => a+b
export { sum }
Teremos a função sum
no shadowSum
.
Além disso, é importante notar que os valores importados pelo importValue
são sempre relativos ao ShadowRealm no qual eles estão inseridos, então, tirando mais um exemplo do blog post dos autores, imagina que ao invés de ser uma simples função de soma, o spec-file.js
modificasse o globalThis
:
globalThis.total = 0;
export function sum(n) {
return globalThis.total += n;
}
export function getTotal() {
return globalThis.total;
}
Se tivéssemos um código local executando a função dentro de um ShadowRealm, o globalThis
seria o objeto dentro do ShadowRealm, e não o globalThis
do escopo global fora do ShadowRealm:
const sr = new ShadowRealm();
const specifier = './spec-file.js';
const [ shadowSum, shadowGetTotal ] = await Promise.all([
sr.importValue(specifier, 'sum'),
sr.importValue(specifier, 'getTotal')
]);
globalThis.total = 0; // Escopo local fora do SR
shadowSum(10); // 10
shadowSum(20); // 30
shadowSum(30); // 60
globalThis.total; // 0
shadowGetTotal(); // 60
// Agora estamos importando no escopo local
const { sum, getTotal } = await import(specifier);
sum(42); // 42
globalThis.total; // 42
// O valor interno é preservado
shadowGetTotal(); // 60
Implicações dos ShadowRealms
Enquanto essa API ainda é uma proposta, ela já melhora muito a forma como trabalhamos com sandboxed code – quando executamos códigos em ambientes separados – hoje isso é feito com iFrames, que é a única forma relativamente boa de se separar dois contextos dentro do mesmo local.
Porém, com os SRs, é possível que tenhamos uma capacidade ainda maior de executar não só funções simples, mas é possível que possamos executar códigos de testes em ambientes isolados separando completamente as responsabilidades, dessa forma, testes unitários, de integração ou qualquer outra coisa, não vão interferir um com o outro.
Indo ainda mais além, seria possível ainda rodar aplicações inteiras dentro de outras aplicações desde que essas aplicações sejam otimizadas e preparadas para trabalhar com modelos de mensagens, enfim, as possibilidades são muitas e são super animadoras!
Conclusão
Se você quiser ficar por dentro dessa e de muitas outras novidades tanto do JS quanto do Node e tecnologia no geral com textos curados e na medida certa, não se esqueça de assinar minha newsletter para receber o melhor conteúdo todos os meses!