<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="https://ktherage.github.io/fr/xsl/atom.xsl" media="all"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="fr">
  <id>https://ktherage.github.io/fr/tags/security/</id>
  <title>Kévin THÉRAGE | Expert Symfony Developer - Security</title>
  <subtitle><![CDATA[Kévin THÉRAGE – Expert Symfony Developer. Technical blog on Symfony, PHP, web development with tutorials, best practices and expert advice for developers.]]></subtitle>
  <link href="https://ktherage.github.io/fr/tags/security/atom.xml" rel="self" type="application/atom+xml" />
  <link href="https://ktherage.github.io/fr/tags/security/" rel="alternate" type="text/html" />
  <updated>2026-06-18T15:17:31+00:00</updated>
  <author>
    <name>Kévin THÉRAGE</name>
    <uri>https://ktherage.github.io/</uri>
  </author>
  <entry xml:lang="fr">
    <id>https://ktherage.github.io/fr/blog/my-eventsubscriber-silenced-errors/</id>
    <title>Mon EventSubscriber masquait les erreurs, voici pourquoi</title>
    <published>2026-04-13T00:00:00+00:00</published>
    <link href="https://ktherage.github.io/fr/blog/my-eventsubscriber-silenced-errors/" rel="alternate" type="text/html" />
    <content type="html">
      <![CDATA[<p>Un ticket Jira est apparu : <em>« Il y a un bug étrange qui empêche les utilisateurs d'accéder à une page à ce moment-là de la journée. »</em>
Les logs indiquaient plusieurs fois : <em>« [ce jour T cette heure] request.ERROR: Uncaught PHP Exception Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException: "Access denied to that resource." at WhitelistSubscriber.php line 99 »</em></p>
<p>Je n'avais aucune idée au début… 😅 Voici comment j'ai compris.</p>
<hr>
<h2 id="la-configuration-1">La configuration</h2>
<p>J'avais un EventSubscriber qui vérifiait l'accès aux pages basé sur une liste blanche de routes. C'était du code legacy — le refactoriser n'était pas à l'ordre du jour à ce moment-là.</p>
<pre><code class="language-php hljs php" translate="no"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">EventSubscriber</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">EventDispatcher</span>\<span class="hljs-title">EventSubscriberInterface</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">HttpKernel</span>\<span class="hljs-title">Event</span>\<span class="hljs-title">RequestEvent</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">HttpKernel</span>\<span class="hljs-title">KernelEvents</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WhitelistRouteSubscriber</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">EventSubscriberInterface</span>
</span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">const</span> WHITELISTED_ROUTES = [
        <span class="hljs-string">'app_login'</span>,
        <span class="hljs-string">'app_homepage'</span>,
        <span class="hljs-string">'app_healthcheck'</span>,
    ];

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSubscribedEvents</span><span class="hljs-params">()</span>: <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [
            KernelEvents::REQUEST =&gt; [<span class="hljs-string">'onKernelRequest'</span>, <span class="hljs-number">0</span>],
        ];
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onKernelRequest</span><span class="hljs-params">(RequestEvent $event)</span>: <span class="hljs-title">void</span>
    </span>{
        $request = $event-&gt;getRequest();
        $route = $request-&gt;attributes-&gt;get(<span class="hljs-string">'_route'</span>);

        <span class="hljs-comment">// Allow whitelisted routes</span>
        <span class="hljs-keyword">if</span> (in_array($route, <span class="hljs-keyword">self</span>::WHITELISTED_ROUTES, <span class="hljs-keyword">true</span>)) {
            <span class="hljs-keyword">return</span>;
        }

        <span class="hljs-comment">// Deny access for non-whitelisted routes</span>
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> AccessDeniedHttpException(<span class="hljs-string">'Route not whitelisted'</span>);
    }
}</code></pre>
<p>Objectif : Bloquer toutes les routes sauf la liste blanche. Simple, non ?</p>
<hr>
<h2 id="le-probleme-1">Le problème</h2>
<p>Les logs montraient des <code translate="no">AccessDeniedHttpException</code> sur des routes que je savais être dans la liste blanche. Premier geste classique : mettre un <code translate="no">dump()</code> dans le subscriber pour voir ce qui arrivait.</p>
<pre><code class="language-php hljs php" translate="no"><span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onKernelRequest</span><span class="hljs-params">(RequestEvent $event)</span>: <span class="hljs-title">void</span>
</span>{
    $request = $event-&gt;getRequest();
    $route = $request-&gt;attributes-&gt;get(<span class="hljs-string">'_route'</span>);

    dump($route); <span class="hljs-comment">// 🔍 Voyons ce qui se passe</span>
    <span class="hljs-comment">// ...</span>
}</code></pre>
<p>Première découverte surprenante : <strong>le subscriber était appelé deux fois</strong> pour une seule requête. Le premier appel avait la route attendue, le second avait <code translate="no">$route = null</code>.</p>
<p>Question évidente : <em>pourquoi <code translate="no">_route</code> est-il null ?</em></p>
<p>J'ai creusé plus loin avec <code translate="no">dump($request-&gt;getPathInfo())</code> pour voir quelle URL était traitée lors du second appel :</p>
<pre><code translate="no">// 1er appel
dump($request-&gt;getPathInfo()); // "/foo"

