Prática recomendada para incorporar JSON arbitrário no DOM?

110

Estou pensando em incorporar JSON arbitrário no DOM assim:

<script type="application/json" id="stuff">
    {
        "unicorns": "awesome",
        "abc": [1, 2, 3]
    }
</script>

Isso é semelhante à maneira como alguém pode armazenar um modelo HTML arbitrário no DOM para uso posterior com um mecanismo de modelo JavaScript. Nesse caso, poderíamos posteriormente recuperar o JSON e analisá-lo com:

var stuff = JSON.parse(document.getElementById('stuff').innerHTML);

Isso funciona , mas é a melhor maneira? Isso viola alguma prática recomendada ou padrão?

Observação: não estou procurando alternativas para armazenar JSON no DOM, já decidi que essa é a melhor solução para o problema específico que estou tendo. Só estou procurando a melhor maneira de fazer isso.

Ben Lee
fonte
1
por que você não tem varem javascript?
Krizz de
@Krizz, ele precisa ser parte de um documento estático que posteriormente é processado por uma cadeia complexa de javascript encapsulado. Armazená-lo no DOM é o que eu quero fazer.
Ben Lee
@Krizz Eu estava diante de um problema semelhante. Eu queria colocar dados em um site diferente para cada usuário sem fazer uma solicitação AJAX. Então, eu embuti algum PHP em um contêiner e fiz algo semelhante ao que você fez acima para obter os dados em javascript.
Patrick Lorio
2
Acho que seu método original é o melhor, na verdade. É 100% válido em HTML5, é expressivo, não cria elementos "falsos" que você apenas remove ou esconde com CSS; e não requer nenhuma codificação de caracteres. Qual é a desvantagem?
Jamie Treworgy de
22
Se você tiver uma string com o valor </script><script>alert()</script><script>dentro de seu objeto JSON, terá surpresas. Isso não é seguro, a menos que você primeiro higienize os dados.
silviot de

Respostas:

77

Acho que seu método original é o melhor. A especificação HTML5 ainda aborda esse uso:

"Quando usado para incluir blocos de dados (ao contrário de scripts), os dados devem ser incorporados inline, o formato dos dados deve ser fornecido usando o atributo type, o atributo src não deve ser especificado e o conteúdo do elemento script deve em conformidade com os requisitos definidos para o formato utilizado. "

Leia aqui: http://dev.w3.org/html5/spec/Overview.html#the-script-element

Você fez exatamente isso. O que não é amar? Nenhuma codificação de caracteres necessária com dados de atributo. Você pode formatá-lo se quiser. É expressivo e o uso pretendido é claro. Não parece um hack (por exemplo, como usar CSS para ocultar seu elemento "transportador"). É perfeitamente válido.

Jamie Treworgy
fonte
3
Obrigado. A citação da especificação me convenceu.
Ben Lee de
17
É perfeitamente válido apenas se você verificar e limpar o objeto JSON primeiro: você não pode simplesmente incorporar dados de origem do usuário. Veja meu comentário sobre a questão.
silviot de
1
extra se perguntando: qual é o bom lugar para colocá-lo? cabeça ou corpo, parte superior ou inferior?
Challet de
1
Infelizmente, parece que a política CSP pode / irá interromper todas as scripttags.
Larry K de
2
Como você se protege com eficácia contra a incorporação de JSON que contém </script> e, portanto, permite a injeção de HTML? Existe algo sólido / fácil ou é melhor usar atributos de dados?
jonasfj
23

Como orientação geral, eu tentaria usar atributos de dados HTML5 . Não há nada que o impeça de inserir um JSON válido. por exemplo:

<div id="mydiv" data-unicorns='{"unicorns":"awesome", "abc":[1,2,3]}' class="hidden"></div>

Se você estiver usando jQuery, recuperá-lo é tão fácil quanto:

