Aller au contenu

Tout ce que la page reçoit est public : les props

Temps 2 · séance 1 · ~10 min

Les slides du chapitre (PDF)PDF

  • Retrouver dans le code source ou dans l'onglet Réseau les props qu'une page a reçues.
  • Repérer une prop qui envoie un modèle entier.
  • Remplacer un modèle entier par un tableau explicite, comme au chapitre 1.07.

1. Une page qui en dit plus qu'elle n'en montre

Cette faille n'a pas d'incident célèbre à elle seule. Une donnée envoyée au navigateur reste lisible, même si la page ne l'affiche pas. Avec Inertia, le serveur envoie les props en JSON, et un modèle entier y part avec toutes ses colonnes.

Le classement du Tamagotchi, /classement, montre les dix créatures les plus heureuses. Pour chacune, la page affiche son nom, son espèce, son score et le nom de son joueur : « Braise, dragon, chez Alice ». Voici le contrôleur, app/Http/Controllers/LeaderboardController.php :

public function __invoke(): Response
{
    return Inertia::render('Leaderboard', [
        'creatures' => Creature::with('user')
            ->orderByRaw('food + mood + energy desc')
            ->take(10)
            ->get(),
    ]);
}

get() renvoie des modèles Creature, chacun avec son modèle User chargé par with('user'). Inertia les transforme en JSON, colonne par colonne. Dans le modèle User, l'attribut #[Hidden] retire password, two_factor_secret, two_factor_recovery_codes et remember_token, comme au chapitre 1.10. Les autres colonnes partent vers le navigateur : l'adresse e-mail, la devise et le solde de chaque joueur.

Le podium du classement : Braise, dragon chez Alice, 265 points, au centre. Moustache, chat chez Alice, 215 points, à gauche. Noisette, lapin chez Bob, 210 points, à droite. Aucune adresse e-mail ni aucun solde des autres joueurs sur le podium. L'en-tête affiche le solde de Mallory, 5 €

Ce que Mallory obtient : l'adresse e-mail et le solde de tous les joueurs du classement, sans attaque. Elle n'a qu'à lire. Le développeur ne l'a pas vu, parce qu'il a regardé la page, et que la page ne montre rien de plus.

2. Lire ce que la page a reçu

Connectée comme Mallory, ouvrez /classement. Les props se lisent à deux endroits.

Au premier chargement, elles sont écrites dans le HTML de la page. Ouvrez le code source avec Ctrl+U, puis cherchez alice@ avec Ctrl+F. Vous tombez dans la balise <script data-page="app" type="application/json">. Voici la première créature, coupée par … :

