6 façons de passer des variables de PHP à JavaScript

6 façons de passer des variables de PHP à JavaScript
Réponse rapide

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 :

php
$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.

php
// À ne pas faire
<script>
  let d = JSON.parse('<?php echo json_encode($donnee); ?>');
</script>

Ce que le navigateur reçoit :

javascript
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.

php
<script>
  const donnees = <?= json_encode($donnee,
      JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE
  ) ?>;
</script>

Résultat :

javascript
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.

php
<script type="application/json" id="donnees-page">
  <?= json_encode($donnee, JSON_HEX_TAG | JSON_UNESCAPED_UNICODE) ?>
</script>
javascript
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.

php
<div id="produit"
     data-id="<?= htmlspecialchars((string) $produit['id'], ENT_QUOTES, 'UTF-8') ?>"
     data-prix="<?= htmlspecialchars((string) $produit['prix'], ENT_QUOTES, 'UTF-8') ?>">
</div>
javascript
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 :

code
valeur brute : valeur" onfocus="alert(1)" autofocus x="
sans échappement : <input value="valeur" onfocus="alert(1)" autofocus x="">
avec échappement : <input value="valeur&quot; onfocus=&quot;alert(1)&quot; autofocus x=&quot;">

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.

php
<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.

api/produits.php
<?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,
);
javascript
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.

src/JsDefs.php
<?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),
        );
    }
}
php
$defs = new JsDefs();
$defs->ajouter('urlPanier', '/panier');
$defs->ajouter('devise', '€');
$defs->ajouter('utilisateurConnecte', false);

echo $defs->rendre();
javascript
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.

javascript
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.

php
$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

JSON.parse('<?= json_encode($x) ?>') Une apostrophe dans la donnée ferme la chaîne JavaScript, et une séquence </script> ferme le bloc au niveau de l'analyseur HTML. Résultat : erreur de syntaxe et injection de script.
JSON_HEX_TAG oublié Sans lui, </script> apparaît littéralement et sort du bloc, y compris dans un <script type="application/json">.
Attribut sans htmlspecialchars Une valeur contenant un guillemet sort de l'attribut et ajoute un gestionnaire d'événement.
ENT_SUBSTITUTE absent Sur une séquence UTF-8 invalide, htmlspecialchars() renvoie une chaîne vide au lieu de la valeur.
POST présenté comme plus sûr que GET Faux. Les deux voyagent en clair sans TLS. La différence tient aux traces : l'URL apparaît dans l'historique, les journaux et le Referer.
Cookie pour passer une donnée à la page Un cookie lisible par JavaScript ne peut pas porter HttpOnly, et repart dans chaque requête.
Donnée du client considérée comme fiable Un prix ou un identifiant de rôle transmis à JavaScript est modifiable. Toute valeur qui revient doit être revalidée côté serveur.

JavaScriptJSONPHPSécuritéXSS

Damien Flandrin Développeur web depuis 2010, créateur de Gekkode et d’Email Impact. Chaque article est testé sur un projet réel avant publication. Contact
Newsletter

Les nouveaux tests, tutoriels et projets, par e-mail.

Tests reproductibles, code versionné, résultats datés. Jamais de spam.