
La méthode par défaut est un bloc lu avec JSON.parse : les données ne sont jamais interprétées comme du code et la page reste compatible avec une politique de sécurité de contenu stricte. Si vous écrivez du JSON directement dans un script, faites-le sans guillemets ni JSON.parse, avec JSON_HEX_TAG et ses options voisines.
Faire passer une valeur de PHP à JavaScript est un besoin quotidien, et la plupart des méthodes qu’on trouve en ligne créent une faille de type XSS. Cet article compare six approches, montre ce que chacune produit avec une donnée hostile, et indique laquelle utiliser selon le cas.
Le fond du problème
PHP s’exécute sur le serveur, JavaScript dans le navigateur. Le seul point de passage est le HTML produit. Quand une donnée PHP est écrite dans du JavaScript, elle change de contexte d’interprétation : ce qui était une chaîne de caractères devient du code. Si la donnée contient les bons caractères, elle sort de la chaîne et devient exécutable.
Prenons une valeur réaliste, telle qu’elle sortirait d’une base de données :
$donnee = [
'nom' => "L'Écran </script><script>alert(1)</script>",
'note' => 4.5,
];Elle contient une apostrophe, une balise fermante et une balise ouvrante. Chacune casse une méthode différente.
1. json_encode dans un bloc script
C’est la méthode la plus courante, et elle est presque toujours mal écrite.
// À ne pas faire
<script>
let d = JSON.parse('<?php echo json_encode($donnee); ?>');
</script>Ce que le navigateur reçoit :
let d = JSON.parse('{"nom":"L'Écran </script><script>alert(1)</script>","note":4.5}');Deux ruptures. L’apostrophe de « L’Écran » ferme la chaîne JavaScript : erreur de syntaxe, la page ne s’exécute plus. Et la séquence </script> ferme le bloc au niveau de l’analyseur HTML, qui ne connaît pas les chaînes JavaScript : le <script>alert(1)</script> qui suit devient un vrai bloc de script.
La forme correcte n’a ni guillemets ni JSON.parse(). Du JSON est déjà une expression JavaScript valide.
<script>
const donnees = <?= json_encode($donnee,
JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE
) ?>;
</script>Résultat :
const donnees = {"nom":"L'Écran alert(1)","note":4.5};Les quatre options d’échappement transforment <, >, &, ' et " en séquences uXXXX. Plus aucun de ces caractères n’apparaît littéralement, donc aucun ne peut fermer quoi que ce soit. JSON_UNESCAPED_UNICODE garde les accents lisibles, ce qui allège la sortie sans rien compromettre.
2. Un bloc JSON séparé
C’est la méthode que nous recommandons par défaut. Les données sortent du code et deviennent du contenu.
<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
);Un <script> avec un type inconnu du navigateur n’est pas exécuté : c’est un simple conteneur de texte. JSON_HEX_TAG reste nécessaire pour empêcher un </script> de fermer le bloc, mais le contenu n’est jamais interprété comme du code.
Cette forme a un avantage décisif : elle fonctionne avec une politique de sécurité de contenu stricte. Un site qui interdit les scripts en ligne pour se protéger des injections ne peut pas utiliser la méthode 1 sans y ajouter un jeton ou une empreinte.
3. Les attributs data
Pour quelques valeurs simples attachées à un élément, l’attribut data-* est la solution la plus naturelle.
<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() avec ENT_QUOTES est obligatoire. Sans lui, une valeur contenant un guillemet sort de l’attribut :
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="">Les valeurs de dataset sont toujours des chaînes : la conversion en nombre est à faire côté JavaScript.
4. Les champs cachés
Un <input type="hidden"> fonctionne, avec le même échappement obligatoire.
<input type="hidden" id="jeton"
value="<?= htmlspecialchars($jeton, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?>">C’est surtout utile quand la valeur doit repartir vers le serveur avec le formulaire. Pour un simple passage vers JavaScript, l’attribut data-* est plus propre : il ne pollue pas l’envoi du formulaire.
ENT_SUBSTITUTE mérite d’être ajouté : sans lui, htmlspecialchars() renvoie une chaîne vide si la donnée contient une séquence UTF-8 invalide. Un champ vide au lieu d’une valeur est un bug difficile à retrouver.
5. Un appel à une API
Dès que les données sont volumineuses, changeantes, ou dépendent d’une action de l’utilisateur, elles n’ont rien à faire dans le 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();Dans ce cas, l’échappement HTML ne se pose plus : la réponse n’est jamais interprétée comme du HTML, à condition que l’en-tête Content-Type soit correct et accompagné de nosniff.
Le coût est une requête supplémentaire. Pour des données nécessaires au premier affichage, la méthode 2 évite cet aller-retour.
6. Accumuler plusieurs valeurs
Quand plusieurs parties de l’application ont chacune une valeur à transmettre, les collecter et les écrire en une fois évite d’éparpiller les blocs <script>. C’est le principe de Media::addJsDef() dans 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);Le stockage dans un objet plutôt que dans une variable globale évite les collisions de noms et rend la classe testable.
Les méthodes à écarter
Deux approches circulent et ne devraient pas être utilisées.
Passer par l’URL. Écrire <a href="script.js?valeur=$v"> n’a pas de sens : un fichier JavaScript est servi tel quel, ses paramètres d’URL ne sont pas lus. Ce qui existe réellement, c’est lire les paramètres de la page courante côté client, mais alors la valeur vient de l’URL, pas de PHP.
const page = new URLSearchParams(window.location.search).get('page');Passer par un cookie. Un cookie destiné à être lu par JavaScript ne peut pas porter l’attribut HttpOnly, qui est la principale protection contre le vol de session. Il repart aussi dans chaque requête, ce qui alourdit tout le trafic. Un cookie sert à conserver un état entre les requêtes, pas à transmettre une donnée dans la page courante.
Une précision sur la sécurité
On lit souvent que POST serait « plus sécurisé » que GET. C’est faux. Les deux transportent les données en clair si la connexion n’est pas chiffrée, et les deux sont modifiables par l’utilisateur. La différence est ailleurs : les paramètres GET apparaissent dans l’URL, donc dans l’historique du navigateur, les journaux du serveur et l’en-tête Referer. C’est une question de fuite par les traces, pas de sécurité du transport.
Le point qui vaut pour les six méthodes : tout ce qui arrive dans le navigateur est visible et modifiable par l’utilisateur. Un prix, un identifiant de rôle ou un total transmis à JavaScript doit être revalidé côté serveur à chaque action. Aucune donnée envoyée au client n’est digne de confiance au retour.
Que choisir
| Besoin | Méthode |
|---|---|
| Un objet de configuration au chargement | Bloc application/json |
| Quelques valeurs liées à un élément | Attributs data-* |
| Une valeur qui repart avec un formulaire | Champ caché |
| Données volumineuses ou tardives | Appel à une API |
| Plusieurs modules qui contribuent | Un accumulateur, rendu en une fois |
| Politique de sécurité de contenu stricte | Bloc application/json, jamais de script en ligne |
Si les données viennent d’une API tierce, c’est une requête cURL qui les récupère côté serveur. L’accumulateur présenté plus haut applique les principes de la POO en PHP, et la redirection après traitement est détaillée dans créer une redirection en PHP.
Voir aussi json_encode et la sérialisation d’objets PHP et le hub Développement web.
json_encode : sérialiser un objet PHP en JSON
json_encode() transforme récursivement tableaux et objets en JSON. Pour un objet, seules les propriétés publiques sont incluses par défaut ; JsonSerializable permet de choisir les données exposées. Ne sérialisez pas automatiquement mots de passe ou jetons.
$produit = (object) ['nom' => 'Clavier', 'prix' => 49.9];
try {
$json = json_encode($produit, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
// {"nom":"Clavier","prix":49.9}
} catch (JsonException $e) {
// Refuser la réponse et journaliser l’erreur côté serveur.
}JSON_THROW_ON_ERROR signale notamment une chaîne UTF-8 invalide ou une référence récursive. Un tableau aux clés numériques non consécutives devient un objet JSON : utilisez array_values() si vous attendez une liste. Pour insérer cette sortie dans du HTML, appliquez également les protections de contexte expliquées plus haut. Référence PHP : json_encode.
Erreurs fréquentes
</script> ferme le bloc au niveau de l'analyseur HTML. Résultat : erreur de syntaxe et injection de script.</script> apparaît littéralement et sort du bloc, y compris dans un <script type="application/json">.htmlspecialchars() renvoie une chaîne vide au lieu de la valeur.HttpOnly, et repart dans chaque requête.