"creatures":[{"id":1,"user_id":1,"name":"Braise","species":"dragon","food":90,"mood":95,"energy":80,"created_at":"…","updated_at":"…","user":{"id":1,"name":"Alice","email":"alice@example.com","email_verified_at":"…",…,"motto":"Un enclos propre, des cr\u00e9atures heureuses.","balance":120}},…

Le \u00e9 est le é de « créatures », encodé par Inertia comme au chapitre 1.07.

Pendant la navigation, Inertia demande les props en JSON. Ouvrez l'onglet Réseau, filtrez sur « Fetch/XHR », puis cliquez sur « 03 Classement » dans l'en-tête. La requête classement porte l'en-tête X-Inertia: true, et sa réponse est du JSON. Ce JSON est l'objet de la page entière : component, props, url, version et sharedProps. Dans l'onglet « Aperçu », dépliez props, puis creatures, puis l'entrée 0. Voici cette première créature :

{
    "id": 1,
    "user_id": 1,
    "name": "Braise",
    "species": "dragon",
    "food": 90,
    "mood": 95,
    "energy": 80,
    "created_at": "…",
    "updated_at": "…",
    "user": {
        "id": 1,
        "name": "Alice",
        "email": "alice@example.com",
        "email_verified_at": "…",
        "created_at": "…",
        "updated_at": "…",
        "two_factor_confirmed_at": null,
        "motto": "Un enclos propre, des créatures heureuses.",
        "balance": 120
    }
}

Les cinq créatures du seeder sont au classement : les adresses d'Alice, de Bob et de Mallory s'y lisent, avec leurs soldes de 120, 0,3 et 5 euros.

La prop partagée auth.user contient aussi le modèle entier, mais c'est celui de Mallory, envoyé à Mallory : le chapitre 1.10 l'a laissé tel quel pour cette raison.

Du contrôleur au navigateur. Avant : Creature::with('user')->get() envoie, pour Alice, son adresse e-mail, sa devise et son solde dans le JSON de la page, qui n'affiche que Braise, chez Alice. Après : chaque créature garde id, name, species, food, mood et energy, son objet user ne contient plus que id et name, et la page affiche la même chose

3. La correction

Au chapitre 1.07, la liste des projets construisait un tableau par projet avec map(). La fiche d'un projet construisait le sien avec only() et les trois points qui le déballent. Combinez ici les deux écritures dans LeaderboardController. Gardez les clés que la page Leaderboard.vue utilise, ni plus ni moins : id, name, species, food, mood, energy, et pour le joueur id et name. La page se sert de creature.user.id pour surligner vos propres créatures.

'creatures' => Creature::with('user')
    ->orderByRaw('food + mood + energy desc')
    ->take(10)
    ->get()
    ->map(fn (Creature $creature) => [
        ...$creature->only('id', 'name', 'species', 'food', 'mood', 'energy'),
        'user' => $creature->user->only('id', 'name'),
    ]),

with('user') reste : il charge les joueurs en une requête, au lieu d'une requête par créature. Rien ne change dans la page.

4. La preuve

Le test de la faille est dans tests/Feature/Flaws/Flaw04PublicPropsTest.php. Il lit la prop creatures et refuse d'y trouver email ou balance. Il ne vérifie rien d'autre : la devise ou une date qui resteraient passeraient le test. Relisez donc aussi le JSON après la correction. Avant la correction :

php artisan test --filter=Flaw04
   FAIL  Tests\Feature\Flaws\Flaw04PublicPropsTest
  ⨯ the leaderboard only sends what it displays                      0.86s
  ────────────────────────────────…
   FAILED  Tests\Feature\Flaws\Flaw04PublicPropsTest > the leaderboard only sends what it displays
  Expecting '[{"id":1,"user_id":1,"name":"…:50}}]' not to contain 'alice@example.com'.
…
  Tests:    1 failed (10 assertions)

Après la correction :

   PASS  Tests\Feature\Flaws\Flaw04PublicPropsTest
  ✓ the leaderboard only sends what it displays

  Tests:    1 passed (12 assertions)

php artisan test passe de 18 tests rouges à 17. L'onglet Réseau garde l'ancienne réponse : cliquez sur « 01 Mon tamagotchi », puis sur « 03 Classement », pour envoyer une nouvelle requête. Dans la nouvelle réponse classement, dépliez props, creatures, puis 0 :

{
    "id": 1,
    "name": "Braise",
    "species": "dragon",
    "food": 90,
    "mood": 95,
    "energy": 80,
    "user": {
        "id": 1,
        "name": "Alice"
    }
}

Chaque objet user ne contient plus que id et name. La page affiche exactement le même podium. Connectée comme Mallory, avec les cinq créatures du seeder et les fichiers compilés par npm run build, le contenu de la balise data-page passe de 2 743 caractères à 1 036.

5. La règle

Toute personne lit les props envoyées à son navigateur : les données des autres joueurs partent dans un tableau construit, avec les seuls champs nécessaires, jamais dans un modèle entier.

Recopiez-la dans votre carnet des failles. Dans votre portfolio, cherchez les Inertia::render qui passent un modèle ou un get() sans map() ni colonnes choisies.

À retenir

  • Inertia écrit les props dans la balise <script data-page> au premier chargement, et les envoie en JSON pendant la navigation.
  • Dans les modèles du Tamagotchi, un modèle passé en prop part avec ses colonnes non masquées par #[Hidden] et avec ses relations chargées.
  • Le code source, Ctrl+U, et l'onglet Réseau suffisent pour lire tout ce que la page a reçu.
  • map() et only() construisent un tableau des seuls champs nécessaires à l'affichage et au fonctionnement de la page.
  • Le test de la faille et le JSON relu prouvent la correction, alors que la page, elle, ne change pas.

Pour aller plus loin