
Il metodo di default è un blocco letto con JSON.parse: i dati non vengono mai interpretati come codice e la pagina resta compatibile con una Content Security Policy stretta. Se scrivi il JSON direttamente dentro uno script, fallo senza apici e senza JSON.parse, con JSON_HEX_TAG e le opzioni vicine.
Far passare un valore da PHP a JavaScript è un’esigenza quotidiana, e la maggior parte dei metodi che si trovano online apre una falla XSS. Questo articolo mette a confronto sei approcci, mostra cosa produce ciascuno con un dato ostile e indica quale usare a seconda del caso.
Il nocciolo del problema
PHP gira sul server, JavaScript nel browser. L’unico punto di passaggio è l’HTML prodotto. Quando un dato PHP viene scritto dentro del JavaScript cambia contesto di interpretazione: quella che era una stringa diventa codice. Se il dato contiene i caratteri giusti, esce dalla stringa e diventa eseguibile.
Prendiamo un valore realistico, come uscirebbe da un database:
$donnee = [
'nom' => "L'Écran </script><script>alert(1)</script>",
'note' => 4.5,
];Contiene un apostrofo, un tag di chiusura e un tag di apertura. Ognuno rompe un metodo diverso.
1. json_encode dentro un blocco script
È il metodo più diffuso, ed è quasi sempre scritto male.
// Da non fare
<script>
let d = JSON.parse('<?php echo json_encode($donnee); ?>');
</script>Quello che riceve il browser:
let d = JSON.parse('{"nom":"L'Écran </script><script>alert(1)</script>","note":4.5}');Due rotture. L’apostrofo di «L’Écran» chiude la stringa JavaScript: errore di sintassi, la pagina non gira più. E la sequenza </script> chiude il blocco a livello del parser HTML, che delle stringhe JavaScript non sa nulla: lo <script>alert(1)</script> che segue diventa un vero blocco di script.
La forma corretta non ha né apici né JSON.parse(). Il JSON è già un’espressione JavaScript valida.
<script>
const donnees = <?= json_encode($donnee,
JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE
) ?>;
</script>Risultato:
const donnees = {"nom":"L'Écran alert(1)","note":4.5};Le quattro opzioni di escape trasformano <, >, &, ' e " in sequenze uXXXX. Nessuno di questi caratteri compare più alla lettera, quindi nessuno può chiudere alcunché. JSON_UNESCAPED_UNICODE mantiene leggibili le lettere accentate, il che alleggerisce l’output senza compromettere nulla.
2. Un blocco JSON separato
È il metodo che consigliamo di default. I dati escono dal codice e diventano contenuto.
<script type="application/json" id="donnees-page">
<?= json_encode($donnee, JSON_HEX_TAG | JSON_UNESCAPED_UNICODE) ?>
</script>const donnees = JSON.parse(
document.getElementById('donnees-page').textContent
);Uno <script> con un type che il browser non conosce non viene eseguito: è un semplice contenitore di testo. JSON_HEX_TAG resta necessario per impedire che un </script> chiuda il blocco, ma il contenuto non viene mai interpretato come codice.
Questa forma ha un vantaggio decisivo: funziona con una Content Security Policy stretta. Un sito che vieta gli script inline per proteggersi dalle injection non può usare il metodo 1 senza aggiungerci un nonce o un’impronta.
3. Gli attributi data
Per qualche valore semplice legato a un elemento, l’attributo data-* è la soluzione più naturale.
<div id="produit"
data-id="<?= htmlspecialchars((string) $produit['id'], ENT_QUOTES, 'UTF-8') ?>"
data-prix="<?= htmlspecialchars((string) $produit['prix'], ENT_QUOTES, 'UTF-8') ?>">
</div>const el = document.getElementById('produit');
const id = Number(el.dataset.id);
const prix = parseFloat(el.dataset.prix);htmlspecialchars() con ENT_QUOTES è obbligatorio. Senza, un valore che contiene delle virgolette esce dall’attributo:
valeur brute : valeur" onfocus="alert(1)" autofocus x="
sans échappement : <input value="valeur" onfocus="alert(1)" autofocus x="">
avec échappement : <input value="valeur" onfocus="alert(1)" autofocus x="">I valori di dataset sono sempre stringhe: la conversione in numero va fatta lato JavaScript.
4. I campi nascosti
Un <input type="hidden"> funziona, con lo stesso escape obbligatorio.
<input type="hidden" id="jeton"
value="<?= htmlspecialchars($jeton, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>">Serve soprattutto quando il valore deve tornare al server insieme al form. Per un semplice passaggio verso JavaScript l’attributo data-* è più pulito: non inquina l’invio del form.
ENT_SUBSTITUTE vale la pena aggiungerlo: senza, htmlspecialchars() restituisce una stringa vuota se il dato contiene una sequenza UTF-8 non valida. Un campo vuoto al posto di un valore è un bug difficile da rintracciare.
5. Una chiamata a un’API
Appena i dati sono voluminosi, mutevoli o dipendono da un’azione dell’utente, non hanno niente da fare nell’HTML.
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
echo json_encode(
['produits' => $produits],
JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR,
);const reponse = await fetch('/api/produits.php', {
headers: { 'Accept': 'application/json' },
});
if (!reponse.ok) {
throw new Error(`HTTP ${reponse.status}`);
}
const { produits } = await reponse.json();In questo caso l’escape HTML non si pone più: la risposta non viene mai interpretata come HTML, a patto che l’header Content-Type sia corretto e accompagnato da nosniff.
Il costo è una richiesta in più. Per dati necessari al primo rendering, il metodo 2 evita l’andata e ritorno.
6. Accumulare più valori
Quando più parti dell’applicazione hanno ciascuna un valore da trasmettere, raccoglierli e scriverli in una volta sola evita di disseminare blocchi <script>. È il principio di Media::addJsDef() in PrestaShop.
<?php
declare(strict_types=1);
final class JsDefs
{
/** @var array<string, mixed> */
private array $valeurs = [];
public function ajouter(string $nom, mixed $valeur): void
{
$this->valeurs[$nom] = $valeur;
}
public function rendre(string $id = 'js-defs'): string
{
return sprintf(
'<script type="application/json" id="%s">%s</script>',
htmlspecialchars($id, ENT_QUOTES, 'UTF-8'),
json_encode($this->valeurs, JSON_HEX_TAG | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
);
}
}$defs = new JsDefs();
$defs->ajouter('urlPanier', '/panier');
$defs->ajouter('devise', '€');
$defs->ajouter('utilisateurConnecte', false);
echo $defs->rendre();const defs = JSON.parse(document.getElementById('js-defs').textContent);
console.log(defs.urlPanier, defs.devise, defs.utilisateurConnecte);Tenere i valori in un oggetto invece che in una variabile globale evita le collisioni di nomi e rende la classe testabile.
I metodi da scartare
Due approcci circolano e non andrebbero usati.
Passare dall’URL. Scrivere <a href="http://script.js?valeur=$v"> non ha senso: un file JavaScript viene servito così com’è, i suoi parametri d’URL non vengono letti. Quello che esiste davvero è leggere lato client i parametri della pagina corrente, ma allora il valore arriva dall’URL, non da PHP.
const page = new URLSearchParams(window.location.search).get('page');Passare da un cookie. Un cookie destinato a essere letto da JavaScript non può portare l’attributo HttpOnly, che è la protezione principale contro il furto di sessione. In più riparte a ogni richiesta, appesantendo tutto il traffico. Un cookie serve a conservare uno stato tra due richieste, non a trasmettere un dato nella pagina corrente.
Una precisazione sulla sicurezza
Si legge spesso che POST sarebbe «più sicuro» di GET. È falso. Entrambi trasportano i dati in chiaro se la connessione non è cifrata, ed entrambi sono modificabili dall’utente. La differenza sta altrove: i parametri GET compaiono nell’URL, quindi nella cronologia del browser, nei log del server e nell’header Referer. È una questione di tracce lasciate in giro, non di sicurezza del trasporto.
Il punto che vale per tutti e sei i metodi: tutto quello che arriva nel browser è visibile e modificabile dall’utente. Un prezzo, un identificativo di ruolo o un totale trasmesso a JavaScript va rivalidato lato server a ogni azione. Nessun dato inviato al client è degno di fiducia quando torna indietro.
Cosa scegliere
| Esigenza | Metodo |
|---|---|
| Un oggetto di configurazione al caricamento | Blocco application/json |
| Qualche valore legato a un elemento | Attributi data-* |
| Un valore che riparte con un form | Campo nascosto |
| Dati voluminosi o caricati più tardi | Chiamata a un’API |
| Più moduli che contribuiscono | Un accumulatore, reso in una volta sola |
| Content Security Policy stretta | Blocco application/json, mai script inline |
Se i dati arrivano da un’API di terze parti, è una richiesta cURL a recuperarli lato server. L’accumulatore visto sopra applica i principi della programmazione a oggetti in PHP, e il redirect dopo l’elaborazione è spiegato in creare un redirect in PHP.
Vedi anche json_encode e la serializzazione degli oggetti PHP e l’hub Sviluppo web.
Errori frequenti
</script> chiude il blocco a livello del parser HTML. Risultato: errore di sintassi e injection di script.</script> compare alla lettera ed esce dal blocco, anche dentro uno <script type="application/json">.htmlspecialchars() restituisce una stringa vuota al posto del valore.HttpOnly, e riparte a ogni richiesta.