var stuff = JSON.parse($('#mydiv').attr('data-unicorns'));
Horatio Alderaan
fonte
1
Faz sentido. Porém, observe que, com aspas simples para o nome da chave, JSON.parsenão funcionará (pelo menos o JSON.parse nativo do Google Chrome não funcionará). A especificação JSON requer aspas duplas. Mas isso é fácil de corrigir usando entidades como ...&lt;unicorns&gt;:....
Ben Lee de
4
Uma pergunta, porém: há algum limite para o comprimento dos atributos no HTML 5?
Ben Lee de
Sim, isso funcionaria. Você também pode alternar para que seu HTML use aspas simples e os dados JSON usem double.
Horatio Alderaan
1
Ok, encontrei a resposta para minha pergunta: stackoverflow.com/questions/1496096/… - isso é suficiente para meus propósitos.
Ben Lee de
2
Isso não funcionaria para uma única string, por exemplo, "I am valid JSON"usando aspas duplas para a tag, ou aspas simples com aspas simples na string, por exemplo data-unicorns='"My JSON's string"', as aspas simples não escapam da codificação como JSON.
Robbie Averill
13

Este método de incorporação de json em uma tag de script tem um possível problema de segurança. Assumindo que os dados json originaram-se da entrada do usuário, é possível criar um membro de dados que, com efeito, sairá da tag do script e permitirá a injeção direta no dom. Veja aqui:

http://jsfiddle.net/YmhZv/1/

Aqui está a injeção

<script type="application/json" id="stuff">
{
    "unicorns": "awesome",
    "abc": [1, 2, 3],
    "badentry": "blah </script><div id='baddiv'>I should not exist.</div><script type="application/json" id='stuff'> ",
}
</script>

Simplesmente não há como escapar / codificar.

MadCoder
fonte
7
Isso é verdade, mas não é realmente uma falha de segurança do método. Se você estiver colocando algo que se originou na entrada do usuário em suas páginas, deverá ser cuidadoso para escapar disso. Este método ainda é válido, desde que você tome as medidas de precaução normais em relação à entrada do usuário.
Ben Lee
JSON não faz parte do HTML, o analisador HTML simplesmente continua. É o mesmo que quando o JSON faria parte de um parágrafo de texto ou elemento div. Escape do HTML do conteúdo do seu programa. Além disso, você também pode escapar de barras. Embora JSON não exija isso, ele tolera barras desnecessárias. Que pode ser usado com o propósito de torná-lo seguro para incorporação. O json_encode do PHP faz isso por padrão.
Timo Tijhof
7

Consulte a regra nº 3.1 na folha de dicas de prevenção de XSS do OWASP.

Digamos que você queira incluir este JSON em HTML:

{
    "html": "<script>alert(\"XSS!\");</script>"
}