// 2e appel
dump($request-&gt;getPathInfo()); // "/foo" ← identique. Attends, quoi ?</code></pre>
<p>Même URL, appelée deux fois. Cela n'avait aucun sens — si c'était la même requête, pourquoi <code translate="no">_route</code> était-il null la deuxième fois ? Je tournais en rond.</p>
<p>J'ai donc dumpé l'objet <code translate="no">$event</code> complet pour avoir plus de contexte, et j'ai réduit le champ à <code translate="no">_controller</code> dans les attributs de la requête :</p>
<pre><code class="language-php hljs php" translate="no">dump($request-&gt;attributes-&gt;get(<span class="hljs-string">'_controller'</span>));
<span class="hljs-comment">// "Symfony\Component\HttpKernel\Controller\ErrorController"</span></code></pre>
<p>Voilà. <code translate="no">_controller</code> ne pointait pas vers mon code du tout. Symfony avait forgé une toute nouvelle requête vers son propre <code translate="no">ErrorController</code>, réutilisant l'URL d'origine — ce qui explique pourquoi <code translate="no">getPathInfo()</code> était si trompeur — mais en contournant complètement le routeur. C'est pourquoi <code translate="no">_route</code> était null.</p>
<hr>
<h2 id="cause-racine-1">Cause racine</h2>
<p>Le flux réel était :</p>
<pre><code translate="no">Requête → /foo
  └── WhitelistSubscriber (1er appel) → _route = 'app_foo' ✅ Accès accordé
      └── Controller → lève RealException 💥
          └── Symfony l'attrape
              └── Sous-requête → ErrorController (contourne le routeur, pas de _route)
                  └── WhitelistSubscriber (2e appel) → _route = null ❌ AccessDenied levé
                      └── RealException est maintenant silencieuse 🔇</code></pre>
<p>Le piège : <strong>l'<code translate="no">AccessDeniedHttpException</code> du subscriber masquait complètement l'exception originale</strong> — celle qui contenait réellement les informations de débogage utiles.</p>
<p>Lorsqu'une exception est levée, le <code translate="no">HttpKernel</code> de Symfony distribue un événement <code translate="no">KernelEvents::EXCEPTION</code>, puis délègue le rendu de l'erreur à <code translate="no">ErrorController</code> via une sous-requête interne. Cette sous-requête réutilise l'URL d'origine — ce qui explique pourquoi <code translate="no">getPathInfo()</code> était trompeur — mais elle contourne complètement la couche de routage, laissant <code translate="no">_route</code> à <code translate="no">null</code>.</p>
<hr>
<h2 id="la-solution-1">La solution</h2>
<p>Vérifiez si la requête est la requête principale (pas une sous-requête) :</p>
<pre><code class="language-php hljs php" translate="no"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">EventSubscriber</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">EventDispatcher</span>\<span class="hljs-title">EventSubscriberInterface</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">HttpKernel</span>\<span class="hljs-title">Event</span>\<span class="hljs-title">RequestEvent</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Symfony</span>\<span class="hljs-title">Component</span>\<span class="hljs-title">HttpKernel</span>\<span class="hljs-title">KernelEvents</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">WhitelistRouteSubscriber</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">EventSubscriberInterface</span>
</span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">const</span> WHITELISTED_ROUTES = [
        <span class="hljs-string">'app_login'</span>,
        <span class="hljs-string">'app_homepage'</span>,
        <span class="hljs-string">'app_healthcheck'</span>,
    ];

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSubscribedEvents</span><span class="hljs-params">()</span>: <span class="hljs-title">array</span>
    </span>{
        <span class="hljs-keyword">return</span> [
            KernelEvents::REQUEST =&gt; [<span class="hljs-string">'onKernelRequest'</span>, <span class="hljs-number">0</span>],
        ];
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onKernelRequest</span><span class="hljs-params">(RequestEvent $event)</span>: <span class="hljs-title">void</span>
    </span>{
        <span class="hljs-comment">// Skip sub-requests (like error handling)</span>
        <span class="hljs-keyword">if</span> (!$event-&gt;isMainRequest()) {
            <span class="hljs-keyword">return</span>;
        }

        $request = $event-&gt;getRequest();
        $route = $request-&gt;attributes-&gt;get(<span class="hljs-string">'_route'</span>);

        <span class="hljs-comment">// Allow whitelisted routes</span>
        <span class="hljs-keyword">if</span> (in_array($route, <span class="hljs-keyword">self</span>::WHITELISTED_ROUTES, <span class="hljs-keyword">true</span>)) {
            <span class="hljs-keyword">return</span>;
        }

        <span class="hljs-comment">// Deny access for non-whitelisted routes</span>
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> AccessDeniedHttpException(<span class="hljs-string">'Route not whitelisted'</span>);
    }
}</code></pre>
<p><code translate="no">isMainRequest()</code> retourne <code translate="no">false</code> pour toute sous-requête interne — gestion d'erreurs, fragments ESI, <code translate="no">hinclude</code> — donc votre logique ne s'exécute que sur les vraies requêtes distribuées par le routeur.</p>
<blockquote>
<p><strong>Note :</strong> <code translate="no">isMainRequest()</code> a remplacé l'ancien <code translate="no">isMasterRequest()</code> dans Symfony 5.3. Si vous êtes sur une version plus ancienne, utilisez <code translate="no">isMasterRequest()</code> à la place.</p>
</blockquote>
<hr>
<h2 id="conclusion-1">Conclusion</h2>
<p>Chaque fois que votre subscriber fait quelque chose de destructeur — lever une exception, rediriger, définir une réponse — demandez-vous : <em>que se passe-t-il quand Symfony appelle ceci sur une sous-requête ?</em></p>
<p>Les sous-requêtes sont omniprésentes dans Symfony : gestion d'erreurs, ESI, fragments. Elles n'ont pas le même contexte qu'une requête principale, et votre subscriber ne connaît pas la différence à moins que vous ne lui disiez.</p>
<p><code translate="no">isMainRequest()</code> est cette vérification. Faites-en un réflexe. 🎉</p>]]>
    </content>
  </entry>
</feed>