Crie um oculto <div>em HTML. Em seguida, escape de seu JSON codificando entidades não seguras (por exemplo, &, <,>, ", 'e, /) e coloque-o dentro do elemento.

<div id="init_data" style="display:none">
        {&#34;html&#34;:&#34;&lt;script&gt;alert(\&#34;XSS!\&#34;);&lt;/script&gt;&#34;}
</div>

Agora você pode acessá-lo lendo o textContentdo elemento usando JavaScript e analisando-o:

var text = document.querySelector('#init_data').textContent;
var json = JSON.parse(text);
console.log(json); // {html: "<script>alert("XSS!");</script>"}
Mateus
fonte
Eu acredito que esta é a melhor e mais segura resposta. Observe que muitos caracteres JSON comuns têm escape e alguns caracteres têm escape duplo, como as aspas internas no objeto {name: 'Dwayne "The Rock" Johnson'}. Mas provavelmente ainda é melhor usar essa abordagem, já que sua biblioteca de framework / modelos provavelmente já inclui uma maneira segura de fazer a codificação HTML. Uma alternativa seria usar base64, que é HTML e seguro para ser colocado dentro de uma string JS. É fácil codificar / decodificar em JS usando btoa () / atob () e provavelmente é fácil para você fazer no lado do servidor.
sstur
Um método ainda mais seguro seria usar o <data>elemento semanticamente correto e incluir os dados JSON no valueatributo. Em seguida, você só precisa escapar das aspas com &quotse usar aspas duplas para incluir os dados ou &#39;se usar aspas simples (o que provavelmente é melhor).
Rúnar Berg
5

Eu sugiro colocar JSON em um script embutido com um retorno de chamada de função (tipo de JSONP ):

<script>
someCallback({
    "unicorns": "awesome",
    "abc": [1, 2, 3]
});
</script>

Se o script em execução for carregado após o documento, você poderá armazená-lo em algum lugar, possivelmente com um argumento identificador adicional: someCallback("stuff", { ... });

cópia de
fonte
@BenLee deve funcionar muito bem, com a única desvantagem de ter que definir a função de callback. A outra solução sugerida quebra em caracteres HTML especiais (por exemplo, &) e aspas, se você os tiver em seu JSON.
cópia de
Isso é melhor porque você não precisa de uma consulta dom para encontrar os dados
Jaseem
@copy Esta solução ainda precisa de escape (apenas um tipo diferente), veja a resposta de MadCoder. Apenas deixando aqui para completude.
pvgoran
2

Minha recomendação seria manter os dados JSON em .jsonarquivos externos e, em seguida, recuperar esses arquivos via Ajax. Você não coloca código CSS e JavaScript na página da web (inline), então por que faria isso com JSON?

Šime Vidas
fonte
12
Você não coloca CSS e Javascript embutidos em uma página da web porque geralmente são compartilhados entre outras páginas. Se os dados em questão forem gerados pelo servidor explicitamente para esse contexto, incorporá-los é muito mais eficiente do que iniciar outra solicitação de algo que não pode ser armazenado em cache.
Jamie Treworgy
É porque estou fazendo atualizações em um sistema legado que foi mal projetado e, em vez de reprojetar todo o sistema, preciso consertar apenas uma parte. Armazenar JSON no DOM é a melhor maneira de corrigir essa parte. Além disso, concordo com o que @jamietre disse.
Ben Lee de
@jamietre Observe que o OP declarou que essa string JSON só é necessária mais tarde . A questão é se ele é necessário sempre ou apenas em alguns casos. Se for necessário apenas em alguns casos, faz sentido tê-lo em um arquivo externo e carregá-lo apenas condicionalmente.
Šime Vidas
2
Concordo que existem muitos "e se" que poderiam inclinar a escala para um lado ou para outro. Mas, de modo geral, se você souber quando a página for renderizada do que você vai precisar - mesmo que seja possível - geralmente é melhor enviá-la imediatamente. Por exemplo, se eu tivesse algumas caixas de informações que começam recolhidas, geralmente gostaria de incluir seu conteúdo embutido para que se expandissem instantaneamente. A sobrecarga de uma nova solicitação é muito comparada à sobrecarga de um pouco de dados extras em uma solicitação existente e cria uma experiência de usuário mais responsiva. Tenho certeza de que existe um ponto de ruptura.
Jamie Treworgy de
2

HTML5 inclui um <data>elemento para manter os dados legíveis por máquina. Como uma alternativa - talvez mais segura - para <script type="application/json">você pode incluir seus dados JSON dentro do valueatributo desse elemento.

const jsonData = document.querySelector('.json-data');
const data = JSON.parse(jsonData.value);

console.log(data)
<data class="json-data" value='
  {
    "unicorns": "awesome",
    "abc": [1, 2, 3],
    "careful": "to escape &#39; quotes"
  }
'></data>

Nesse caso, você precisa substituir todas as aspas simples por &#39;ou por, &quot;se optar por colocar o valor entre aspas duplas. Caso contrário, o risco de ataques XSS , como outras respostas sugeriram.

Rúnar Berg
fonte