<?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>JWT Vulnerabilities: The Auth Bugs Hiding Behind a Green Checkmark</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Thu, 17 Sep 2026 23:25:52 +0000</pubDate>
      <link>https://dev.to/kriosa/jwt-vulnerabilities-the-auth-bugs-hiding-behind-a-green-checkmark-4ae3</link>
      <guid>https://dev.to/kriosa/jwt-vulnerabilities-the-auth-bugs-hiding-behind-a-green-checkmark-4ae3</guid>
      <description>&lt;p&gt;How algorithm confusion, the &lt;code&gt;none&lt;/code&gt; algorithm, weak signing secrets, missing expiration checks, and improper claim validation can turn a cryptographically signed token into an attacker's path to unauthorized access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why JWTs get trusted more than they should
&lt;/h2&gt;

&lt;p&gt;A JWT looks like proof.&lt;/p&gt;

&lt;p&gt;Three base64url-encoded segments, a signature at the end, and a library that returns &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt; can create the impression that a token is either valid or invalid. That confidence is exactly where many JWT security problems begin.&lt;/p&gt;

&lt;p&gt;A signature only establishes integrity if the verifier agrees with the application about &lt;strong&gt;which algorithm was used, which key should verify it, and whether the token is still valid in context&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Get any of those wrong — or allow attacker-controlled input to influence the verification process — and "the signature checks out" can stop meaning what developers think it means.&lt;/p&gt;

&lt;p&gt;JWTs are used for API authentication, SSO, mobile sessions, password-reset flows, and service-to-service authentication. That makes JWT verification logic part of a security boundary, and small implementation mistakes can have serious consequences.&lt;/p&gt;

&lt;h2&gt;
  
  
  The anatomy, quickly
&lt;/h2&gt;

&lt;p&gt;A JWT consists of three base64url-encoded segments joined by dots:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;header.payload.signature&lt;/code&gt;&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwiYWRtaW4iOnRydWV9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first two segments are encoded, not encrypted. Anyone holding the token can decode them without knowing the signing key:&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;header&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;span class="nl"&gt;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HS256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"typ"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JWT"&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;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;payload&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;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1234"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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="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;That header is important because &lt;strong&gt;the token carries its own algorithm identifier&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The server therefore has to decide whether that algorithm is acceptable. It should not blindly accept whatever the token claims.&lt;/p&gt;

&lt;p&gt;The payload is also attacker-controlled until the signature has been successfully verified. A decoded claim is data — not authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #1: Accepting the &lt;code&gt;none&lt;/code&gt; algorithm
&lt;/h2&gt;

&lt;p&gt;JWT defines &lt;code&gt;none&lt;/code&gt; as an algorithm meaning that the token has no digital signature.&lt;/p&gt;

&lt;p&gt;That can be legitimate in narrowly controlled scenarios, but an application using signed JWTs for authentication should not allow an attacker to switch a token into an unsigned mode.&lt;/p&gt;

&lt;p&gt;The vulnerable pattern looks conceptually 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="c1"&gt;// DON'T DO THIS — trusting the algorithm supplied by the token&lt;/span&gt;
&lt;span class="nv"&gt;$header&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;base64_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$parts&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="kc"&gt;true&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;$header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'alg'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'none'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Never treat an attacker-controlled header as permission&lt;/span&gt;
    &lt;span class="c1"&gt;// to skip authentication.&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="nb"&gt;base64_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$parts&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="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;An attacker could modify the payload:&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;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1234"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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="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;and submit an unsigned token such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;modified-header&amp;gt;.&amp;lt;modified-payload&amp;gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important security flaw isn't the existence of &lt;code&gt;none&lt;/code&gt;. It's allowing &lt;strong&gt;untrusted token input to determine whether signature verification happens at all&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix
&lt;/h3&gt;

&lt;p&gt;The application should determine the accepted algorithm server-side.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;firebase/php-jwt&lt;/code&gt;, for example:&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="no"&gt;Firebase\JWT\JWT&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;Firebase\JWT\Key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JWT&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;decode&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'HS256'&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 verifier is explicitly configured to use &lt;code&gt;HS256&lt;/code&gt;. The token does not get to select a different verification algorithm.&lt;/p&gt;

&lt;p&gt;Modern JWT libraries have significantly reduced this class of vulnerability by making explicit algorithm/key configuration part of their APIs. The remaining risk is usually application code that bypasses those protections or reintroduces dynamic algorithm selection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #2: Algorithm confusion — RS256 tokens verified as HS256
&lt;/h2&gt;

&lt;p&gt;Algorithm confusion is more subtle.&lt;/p&gt;

&lt;p&gt;It primarily affects implementations using asymmetric algorithms such as RS256, where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the issuer signs with a &lt;strong&gt;private key&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;the verifier checks the signature with the corresponding &lt;strong&gt;public key&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The public key is intentionally not secret. It may even be published through a JWKS endpoint.&lt;/p&gt;

&lt;p&gt;The dangerous implementation pattern is one where the token can influence the selected algorithm and the verification code is willing to use the same configured key material across incompatible algorithm families.&lt;/p&gt;

&lt;p&gt;Conceptually:&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;// DON'T DO THIS&lt;/span&gt;
&lt;span class="nv"&gt;$header&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;base64_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$parts&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="kc"&gt;true&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="nf"&gt;getPublicKeyForVerification&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// The token is influencing which verification algorithm is used.&lt;/span&gt;
&lt;span class="nv"&gt;$decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JWT&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;decode&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Key&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;$header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'alg'&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 an implementation changes from RSA verification to HMAC verification based on an attacker-controlled &lt;code&gt;alg&lt;/code&gt; value, the public RSA key may be interpreted as an HMAC secret.&lt;/p&gt;

&lt;p&gt;Because the public key is not secret, an attacker who knows it could potentially construct an HMAC-signed token using that public value.&lt;/p&gt;

&lt;p&gt;Conceptually:&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;$forgedToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JWT&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sub'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'admin'&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="nv"&gt;$publicKeyPem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'HS256'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whether this works depends on the exact library and application configuration. It is &lt;strong&gt;not&lt;/strong&gt; a universal property of JWT or RS256.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix
&lt;/h3&gt;

&lt;p&gt;Pin the algorithm and match it to the expected key type:&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;$decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JWT&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;decode&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$rsaPublicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'RS256'&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 multiple algorithms are genuinely required, the application should maintain an explicit mapping between algorithms and compatible keys rather than allowing the token to select an arbitrary combination.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RS256 → RSA public key
ES256 → EC public key
HS256 → HMAC secret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The token's &lt;code&gt;alg&lt;/code&gt; value should be treated as an input to validate, not as an instruction the server must obey.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #3: Weak, guessable, or brute-forceable HMAC secrets
&lt;/h2&gt;

&lt;p&gt;HS256 security depends heavily on the secrecy and unpredictability of the HMAC key.&lt;/p&gt;

&lt;p&gt;Weak defaults are still a common problem:&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;// DON'T DO THIS&lt;/span&gt;
&lt;span class="nv"&gt;$secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'secret'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$secret&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;'JWT_SECRET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'changeme'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'my-app-jwt-key-2023'&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 who obtains a valid HS256 token can test candidate secrets &lt;strong&gt;offline&lt;/strong&gt;. There is no login endpoint or rate limiter involved in that process.&lt;/p&gt;

&lt;p&gt;If the secret is weak enough to appear in a dictionary or predictable pattern, an attacker may recover it and then generate valid tokens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix
&lt;/h3&gt;

&lt;p&gt;Generate a cryptographically random secret:&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="nb"&gt;bin2hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;random_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That produces 256 bits of random data.&lt;/p&gt;

&lt;p&gt;Store the resulting value securely rather than committing it to source control:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JWT_SECRET=&amp;lt;random-secret&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not ship weak development defaults such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;secret
changeme
password
jwt-secret
my-app-key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For architectures where multiple independent services need to verify tokens but should not be able to mint them, asymmetric signing can also reduce the blast radius of a compromised verification component because the verifier only needs the public key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #4: Missing or ignored expiration
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;exp&lt;/code&gt; claim defines when a JWT expires.&lt;/p&gt;

&lt;p&gt;For example:&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;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1234"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1750000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1750000900&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;The important distinction is that the payload itself is not trusted simply because it contains &lt;code&gt;exp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The application must verify the token's signature &lt;strong&gt;and then enforce the registered claims according to its security policy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A vulnerable implementation might simply decode the payload:&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;// DON'T DO THIS&lt;/span&gt;
&lt;span class="nv"&gt;$payload&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;base64_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$parts&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="kc"&gt;true&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="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sub'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not authentication.&lt;/p&gt;

&lt;p&gt;A secure verifier should perform cryptographic verification and enforce expiration.&lt;/p&gt;

&lt;p&gt;For example:&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;$payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'sub'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'iat'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s1"&gt;'exp'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 15 minutes&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="no"&gt;JWT&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'HS256'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;firebase/php-jwt&lt;/code&gt;, expiration is checked during decoding when the &lt;code&gt;exp&lt;/code&gt; claim is present and validation has not been disabled.&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JWT&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;decode&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'HS256'&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;\Firebase\JWT\ExpiredException&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="nf"&gt;respond_unauthorized&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;But there is an important application-level question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if &lt;code&gt;exp&lt;/code&gt; is missing?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your security model requires every access token to expire, a missing &lt;code&gt;exp&lt;/code&gt; should be rejected rather than treated as "no expiration."&lt;/p&gt;

&lt;p&gt;Short-lived access tokens combined with a separate refresh-token mechanism are generally easier to contain than long-lived access tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #5: Missing issuer and audience validation
&lt;/h2&gt;

&lt;p&gt;A token can have a perfectly valid signature and still be the wrong token for the endpoint receiving it.&lt;/p&gt;

&lt;p&gt;This matters especially in microservice architectures.&lt;/p&gt;

&lt;p&gt;Imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authentication Service
        │
        ├── issues token for billing-service
        │
        ▼
Billing API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and another service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authentication Service
        │
        └── issues token for admin-service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If multiple services trust the same signing infrastructure but don't validate the token's intended audience, a valid token issued for one service may potentially be replayed against another.&lt;/p&gt;

&lt;p&gt;A verifier that only checks the signature:&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;$decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JWT&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;decode&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'HS256'&lt;/span&gt;&lt;span class="p"&gt;)&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="nv"&gt;$decoded&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;hasn't established that the token was actually intended for this service.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix
&lt;/h3&gt;

&lt;p&gt;Include issuer and audience claims where your architecture requires 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="nv"&gt;$payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'iss'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://auth.example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'aud'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'billing-service'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'sub'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'iat'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s1"&gt;'exp'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;900&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 validate them during verification:&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="nv"&gt;$decoded&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;iss&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'https://auth.example.com'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nv"&gt;$decoded&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;aud&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'billing-service'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;respond_unauthorized&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 exact validation mechanism depends on the JWT library.&lt;/p&gt;

&lt;p&gt;The important principle is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A valid signature does not automatically mean a valid token for this service.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Bug #6: Unsafe &lt;code&gt;kid&lt;/code&gt; handling
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;kid&lt;/code&gt; header identifies a signing key.&lt;/p&gt;

&lt;p&gt;For example:&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;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RS256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"key-2026-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"typ"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JWT"&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;Using &lt;code&gt;kid&lt;/code&gt; to select among trusted keys is legitimate and common.&lt;/p&gt;

&lt;p&gt;The problem appears when an application treats the attacker-controlled value as a trusted filesystem path, database expression, URL, or query fragment.&lt;/p&gt;

&lt;p&gt;For example:&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;// DON'T DO THIS&lt;/span&gt;
&lt;span class="nv"&gt;$key&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="s2"&gt;"/keys/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'kid'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.pem"&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&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 php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// DON'T build raw queries from attacker-controlled input.&lt;/span&gt;
&lt;span class="nv"&gt;$key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"SELECT key FROM keys WHERE id = '&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'kid'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depending on the surrounding implementation, unsafe handling could create path traversal, injection, unexpected key selection, or other vulnerabilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix
&lt;/h3&gt;

&lt;p&gt;Treat &lt;code&gt;kid&lt;/code&gt; as untrusted input.&lt;/p&gt;

&lt;p&gt;Prefer a strict lookup against known identifiers:&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;$allowedKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'key-2026-01'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$rsaPublicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'key-2026-02'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$nextRsaPublicKey&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="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$allowedKeys&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$kid&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;respond_unauthorized&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;$allowedKeys&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$kid&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 database is required, use parameterized queries and constrain the lookup to the keys that the service is actually authorized to trust.&lt;/p&gt;

&lt;p&gt;Never allow an arbitrary &lt;code&gt;kid&lt;/code&gt; value to become an arbitrary filesystem path or query.&lt;/p&gt;

&lt;h2&gt;
  
  
  What your JWT library already does for you
&lt;/h2&gt;

&lt;p&gt;Modern JWT libraries handle a significant portion of the cryptographic work, but they cannot determine your application's trust model.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;firebase/php-jwt&lt;/code&gt; requires the verification key and allowed algorithm to be supplied to &lt;code&gt;decode()&lt;/code&gt;. That helps prevent applications from blindly accepting an algorithm selected by the token.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;lcobucci/jwt&lt;/code&gt; similarly uses explicit configuration for signing and validation rather than treating the token's header as an authorization decision.&lt;/p&gt;

&lt;p&gt;The important distinction is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The library can enforce cryptographic rules. It cannot decide your application's security policy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It cannot know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which issuer your application trusts;&lt;/li&gt;
&lt;li&gt;which audience this API should accept;&lt;/li&gt;
&lt;li&gt;whether every access token must contain &lt;code&gt;exp&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;how long tokens should live;&lt;/li&gt;
&lt;li&gt;which users or sessions should be revoked;&lt;/li&gt;
&lt;li&gt;which services should trust which tokens;&lt;/li&gt;
&lt;li&gt;whether a &lt;code&gt;kid&lt;/code&gt; is authorized for a particular endpoint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are application-level decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What libraries cannot fix
&lt;/h2&gt;

&lt;p&gt;A correctly configured library cannot compensate for a weak secret:&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="s1"&gt;'secret'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It cannot add an &lt;code&gt;aud&lt;/code&gt; claim that you never issued.&lt;/p&gt;

&lt;p&gt;It cannot decide that an old token should be revoked because a user's permissions changed.&lt;/p&gt;

&lt;p&gt;And it cannot protect an application if developers decode the payload manually and use its claims before verification.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Decode for inspection if you need to. Trust only after verification.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Detection as a second layer
&lt;/h1&gt;

&lt;p&gt;A correctly configured JWT implementation should prevent the known verification failures.&lt;/p&gt;

&lt;p&gt;Detection still matters because authentication systems evolve.&lt;/p&gt;

&lt;p&gt;Useful signals include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unexpected algorithm values;&lt;/li&gt;
&lt;li&gt;repeated algorithm mismatches;&lt;/li&gt;
&lt;li&gt;malformed JWT structures;&lt;/li&gt;
&lt;li&gt;unusual or invalid &lt;code&gt;kid&lt;/code&gt; values;&lt;/li&gt;
&lt;li&gt;repeated signature-verification failures from the same source;&lt;/li&gt;
&lt;li&gt;tokens with audiences that don't match the endpoint;&lt;/li&gt;
&lt;li&gt;repeated requests using expired tokens;&lt;/li&gt;
&lt;li&gt;sudden changes in authentication failure patterns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But context matters.&lt;/p&gt;

&lt;p&gt;A single failed JWT can simply be a broken client.&lt;/p&gt;

&lt;p&gt;A malformed &lt;code&gt;kid&lt;/code&gt; can be a scanner, a misconfigured integration, or a developer testing an endpoint.&lt;/p&gt;

&lt;p&gt;A burst of different algorithm values against the same authentication endpoint is more interesting because it may indicate systematic probing.&lt;/p&gt;

&lt;p&gt;The important word is &lt;strong&gt;pattern&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A detection signal is not proof of an attack, and detection is not the same as prevention.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is where an application-level security layer such as Kriosa can complement secure JWT verification.&lt;/p&gt;

&lt;h1&gt;
  
  
  See It Blocked: Try the Live Sandbox
&lt;/h1&gt;

&lt;p&gt;Reading about an &lt;code&gt;alg: none&lt;/code&gt; bypass is one thing — watching it get stopped is another. The Kriosa sandbox lets you send a forged token (unsigned, or algorithm-confused) at a Kriosa-protected auth endpoint and watch it get flagged and blocked in real time — no signup required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://kriosa.com/demo.php" rel="noopener noreferrer"&gt;Try the JWT sandbox →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  What Is Kriosa?
&lt;/h1&gt;

&lt;p&gt;Kriosa is an application-level security layer for PHP and Laravel applications.&lt;/p&gt;

&lt;p&gt;It sits at the application boundary and analyzes incoming requests for suspicious traffic before that traffic reaches sensitive application logic.&lt;/p&gt;

&lt;p&gt;For JWT-based authentication, Kriosa can provide an additional detection and visibility layer around suspicious token activity, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unexpected algorithm values;&lt;/li&gt;
&lt;li&gt;repeated malformed tokens;&lt;/li&gt;
&lt;li&gt;suspicious &lt;code&gt;kid&lt;/code&gt; patterns;&lt;/li&gt;
&lt;li&gt;authentication probing;&lt;/li&gt;
&lt;li&gt;repeated verification failures;&lt;/li&gt;
&lt;li&gt;unusual token-related request behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But Kriosa is &lt;strong&gt;not a replacement for secure JWT verification&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The primary defense remains the verification implementation itself.&lt;/p&gt;

&lt;p&gt;Your application still needs to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pin accepted algorithms.&lt;/li&gt;
&lt;li&gt;Use strong signing keys.&lt;/li&gt;
&lt;li&gt;Verify signatures correctly.&lt;/li&gt;
&lt;li&gt;Enforce expiration.&lt;/li&gt;
&lt;li&gt;Validate issuer and audience where required.&lt;/li&gt;
&lt;li&gt;Safely resolve &lt;code&gt;kid&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Implement appropriate revocation/session controls.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Kriosa adds visibility around the traffic reaching those controls.&lt;/p&gt;

&lt;h1&gt;
  
  
  How Kriosa Can Help Detect Suspicious JWT Activity
&lt;/h1&gt;

&lt;p&gt;JWT abuse is fundamentally an application-layer problem.&lt;/p&gt;

&lt;p&gt;The dangerous condition isn't simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JWT verification failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;because legitimate applications generate plenty of those.&lt;/p&gt;

&lt;p&gt;A mobile client may send an expired token.&lt;/p&gt;

&lt;p&gt;A browser may hold stale authentication state.&lt;/p&gt;

&lt;p&gt;A service integration may send a malformed token after a deployment.&lt;/p&gt;

&lt;p&gt;The more useful signal is &lt;strong&gt;behavior across requests&lt;/strong&gt;.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request 1 → alg: HS256
Request 2 → alg: RS256
Request 3 → alg: none
Request 4 → malformed kid
Request 5 → repeated signature failure
Request 6 → different malformed kid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One request may be meaningless.&lt;/p&gt;

&lt;p&gt;A sequence like this provides much more useful security telemetry.&lt;/p&gt;

&lt;p&gt;Kriosa can help developers identify, log, investigate, and respond to suspicious application-layer authentication behavior.&lt;/p&gt;

&lt;p&gt;The distinction remains important:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Detection is not prevention.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If JWT verification is broken, the verification code needs to be fixed.&lt;/p&gt;

&lt;h1&gt;
  
  
  Prevention Comes First
&lt;/h1&gt;

&lt;p&gt;A secure JWT implementation should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pin accepted verification algorithms server-side.&lt;/li&gt;
&lt;li&gt;Explicitly reject algorithms your service does not support.&lt;/li&gt;
&lt;li&gt;Never derive the verification algorithm from the token alone.&lt;/li&gt;
&lt;li&gt;Use cryptographically random HMAC secrets when HMAC is appropriate.&lt;/li&gt;
&lt;li&gt;Keep signing secrets outside source control.&lt;/li&gt;
&lt;li&gt;Use asymmetric signing when its trust model fits the architecture.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;exp&lt;/code&gt; on access tokens and enforce it.&lt;/li&gt;
&lt;li&gt;Validate &lt;code&gt;iss&lt;/code&gt; and &lt;code&gt;aud&lt;/code&gt; where tokens cross trust boundaries.&lt;/li&gt;
&lt;li&gt;Validate &lt;code&gt;kid&lt;/code&gt; against trusted key identifiers.&lt;/li&gt;
&lt;li&gt;Use short-lived access tokens where appropriate.&lt;/li&gt;
&lt;li&gt;Use refresh-token mechanisms for longer sessions.&lt;/li&gt;
&lt;li&gt;Provide a revocation strategy appropriate to the application.&lt;/li&gt;
&lt;li&gt;Log and monitor meaningful authentication failures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These controls address the vulnerability itself.&lt;/p&gt;

&lt;h1&gt;
  
  
  Detection Adds Another Layer
&lt;/h1&gt;

&lt;p&gt;Security controls can fail during future development.&lt;/p&gt;

&lt;p&gt;A developer can introduce dynamic algorithm selection.&lt;/p&gt;

&lt;p&gt;A new microservice can be added without updating audience validation.&lt;/p&gt;

&lt;p&gt;A key rotation can introduce unsafe &lt;code&gt;kid&lt;/code&gt; handling.&lt;/p&gt;

&lt;p&gt;A configuration change can accidentally restore a weak development secret.&lt;/p&gt;

&lt;p&gt;A library upgrade can change behavior that developers previously relied upon.&lt;/p&gt;

&lt;p&gt;That's why defense in depth matters.&lt;/p&gt;

&lt;p&gt;A practical security model can look like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secure verification → Claim validation → Detection → Logging &amp;amp; response&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kriosa fits into the detection layer.&lt;/p&gt;

&lt;p&gt;Your verification logic should prevent the vulnerability.&lt;/p&gt;

&lt;p&gt;Your claim validation should reject expired, misscoped, or otherwise unacceptable tokens.&lt;/p&gt;

&lt;p&gt;Kriosa can provide additional visibility into suspicious application traffic.&lt;/p&gt;

&lt;p&gt;Your logging and monitoring infrastructure can help your team investigate and respond.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kriosa does not make broken JWT verification safe. It adds another layer of visibility around the traffic attempting to reach it.&lt;/strong&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  JWT Security Checklist
&lt;/h1&gt;

&lt;p&gt;Before shipping or auditing a JWT-based authentication flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Is the accepted algorithm pinned server-side?&lt;/li&gt;
&lt;li&gt;[ ] Is the token's &lt;code&gt;alg&lt;/code&gt; value treated as untrusted input?&lt;/li&gt;
&lt;li&gt;[ ] Are unsupported algorithms explicitly rejected?&lt;/li&gt;
&lt;li&gt;[ ] Is &lt;code&gt;none&lt;/code&gt; prevented from becoming an unintended authentication bypass?&lt;/li&gt;
&lt;li&gt;[ ] If using asymmetric signing, can an RSA/EC public key ever be interpreted as an HMAC secret?&lt;/li&gt;
&lt;li&gt;[ ] Is the HMAC secret cryptographically random and sufficiently strong?&lt;/li&gt;
&lt;li&gt;[ ] Is the secret stored outside source control?&lt;/li&gt;
&lt;li&gt;[ ] Is &lt;code&gt;exp&lt;/code&gt; present on every token that is supposed to expire?&lt;/li&gt;
&lt;li&gt;[ ] Is &lt;code&gt;exp&lt;/code&gt; actually enforced by the verification flow?&lt;/li&gt;
&lt;li&gt;[ ] Are missing expiration claims rejected when expiration is mandatory?&lt;/li&gt;
&lt;li&gt;[ ] Are &lt;code&gt;iss&lt;/code&gt; and &lt;code&gt;aud&lt;/code&gt; validated where the architecture requires them?&lt;/li&gt;
&lt;li&gt;[ ] Is &lt;code&gt;kid&lt;/code&gt; validated against a strict allowlist of known key identifiers before being used to look up a key?&lt;/li&gt;
&lt;li&gt;[ ] Are access tokens short-lived, with a separate refresh-token mechanism for longer sessions?&lt;/li&gt;
&lt;li&gt;[ ] Is there a revocation strategy for tokens that need to be invalidated before their natural expiration?&lt;/li&gt;
&lt;li&gt;[ ] Are verification failures — especially algorithm mismatches and malformed &lt;code&gt;kid&lt;/code&gt; values — logged and monitored?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't to trust a signature because a library returned &lt;code&gt;true&lt;/code&gt;. It's to make sure that &lt;code&gt;true&lt;/code&gt; only happens when the algorithm, the key, the expiration, and the intended audience all agree with what your application actually issued and actually expects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try Kriosa
&lt;/h2&gt;

&lt;p&gt;If you want an additional application-level security layer for your PHP or Laravel application:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;See it stop a JWT bypass attempt yourself:&lt;/strong&gt; &lt;a href="https://kriosa.com/demo.php" rel="noopener noreferrer"&gt;Try the live sandbox →&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Install it with Composer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require kriosa-ai/kriosa-php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Documentation:&lt;/strong&gt; &lt;a href="https://kriosa.com/documentation.php" rel="noopener noreferrer"&gt;Kriosa 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;em&gt;Kriosa — sleep better, we're awake.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>php</category>
      <category>laravel</category>
      <category>security</category>
    </item>
    <item>
      <title>SSRF: The Vulnerability Hiding in Every Paste a URL Feature</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Tue, 15 Sep 2026 09:37:42 +0000</pubDate>
      <link>https://dev.to/kriosa/ssrf-the-vulnerability-hiding-in-every-paste-a-url-feature-4jpd</link>
      <guid>https://dev.to/kriosa/ssrf-the-vulnerability-hiding-in-every-paste-a-url-feature-4jpd</guid>
      <description>&lt;p&gt;Originally published on Kriosa&lt;/p&gt;

&lt;p&gt;How server-side URL fetching can expose internal services  and how to build these features without turning your server into an attacker's proxy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is everywhere now
&lt;/h2&gt;

&lt;p&gt;Ten years ago SSRF was a niche finding buried in the appendix of a pentest report. Today it's one of the highest-impact bugs in the OWASP Top 10, and the reason is boring: modern apps fetch URLs &lt;em&gt;on the user's behalf&lt;/em&gt;, constantly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks&lt;/strong&gt; — "give us a URL and we'll POST to it when something happens."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image proxies / avatar uploaders&lt;/strong&gt; — "paste a link to your profile picture."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF and screenshot generation&lt;/strong&gt; — "render this URL as a PDF/image for the user."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link previews&lt;/strong&gt; — "paste a link in chat and we'll show a rich preview card."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these features has the same shape: a user supplies a URL, and the &lt;em&gt;server&lt;/em&gt;  not the user's browser fetches it. That's the entire vulnerability class in one sentence. The server has network access the user doesn't: internal services, admin panels, databases on private IPs, and — on every major cloud provider  a metadata endpoint that hands out credentials to anyone who can reach it.&lt;/p&gt;

&lt;p&gt;A previous piece on this account touched on SSRF only in passing, as a side note inside a discussion of insecure deserialization. It deserves its own treatment, because it doesn't require deserializing anything, doesn't require a memory-corruption bug, and doesn't even require authentication in a lot of real-world cases. It just requires a text box that accepts a URL and a server willing to fetch it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The attack narrative
&lt;/h2&gt;

&lt;p&gt;Say your app has an image proxy: paste a URL, the server fetches it, resizes it, and serves it back. The naive implementation:&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;// DON'T DO THIS&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="nv"&gt;$image&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="nv"&gt;$url&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: image/jpeg'&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;$image&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works perfectly for &lt;code&gt;https://example.com/cat.jpg&lt;/code&gt;. It also works perfectly for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://169.254.169.254/latest/meta-data/iam/security-credentials/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;169.254.169.254&lt;/code&gt; is the &lt;strong&gt;cloud instance metadata endpoint&lt;/strong&gt; — on AWS, GCP, Azure, and most others, any process on the instance (including, in this case, &lt;em&gt;your web server acting on the attacker's behalf&lt;/em&gt;) can query it without authentication. On AWS specifically, that endpoint can hand back temporary IAM credentials for whatever role the instance is running as. If that role has S3, RDS, or Lambda permissions, the attacker didn't just read an image — they now have working AWS credentials, exfiltrated through your image proxy, without ever touching your app's actual authentication.&lt;/p&gt;

&lt;p&gt;Even without a cloud metadata endpoint in play, the same request can be pointed at:&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;http&lt;/span&gt;://&lt;span class="n"&gt;localhost&lt;/span&gt;:&lt;span class="m"&gt;6379&lt;/span&gt;/          &lt;span class="c"&gt;# an unauthenticated Redis instance
&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;://&lt;span class="m"&gt;192&lt;/span&gt;.&lt;span class="m"&gt;168&lt;/span&gt;.&lt;span class="m"&gt;1&lt;/span&gt;.&lt;span class="m"&gt;50&lt;/span&gt;:&lt;span class="m"&gt;8080&lt;/span&gt;/&lt;span class="n"&gt;admin&lt;/span&gt;  &lt;span class="c"&gt;# an internal admin panel with no auth wall,
&lt;/span&gt;                                 &lt;span class="c"&gt;# because "it's internal, nobody can reach it"
&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;://&lt;span class="m"&gt;10&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;5&lt;/span&gt;:&lt;span class="m"&gt;5432&lt;/span&gt;/           &lt;span class="c"&gt;# a database port, for banner-grabbing / probing
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of this requires the attacker to be on your network. It requires your server to make one HTTP request on their behalf, to an address they chose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where SSRF actually hides
&lt;/h2&gt;

&lt;p&gt;It's rarely the obvious "image proxy" example in a real codebase. It's usually one of these, added months apart by different people, none of whom were thinking about the other three:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Webhook registration&lt;/strong&gt; — a user configures a callback URL, and your job queue fetches it later, with no user present to notice anything odd about where the request goes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF/screenshot generation&lt;/strong&gt; — "render this invoice as a PDF" often means spinning up a headless browser and pointing it at a URL. A headless Chrome instance following &lt;code&gt;window.location&lt;/code&gt; redirects is a full SSRF engine with a UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link unfurling&lt;/strong&gt; — chat apps, note-taking tools, and CMSs that show a title/image/description for a pasted link all fetch that link server-side, unauthenticated, the instant it's pasted — often before the user even sends the message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Import from URL"&lt;/strong&gt; — CSV imports, avatar-from-URL, "sync this doc from a link" — any feature phrased as &lt;em&gt;fetch this and do something with it&lt;/em&gt; is a candidate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can grep your codebase for &lt;code&gt;curl_exec&lt;/code&gt;, &lt;code&gt;file_get_contents(&lt;/code&gt;, &lt;code&gt;Http::get(&lt;/code&gt;, &lt;code&gt;GuzzleHttp\Client&lt;/code&gt;, or a headless-browser &lt;code&gt;goto()&lt;/code&gt;/&lt;code&gt;-&amp;gt;visit()&lt;/code&gt; call and find one where the URL traces back to user input, you've found a candidate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the obvious fixes don't work
&lt;/h2&gt;

&lt;p&gt;The first instinct is usually a blocklist: reject &lt;code&gt;localhost&lt;/code&gt;, &lt;code&gt;127.0.0.1&lt;/code&gt;, and private IP ranges. This is necessary but nowhere near sufficient, and it's worth walking through why, because every bypass here has shown up in real bug bounty reports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encoding tricks.&lt;/strong&gt; &lt;code&gt;127.0.0.1&lt;/code&gt; has a lot of equivalent spellings a naive string-match blocklist won't catch:&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;http&lt;/span&gt;://&lt;span class="m"&gt;2130706433&lt;/span&gt;/           &lt;span class="c"&gt;# decimal IP encoding of 127.0.0.1
&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;://&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="n"&gt;x7f000001&lt;/span&gt;/           &lt;span class="c"&gt;# hex encoding
&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;://&lt;span class="m"&gt;0177&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="c"&gt;# octal encoding
&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;://&lt;span class="m"&gt;127&lt;/span&gt;.&lt;span class="m"&gt;1&lt;/span&gt;/                &lt;span class="c"&gt;# shorthand form
&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;://[::&lt;span class="n"&gt;ffff&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="c"&gt;# IPv4-mapped IPv6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Redirects.&lt;/strong&gt; Even if the &lt;em&gt;initial&lt;/em&gt; URL passes validation, an attacker-controlled server can respond with a 302 to an internal address:&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="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/redirect-me&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;attacker.com&lt;/span&gt;

HTTP/1.1 302 Found
Location: http://169.254.169.254/latest/meta-data/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your HTTP client follows redirects by default (most do, including Guzzle and cURL), you validated a URL that was never actually fetched — the one that got fetched was chosen by the attacker's server &lt;em&gt;after&lt;/em&gt; your check ran.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DNS rebinding.&lt;/strong&gt; This is the one that breaks "resolve the hostname, check the IP, then fetch it" logic specifically. An attacker sets up a domain with a very short DNS TTL. The first resolution — the one your validation code sees — returns a public, harmless IP. By the time the actual HTTP client re-resolves the same hostname a moment later to make the connection, the DNS record has changed to point at &lt;code&gt;169.254.169.254&lt;/code&gt; or an internal address. The check and the fetch ran against two different IPs for the same hostname, because DNS resolution isn't pinned between them.&lt;/p&gt;

&lt;p&gt;None of these are exotic. They're the standard toolkit, and a blocklist-only defense falls to at least one of them almost every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building it correctly
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Prefer an allowlist over a blocklist wherever the feature allows it.&lt;/strong&gt; If your image proxy only ever needs to fetch from a handful of known CDNs, allowlist those hostnames explicitly. This closes the entire class of bypass above in one step, because there's no "everything except X" logic to route around.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. When an allowlist isn't possible, validate the IP you're actually connecting to — not the hostname you started with.&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;isBlockedIp&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;$ip&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;// Reject private, loopback, link-local, and reserved ranges —&lt;/span&gt;
    &lt;span class="c1"&gt;// this must cover IPv6 equivalents too, not just IPv4.&lt;/span&gt;
    &lt;span class="nv"&gt;$blocked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'127.0.0.0/8'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'10.0.0.0/8'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'172.16.0.0/12'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'192.168.0.0/16'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'169.254.0.0/16'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// link-local — this is what covers the cloud&lt;/span&gt;
                            &lt;span class="c1"&gt;// metadata endpoint on AWS/GCP/Azure&lt;/span&gt;
        &lt;span class="s1"&gt;'::1/128'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'fc00::/7'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'fe80::/10'&lt;/span&gt;&lt;span class="p"&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;$blocked&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$range&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;ipInRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$range&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Resolve the hostname, validate the resulting address, and make sure the HTTP client actually connects using that validated address — not a fresh resolution of its own.&lt;/strong&gt; This is the part that closes DNS rebinding: the gap isn't "did you check the IP," it's "did the connection use the IP you checked." A simple &lt;code&gt;gethostbyname()&lt;/code&gt; call is a starting point, but it isn't the whole fix on its own — it only returns an IPv4 address, so it does nothing for IPv6, and calling it doesn't guarantee anything about which address your HTTP client ends up connecting to if that client performs its own independent DNS lookup afterward. In production, use an HTTP client or network layer that lets you control DNS resolution directly, or otherwise pin the destination for the connection — for example, a custom resolver/handler in your HTTP client, or a &lt;code&gt;curl&lt;/code&gt; &lt;code&gt;CURLOPT_RESOLVE&lt;/code&gt; mapping — so the same address you validated is the one actually opened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Disable automatic redirect-following, or validate every hop.&lt;/strong&gt; With Guzzle:&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;$client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GET'&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="s1"&gt;'allow_redirects'&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="c1"&gt;// handle redirects yourself, re-validating&lt;/span&gt;
                                 &lt;span class="c1"&gt;// the Location header through the same&lt;/span&gt;
                                 &lt;span class="c1"&gt;// isBlockedIp() check before following it&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;5. Restrict scheme and port.&lt;/strong&gt; &lt;code&gt;file://&lt;/code&gt;, &lt;code&gt;gopher://&lt;/code&gt;, and &lt;code&gt;dict://&lt;/code&gt; handlers in some HTTP libraries can be abused for local file reads or protocol smuggling if a URL parser accepts them at all. Explicitly allow only &lt;code&gt;http&lt;/code&gt;/&lt;code&gt;https&lt;/code&gt;, and consider restricting to standard ports (80/443) unless the feature genuinely needs otherwise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. For anything render-heavy (headless browser PDF/screenshot generation), run it in a network-isolated environment.&lt;/strong&gt; A headless Chrome instance that can reach your internal network is SSRF with JavaScript execution attached. Give it its own egress rules, separate from the rest of your infrastructure, and block the metadata endpoint at the network layer (or require IMDSv2 on AWS, which requires a token from a PUT request that most simple SSRF payloads can't perform) as a second line of defense.&lt;/p&gt;

&lt;p&gt;The pattern across all of this: &lt;strong&gt;validate what you're actually going to connect to, at the moment you connect to it — not a proxy for that (the hostname, the first DNS answer, the first URL before a redirect).&lt;/strong&gt; Every SSRF bypass technique above is a variation on attacking the gap between what got validated and what got fetched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detection, as a second layer
&lt;/h2&gt;

&lt;p&gt;Even a correctly built fetcher benefits from visibility into who's testing its edges. Patterns worth flagging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repeated requests where the target resolves to a private, loopback, or link-local range, especially &lt;code&gt;169.254.169.254&lt;/code&gt; specifically.&lt;/li&gt;
&lt;li&gt;A burst of URLs using IP-encoding tricks (decimal, hex, octal, IPv6-mapped) against the same feature in a short window — a strong signal of deliberate blocklist probing rather than a legitimate one-off.&lt;/li&gt;
&lt;li&gt;A webhook or import URL that resolves differently on successive requests in a way consistent with DNS rebinding (same hostname, IP changing between requests).&lt;/li&gt;
&lt;li&gt;Fetches to non-standard ports or non-HTTP(S) schemes on an endpoint that should only ever see image or document URLs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is exactly the kind of traffic pattern a detection layer like Kriosa is positioned to flag. Kriosa can help detect and surface SSRF probing, but it does not replace destination validation, redirect controls, DNS handling, or network-level isolation — it watches for the probing that suggests someone is testing whether those controls can be bypassed, after they're in place, not instead of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Kriosa?
&lt;/h2&gt;

&lt;p&gt;Kriosa is an application-level security layer for PHP and Laravel applications. It sits at the application boundary and analyzes incoming requests for suspicious traffic before that traffic reaches sensitive application logic.&lt;/p&gt;

&lt;p&gt;For features that fetch a user-supplied URL webhooks, image proxies, PDF/screenshot generation, link previews Kriosa provides an additional detection and visibility layer by helping surface suspicious request patterns associated with attempts to probe, bypass, or abuse those fetches.&lt;/p&gt;

&lt;p&gt;But Kriosa is &lt;strong&gt;not a replacement for building the fetch correctly&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If your application exposes any feature where the server requests a URL on a user's behalf, the primary fix is to get that fetch itself right allowlisting where possible, validating the resolved address rather than just the hostname, pinning the connection to that validated address, disabling or re-validating redirects, and restricting scheme and port.&lt;/p&gt;

&lt;p&gt;Think of Kriosa as &lt;strong&gt;defense in depth&lt;/strong&gt;, not a substitute for a correctly implemented fetch.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Kriosa Can Help Detect Suspicious SSRF Activity
&lt;/h3&gt;

&lt;p&gt;SSRF is fundamentally an application-layer problem.&lt;/p&gt;

&lt;p&gt;The dangerous condition is not simply that someone submitted a URL to be fetched. Legitimate users paste webhook URLs, image links, and documents to render constantly, and a single fetch is completely normal.&lt;/p&gt;

&lt;p&gt;The real problem is when a pattern of requests suggests someone is &lt;strong&gt;probing the fetch itself&lt;/strong&gt; — testing IP-encoding tricks, chaining redirects toward internal addresses, exploiting DNS rebinding, or repeatedly targeting the cloud metadata endpoint.&lt;/p&gt;

&lt;p&gt;That makes detection useful as an additional layer, but it does not change where the vulnerability must ultimately be fixed: in the fetch logic itself.&lt;/p&gt;

&lt;p&gt;Patterns worth investigating include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- URLs resolving to private, loopback, or link-local ranges, especially 169.254.169.254
- A burst of decimal/hex/octal/IPv6-mapped IP encodings against the same feature
- The same hostname resolving to different IPs across successive requests (rebinding)
- Fetches to non-standard ports or non-HTTP(S) schemes on an image/document-only endpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, a burst of requests carrying encoded loopback addresses against a single image-proxy endpoint may be worth investigating even though no individual request looks obviously malicious.&lt;/p&gt;

&lt;p&gt;The important word is &lt;strong&gt;pattern&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A single unusual URL is not automatically an attack. Context and volume matter.&lt;/p&gt;

&lt;p&gt;Kriosa can add another layer of visibility by helping developers identify suspicious request patterns, unusual redirect chains, and repeated probing of URL-fetching endpoints.&lt;/p&gt;

&lt;p&gt;The distinction is important:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A detection signal is not proof of an attack, and detection is not the same as prevention.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Prevention Comes First
&lt;/h3&gt;

&lt;p&gt;The application should still:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prefer an allowlist of known hostnames over a blocklist wherever the feature allows it.&lt;/li&gt;
&lt;li&gt;Validate the IP actually being connected to, not just the original hostname.&lt;/li&gt;
&lt;li&gt;Pin the resolved address between validation and connection so DNS can't change underneath the check.&lt;/li&gt;
&lt;li&gt;Disable automatic redirect-following, or re-validate every hop before following it.&lt;/li&gt;
&lt;li&gt;Restrict the URL scheme to &lt;code&gt;http&lt;/code&gt;/&lt;code&gt;https&lt;/code&gt; only, and consider restricting ports.&lt;/li&gt;
&lt;li&gt;Run any headless-browser rendering (PDF/screenshot generation) in a network-isolated environment.&lt;/li&gt;
&lt;li&gt;Block the cloud metadata endpoint at the network layer, or require a token-based metadata API (like IMDSv2 on AWS), as a second line of defense.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These controls address the vulnerability itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Detection Adds Another Layer
&lt;/h3&gt;

&lt;p&gt;A correctly built fetcher closes off the known failure modes, but it does not guarantee that every future change to the feature preserves that correctness.&lt;/p&gt;

&lt;p&gt;An attacker sweeping a feature with encoded IPs, or repeatedly chaining redirects toward internal addresses, may be attempting to find a gap in the implementation  or attempting to reach the metadata endpoint live.&lt;/p&gt;

&lt;p&gt;That activity is useful security telemetry.&lt;/p&gt;

&lt;p&gt;Kriosa is designed to provide additional application-level protection and visibility into suspicious traffic helping developers &lt;strong&gt;detect, log, investigate, and respond to activity that may indicate an SSRF attempt&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Even a well-built Guzzle or cURL setup won't, on its own, flag a burst of probing across dozens of encoding variants against the same endpoint — that's a pattern-over-time observation, not a single-request validation decision, and it's exactly the kind of traffic pattern a detection layer is positioned to flag.&lt;/p&gt;

&lt;p&gt;The goal is not to make an unvalidated fetch safe.&lt;/p&gt;

&lt;p&gt;The goal is to &lt;strong&gt;get the fetch right first, then add visibility around the traffic attempting to reach it.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Kriosa?
&lt;/h3&gt;

&lt;p&gt;Security controls can fail.&lt;/p&gt;

&lt;p&gt;A developer can add a new "import from URL" feature months after the original allowlist was written and forget to route it through the same validation. A proxy change can stop stripping a forged header that redirect-validation logic relied on. A dependency upgrade can change an HTTP client's default redirect behavior without anyone noticing. A headless-browser feature can get added without anyone considering its network access is a fetch surface too.&lt;/p&gt;

&lt;p&gt;That is why defense in depth matters.&lt;/p&gt;

&lt;p&gt;A practical security model can look like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secure fetch → Destination validation → Detection → Logging &amp;amp; response&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kriosa fits into the detection layer.&lt;/p&gt;

&lt;p&gt;Your fetch logic should prevent the vulnerability.&lt;/p&gt;

&lt;p&gt;Your validation should reject unresolved, private, or malformed destinations.&lt;/p&gt;

&lt;p&gt;Kriosa can provide additional visibility into suspicious application traffic.&lt;/p&gt;

&lt;p&gt;Your logs and monitoring can help you investigate and respond.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kriosa does not replace a secure fetch. It adds another layer for detecting and monitoring the traffic that reaches it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  SSRF Prevention Checklist
&lt;/h2&gt;

&lt;p&gt;Before shipping any feature where the server fetches a user-supplied URL:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Can this be an allowlist of known hostnames instead of a blocklist? If yes, do that and skip the rest of this list.&lt;/li&gt;
&lt;li&gt;[ ] Is every private, loopback, link-local (&lt;code&gt;169.254.0.0/16&lt;/code&gt;), and reserved IP range blocked — for both IPv4 and IPv6?&lt;/li&gt;
&lt;li&gt;[ ] Is the IP actually being connected to validated, not just the original hostname?&lt;/li&gt;
&lt;li&gt;[ ] Is the resolved IP pinned between validation and connection, so DNS can't change underneath the check (rebinding)?&lt;/li&gt;
&lt;li&gt;[ ] Is automatic redirect-following disabled, or is every redirect hop re-validated before being followed?&lt;/li&gt;
&lt;li&gt;[ ] Is the URL scheme restricted to &lt;code&gt;http&lt;/code&gt;/&lt;code&gt;https&lt;/code&gt; only — no &lt;code&gt;file://&lt;/code&gt;, &lt;code&gt;gopher://&lt;/code&gt;, &lt;code&gt;dict://&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;[ ] Are decimal/hex/octal/IPv6-mapped IP encodings normalized before the blocklist check runs, not bypassed by them?&lt;/li&gt;
&lt;li&gt;[ ] If this feature uses a headless browser (PDF/screenshot generation), does it run in a network-isolated environment with its own egress rules?&lt;/li&gt;
&lt;li&gt;[ ] Is the cloud metadata endpoint blocked at the network layer, or is IMDSv2 (token-required) enforced, as defense in depth beyond the application-level check?&lt;/li&gt;
&lt;li&gt;[ ] Are requests to internal ranges or the metadata endpoint logged and monitored, even when blocked?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't to write one clever check. It's to make sure that at the exact moment your server opens a connection, it's opening it to an address the &lt;em&gt;application&lt;/em&gt; chose to trust — not an address an attacker steered it toward through a redirect, a DNS trick, or an encoding quirk.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try Kriosa
&lt;/h2&gt;

&lt;p&gt;If you want an additional application-level security layer for your PHP or Laravel application:&lt;/p&gt;

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

&lt;p&gt;Install it with Composer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require kriosa-ai/kriosa-php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Documentation:&lt;/strong&gt; &lt;a href="https://kriosa.com/documentation.php" rel="noopener noreferrer"&gt;Kriosa 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;em&gt;Kriosa — sleep better, we're awake.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>php</category>
      <category>security</category>
      <category>laravel</category>
    </item>
    <item>
      <title>The Password Reset Flow: Where Good PHP &amp; Laravel Developers Ship Bad Security</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Fri, 11 Sep 2026 16:56:43 +0000</pubDate>
      <link>https://dev.to/kriosa/the-password-reset-flow-where-good-php-laravel-developers-ship-bad-security-3p2h</link>
      <guid>https://dev.to/kriosa/the-password-reset-flow-where-good-php-laravel-developers-ship-bad-security-3p2h</guid>
      <description>&lt;p&gt;A synthesis of secure token generation, constant-time comparison, rate limiting, and PHP's type system applied to the one feature that, when broken, can hand over an account.&lt;/p&gt;

&lt;p&gt;Every PHP course teaches these concepts in isolation: "use &lt;code&gt;random_bytes()&lt;/code&gt; for tokens," "use &lt;code&gt;hash_equals()&lt;/code&gt; for comparisons," "rate limit your endpoints," "watch out for type juggling with &lt;code&gt;==&lt;/code&gt;." They're presented as separate lessons, separate quiz questions, separate checklist items.&lt;/p&gt;

&lt;p&gt;Then a developer sits down to build "forgot password," combines all four incorrectly, and ships an account takeover vulnerability that passes code review because each individual line looks fine.&lt;/p&gt;

&lt;p&gt;Password reset is the perfect case study because it's small enough to fit in one article and high-stakes enough that every shortcut has a name and a CVE history. Almost every step in the flow has a security failure mode. Let's break it seven different ways before we get it right — then look at how much of this Laravel already solves for you, and where it doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flow, in theory
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;User submits their email.&lt;/li&gt;
&lt;li&gt;Server generates a reset token, stores it (hashed) with an expiration, emails a link containing the token.&lt;/li&gt;
&lt;li&gt;User clicks the link.&lt;/li&gt;
&lt;li&gt;Server validates the token: does it exist, does it match, has it expired, has it been used.&lt;/li&gt;
&lt;li&gt;User sets a new password.&lt;/li&gt;
&lt;li&gt;Server invalidates the token and (ideally) every existing session.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Bug #1: The predictable token
&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;// DON'T DO THIS&lt;/span&gt;
&lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;md5&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="mf"&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;'email'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks like "generating something unique," and it compiles, and it works in the demo. It is also completely guessable. &lt;code&gt;time()&lt;/code&gt; has second-level resolution — an attacker targeting a known email address can brute-force the exact timestamp window in seconds, especially if they can trigger the reset themselves and observe roughly when it was generated. &lt;code&gt;md5()&lt;/code&gt; of a short, mostly-known input space is not cryptographic protection; it's obfuscation with extra steps.&lt;/p&gt;

&lt;p&gt;The same failure shows up with &lt;code&gt;uniqid()&lt;/code&gt;, &lt;code&gt;mt_rand()&lt;/code&gt;, or anything seeded from predictable state. &lt;code&gt;uniqid()&lt;/code&gt; is explicitly documented as not suitable for security purposes — it encodes the current time in microseconds, and even &lt;code&gt;uniqid(..., true)&lt;/code&gt; (the "more entropy" flag) is guessable enough that it should never appear in an auth flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&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;$token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;bin2hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;random_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 64 hex characters, CSPRNG&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;random_bytes()&lt;/code&gt; pulls from the OS's cryptographically secure random source. 32 bytes gives you 256 bits of entropy — nobody is brute-forcing that within the token's lifetime, full stop. This is the same primitive you'd use for session IDs or CSRF tokens, because it's solving the same problem: produce a value an attacker cannot predict or reproduce.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #2: Storing the token in plaintext
&lt;/h2&gt;

&lt;p&gt;Say you got step one right — the token is unguessable. Now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;"INSERT INTO password_resets (email, token, expires_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;$email&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="nv"&gt;$expiresAt&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The token sits in your &lt;code&gt;password_resets&lt;/code&gt; table exactly as it was emailed. If that table leaks — SQL injection elsewhere in the app, a misconfigured backup, a careless &lt;code&gt;SELECT *&lt;/code&gt; logged somewhere — every unexpired reset token is immediately usable by whoever has the dump. There's no reason for the server to ever need the raw token again after emailing it; it only needs to verify that what the user presents matches what it issued.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; hash the token before storing it, the same way you'd never store a plaintext password, and validate the shape of whatever comes back from the client before you touch it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;bin2hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;random_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nv"&gt;$tokenHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha256'&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="nv"&gt;$stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;"INSERT INTO password_resets (email, token_hash, expires_at, used) VALUES (?, ?, ?, 0)"&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;$email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$tokenHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$expiresAt&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// Only the raw $token goes in the emailed URL - never stored anywhere.&lt;/span&gt;
&lt;span class="nf"&gt;mail_reset_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$email&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="c1"&gt;// On the incoming request - before it ever reaches a query or a hash function&lt;/span&gt;
&lt;span class="nv"&gt;$submittedToken&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="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;$submittedToken&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;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/^[a-f0-9]{64}$/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$submittedToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;respond_invalid_or_expired&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;That validation step matters for its own reasons — it stops a stray array (&lt;code&gt;token[]=x&lt;/code&gt;) or an unexpected type from ever reaching your comparison or database logic, closing off a whole category of "well, that shouldn't have been possible" bugs before they start.&lt;/p&gt;

&lt;p&gt;Notice the storage choice is deliberately not &lt;code&gt;password_hash()&lt;/code&gt; / bcrypt. Bcrypt is designed to be slow, to resist offline brute-forcing of low-entropy human passwords. A reset token already has 256 bits of entropy — a fast hash like SHA-256 is appropriate and correct here; the security comes from the token's randomness, not from hashing cost. Using bcrypt on a token you're about to look up by exact match is a sign the two concepts (password hashing vs. token fingerprinting) got merged incorrectly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #3: Comparing tokens with &lt;code&gt;==&lt;/code&gt; instead of &lt;code&gt;hash_equals()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Here's the part where "use &lt;code&gt;hash_equals()&lt;/code&gt;" from the course actually matters — though it's worth being precise about why, because it's easy to overstate this one.&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;// DON'T DO THIS&lt;/span&gt;
&lt;span class="nv"&gt;$row&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="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'token_hash'&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&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$submittedToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// proceed with reset&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;Type juggling.&lt;/strong&gt; PHP's &lt;code&gt;==&lt;/code&gt; performs type coercion before comparing. In this particular line, both sides are normally well-formed hex strings, so &lt;code&gt;==&lt;/code&gt; doesn't hand an attacker an automatic bypass the way it famously did with naive &lt;code&gt;md5()&lt;/code&gt; password checks using PHP's "magic hash" quirk (two different hashes that both look like scientific notation for zero comparing as equal under &lt;code&gt;==&lt;/code&gt;). But that's a fact about this specific data shape, not a reason to rely on it — the input validation from Bug #2 is what actually forecloses a type-confusion path (an array or unexpected type slipping through), and &lt;code&gt;==&lt;/code&gt; is still the wrong operator on principle for comparing secret-derived values. Use &lt;code&gt;===&lt;/code&gt; or &lt;code&gt;hash_equals()&lt;/code&gt;, never &lt;code&gt;==&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timing.&lt;/strong&gt; Switching to strict equality raises a subtler question:&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="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'token_hash'&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&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$submittedToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="mf"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PHP's native string comparison short-circuits on the first mismatched byte, so &lt;code&gt;===&lt;/code&gt; can leak, via response time, how many leading characters of the two hashes matched. It's worth being precise about what that actually exposes: the attacker would be learning about the SHA-256 digest, not recovering your 256-bit random token — directly reversing a hash from partial timing leakage is still computationally infeasible. So this isn't "the primary way an attacker steals your reset token" the way a guessable token generator (Bug #1) is. It's a defense-in-depth concern — a side channel that shouldn't exist, on principle, even though exploiting it here would be extraordinarily impractical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix, regardless:&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;hash_equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'token_hash'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$submittedToken&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// proceed&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; always compares the full length of both strings regardless of where they first differ, so no timing information leaks at all. The right way to think about it: &lt;code&gt;hash_equals()&lt;/code&gt; is the correct, cost-free primitive for comparing anything secret-derived — use it by default rather than reasoning case-by-case about whether a particular leak is currently exploitable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #4: No expiration, or expiration checked wrong
&lt;/h2&gt;

&lt;p&gt;A token that's cryptographically strong and safely compared is still dangerous if it works forever — and the lookup pattern matters too. Rather than fetching a reset record by email and then comparing the submitted token against it, look the record up by the token's hash 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="nv"&gt;$submittedToken&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="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;$submittedToken&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;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/^[a-f0-9]{64}$/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$submittedToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;respond_invalid_or_expired&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$tokenHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$submittedToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;"SELECT id, email, token_hash, expires_at, used
   FROM password_resets
   WHERE token_hash = ?
   LIMIT 1"&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;$tokenHash&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nv"&gt;$row&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;$row&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'used'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;strtotime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'expires_at'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&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="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;$row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'token_hash'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$tokenHash&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;// Generic failure - see Bug #7 for why the wording matters&lt;/span&gt;
    &lt;span class="nf"&gt;respond_invalid_or_expired&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 flow becomes token → hash → find record → validate state, instead of email → find record → compare token. It's a small restructuring, but it means a reset link is self-contained: whoever holds the token can look up exactly one record, rather than the query being organized around an email address that isn't even part of what the user presented.&lt;/p&gt;

&lt;p&gt;Short expiration windows (15–30 minutes is typical) matter independently of this — reset links get forwarded in email chains, sit in inboxes for months, get pre-fetched by corporate email security scanners that visit every link in an inbox (a real, common cause of tokens getting "used" by a bot before the actual user clicks). An unexpired token from six months ago is an open door regardless of how well everything else is built.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #5: Token reuse — forgetting the token is a one-time credential
&lt;/h2&gt;

&lt;p&gt;Even with expiration, a token is often valid for its entire window and checked only for "does it match and hasn't it expired" — never "has it already been consumed." That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A token forwarded to a colleague, or intercepted once, works repeatedly until it expires.&lt;/li&gt;
&lt;li&gt;If the reset email is exposed in a proxy log, browser history on a shared machine, or a referrer header leak, anyone with the link can reset the password again later, even after the legitimate user already reset it once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; mark it used atomically, in the same statement that establishes you're allowed to proceed — this is the part worth internalizing beyond just this 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;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;beginTransaction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;"UPDATE password_resets SET used = 1
   WHERE token_hash = ? AND used = 0 AND expires_at &amp;gt; 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;$tokenHash&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;$stmt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;rowCount&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;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;rollBack&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;respond_invalid_or_expired&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="s2"&gt;"UPDATE users SET password_hash = ? WHERE email = ?"&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="nb"&gt;password_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$newPassword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;PASSWORD_DEFAULT&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;$row&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="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Invalidate existing sessions here too - a reset should log out&lt;/span&gt;
&lt;span class="c1"&gt;// every other active session, in case the account was already compromised.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;WHERE used = 0 AND expires_at &amp;gt; NOW()&lt;/code&gt; inside the same &lt;code&gt;UPDATE&lt;/code&gt; closes a race condition that a separate &lt;code&gt;SELECT&lt;/code&gt; then &lt;code&gt;UPDATE&lt;/code&gt; cannot: two simultaneous requests carrying the same token can't both succeed, because only one &lt;code&gt;UPDATE&lt;/code&gt; will actually affect a row. Checking a credential and consuming it need to be one atomic state transition, not two sequential steps — that principle applies well beyond password resets, to anything single-use: invite codes, coupon redemptions, idempotency keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #6: Host header injection in the reset link itself
&lt;/h2&gt;

&lt;p&gt;This one doesn't live in your token logic at all — it lives in how you build the URL you email.&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;// DON'T DO THIS&lt;/span&gt;
&lt;span class="nv"&gt;$resetLink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTP_HOST'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/reset?token="&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Reset your password"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Click here: &lt;/span&gt;&lt;span class="nv"&gt;$resetLink&lt;/span&gt;&lt;span class="s2"&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;$_SERVER['HTTP_HOST']&lt;/code&gt; (and &lt;code&gt;X-Forwarded-Host&lt;/code&gt;) is a request header the client controls. Whether this is exploitable depends on your specific deployment — namely, whether the application or the proxy/load balancer in front of it accepts and forwards an attacker-controlled Host value instead of stripping or validating it. Where that's true, an attacker can submit a password reset request with a forged header:&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="nf"&gt;POST&lt;/span&gt; &lt;span class="nn"&gt;/forgot-password&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;attacker-controlled.com&lt;/span&gt;
&lt;span class="s"&gt;...&lt;/span&gt;
&lt;span class="s"&gt;email=victim@example.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your server dutifully generates a valid, correctly-hashed, properly-expiring, single-use token and emails the victim a link pointing to &lt;code&gt;https://attacker-controlled.com/reset?token=...&lt;/code&gt;. The victim, trusting an email that genuinely came from your mail server, clicks it. The attacker's server logs the token and immediately replays it against your real reset endpoint. Every other bug we just fixed becomes irrelevant, because the entire token was handed to the attacker by the victim's own click.&lt;/p&gt;

&lt;p&gt;This is a real, repeatedly-disclosed class of vulnerability, not a theoretical one — it's shown up in production frameworks and CMSs specifically through "forgot password" emails, precisely because that flow is one of the few places an app builds an absolute URL from request-influenced data and sends it somewhere sensitive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; never trust the Host header for anything security-relevant. Use a hardcoded or config-driven application 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="c1"&gt;// config.php&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;'APP_BASE_URL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'https://app.example.com'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// never derived from the request&lt;/span&gt;

&lt;span class="nv"&gt;$resetLink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;APP_BASE_URL&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/reset?token='&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;urlencode&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you must support multiple environments, validate &lt;code&gt;HTTP_HOST&lt;/code&gt; against an explicit allowlist at the point it enters your app — but for anything going into an email or a redirect, a config value is simpler and safer than validation logic you have to get right every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #7: Enumeration and abuse — two separate problems, one endpoint
&lt;/h2&gt;

&lt;p&gt;Even a perfect implementation of everything above leaks information if the endpoint responds differently depending on what happened internally:&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="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$userExists&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="s2"&gt;"No account found with that email."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// enumeration&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;$tokenInvalid&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="s2"&gt;"Token invalid."&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;$tokenExpired&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="s2"&gt;"Token expired."&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;It helps to treat these as two distinct problems, because they have two distinct fixes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Account enumeration&lt;/strong&gt; is about what the response reveals. Returning "No account found" vs. a success message tells an attacker which emails are registered — useful for building a credential-stuffing target list against a different, much larger breach.&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;// Same response whether or not the email exists&lt;/span&gt;
&lt;span class="nf"&gt;respond_generic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"If that email is registered, a reset link has been sent."&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;Abuse&lt;/strong&gt; is about how often the endpoint can be hit at all — flooding a victim's inbox with reset emails, or, if some earlier bug survived, brute-forcing tokens live against the endpoint rather than offline.&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="nf"&gt;attempts_exceeded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$max&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;attempts_exceeded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$max&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// still return the generic message - don't let the rate limiter&lt;/span&gt;
    &lt;span class="c1"&gt;// itself become a new oracle by responding differently once triggered&lt;/span&gt;
    &lt;span class="nf"&gt;respond_generic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"If that email is registered, a reset link has been sent."&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generic responses close the enumeration leak; rate limiting closes the abuse vector. They solve different problems and aren't substitutes for each other — a beautifully generic response message still doesn't stop someone from hammering the endpoint 50,000 times a minute, and a strict rate limit doesn't stop an attacker from telling registered emails apart on their very first request.&lt;/p&gt;

&lt;p&gt;Rate limit by email and by IP, independently — email-only limiting lets an attacker distribute requests across many IPs, IP-only limiting lets them rotate target emails from one machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Laravel already does for you
&lt;/h2&gt;

&lt;p&gt;If you're building this on Laravel rather than raw PHP, the built-in Password broker (&lt;code&gt;Illuminate\Auth\Passwords\PasswordBroker&lt;/code&gt;) already handles several of these correctly out of the box:&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\Password&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Password&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;sendResetLink&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Token generation&lt;/strong&gt; uses a cryptographically secure random string, and Laravel stores only its hash in the &lt;code&gt;password_reset_tokens&lt;/code&gt; table — Bugs #1 and #2 are handled for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comparison&lt;/strong&gt; goes through &lt;code&gt;hash_equals()&lt;/code&gt; internally when validating the submitted token against the stored hash — Bug #3 is handled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expiration&lt;/strong&gt; is configurable via &lt;code&gt;auth.php&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'passwords'&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;'users'&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;'provider'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'table'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'password_reset_tokens'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'expire'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;// minutes&lt;/span&gt;
        &lt;span class="s1"&gt;'throttle'&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;// seconds between requests&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;throttle&lt;/code&gt; value gives you request-level rate limiting on the sending side for free — Bug #7's abuse half is largely covered.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enumeration&lt;/strong&gt; is handled if you use the generic status messages Laravel already returns (&lt;code&gt;PASSWORD_RESET_LINK_SENT&lt;/code&gt; etc.) rather than branching your own response on &lt;code&gt;Password::INVALID_USER&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What Laravel does not automatically save you from:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bug #5 (reuse):&lt;/strong&gt; Laravel deletes the token row after a successful reset, so a single successful use does invalidate it — but if you've customized the reset controller and swapped in your own completion logic, it's easy to lose that deletion. Confirm &lt;code&gt;Password::reset()&lt;/code&gt; (or your override) actually removes the row, and consider also revoking existing sessions/tokens for the user as part of that same flow, since Laravel doesn't do that for you by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bug #6 (host header injection):&lt;/strong&gt; this is the one Laravel developers miss most often, because &lt;code&gt;url()&lt;/code&gt; and &lt;code&gt;route()&lt;/code&gt; helpers build absolute URLs from the current request's host by default. If your app sits behind a proxy that isn't listed in &lt;code&gt;TrustProxies&lt;/code&gt;, or if &lt;code&gt;APP_URL&lt;/code&gt; isn't set and Laravel falls back to inferring the host, a forged &lt;code&gt;Host&lt;/code&gt;/&lt;code&gt;X-Forwarded-Host&lt;/code&gt; header can still end up baked into the reset email. Set &lt;code&gt;APP_URL&lt;/code&gt; explicitly in &lt;code&gt;.env&lt;/code&gt;, configure &lt;code&gt;TrustProxies&lt;/code&gt; (or Laravel's &lt;code&gt;trustProxies()&lt;/code&gt; in &lt;code&gt;bootstrap/app.php&lt;/code&gt; on newer versions) to only trust your actual load balancer, and consider using &lt;code&gt;URL::forceRootUrl()&lt;/code&gt; for anything as sensitive as a password reset link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IP-based throttling on the reset submission endpoint&lt;/strong&gt; (as opposed to the send-link endpoint) still needs Laravel's throttle middleware applied explicitly to that route — the framework's built-in throttle config governs how often a link can be requested, not how many reset attempts can be made against a given token.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The takeaway isn't "Laravel is safe, raw PHP is dangerous" — it's that a framework encoding these decisions for you is exactly the kind of defense-in-depth that raises the floor. It also means the two places worth double-checking in any Laravel app are the ones the framework can't infer on your behalf: what happens to the token row after a successful reset, and where the host in that email link actually comes from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;None of these seven bugs is exotic. Individually, each is a small, well-known fix. What makes password reset a genuinely dangerous feature is that it sits at the intersection of all of them simultaneously — cryptographic randomness, safe comparison, database hygiene, time handling, transactional state, trust boundaries around the Host header, and abuse prevention — all in a flow that's maybe 80 lines of code (or a few config values, in Laravel).&lt;/p&gt;

&lt;p&gt;That's also exactly why it's worth building once, correctly, as a reference rather than reimplementing from memory in every project, or trusting a framework default you've never actually read. If you've internalized &lt;code&gt;random_bytes()&lt;/code&gt;, &lt;code&gt;hash_equals()&lt;/code&gt;, rate limiting, and the type-juggling pitfalls of &lt;code&gt;==&lt;/code&gt; as separate facts, this flow is where you find out whether you understand why each one exists, not just that it exists.&lt;/p&gt;

&lt;p&gt;Code review and correct primitives are the first layer. They should catch all seven of these before anything ships. A second, independent layer — logging and flagging unusual traffic patterns against the reset endpoint itself, like repeated token attempts from one source or a burst of requests across many emails from one IP — doesn't fix a broken implementation, but it does mean that if something upstream slips through anyway, you find out from a log line instead of a breach report. Tools built for exactly that job (Kriosa is one example, for PHP and Laravel apps specifically) sit at the application boundary and watch for that kind of probing without touching the reset logic itself — worth having, but never a substitute for getting the seven bugs above right first.&lt;/p&gt;

&lt;p&gt;If you're auditing an existing reset flow, the fastest way to find problems is usually: read the token generation line, read the comparison line, then check whether the Host header ever touches the emailed URL. Those three spots account for the large majority of real-world reset vulnerabilities.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is Kriosa?
&lt;/h2&gt;

&lt;p&gt;Kriosa is an application-level security layer for PHP and Laravel applications. It sits at the application boundary and analyzes incoming requests for suspicious traffic before that traffic reaches sensitive application logic.&lt;/p&gt;

&lt;p&gt;For password reset flows, Kriosa can provide an additional detection and visibility layer by helping surface suspicious request patterns associated with attempts to probe, brute-force, or abuse a "forgot password" endpoint.&lt;/p&gt;

&lt;p&gt;But Kriosa is &lt;strong&gt;not a replacement for building the reset flow correctly&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If your application exposes a password reset endpoint, the primary fix is to get the flow itself right: cryptographically random tokens, constant-time comparison, single-use enforcement, generic responses, and a reset link built from a trusted URL rather than the request's Host header.&lt;/p&gt;

&lt;p&gt;Think of Kriosa as &lt;strong&gt;defense in depth&lt;/strong&gt;, not a substitute for a correctly implemented reset flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Kriosa Can Help Detect Suspicious Reset-Flow Activity
&lt;/h3&gt;

&lt;p&gt;Password reset abuse is fundamentally an application-layer problem.&lt;/p&gt;

&lt;p&gt;The dangerous condition is not simply that someone submitted a password reset request. Legitimate users forget passwords constantly, and a single reset request is completely normal.&lt;/p&gt;

&lt;p&gt;The real problem is when a pattern of requests suggests someone is &lt;strong&gt;probing the flow itself&lt;/strong&gt; — testing whether emails exist, guessing at tokens, replaying old links, or hammering the endpoint from many sources at once.&lt;/p&gt;

&lt;p&gt;That makes detection useful as an additional layer, but it does not change where the vulnerability must ultimately be fixed: in the reset flow's own logic.&lt;/p&gt;

&lt;p&gt;Patterns worth investigating include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Many reset requests for different emails from one IP (enumeration sweep)&lt;/li&gt;
&lt;li&gt;Repeated token attempts against the reset-submission endpoint (token brute-forcing)&lt;/li&gt;
&lt;li&gt;A token being presented after it should already be expired or used (replay)&lt;/li&gt;
&lt;li&gt;Reset requests whose Host header doesn't match the app's known domain (host header injection attempts)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, a burst of &lt;code&gt;POST /reset-password&lt;/code&gt; requests carrying malformed or near-miss tokens against a single account may be worth investigating even though no individual request looks obviously malicious.&lt;/p&gt;

&lt;p&gt;The important word is &lt;strong&gt;pattern&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A single reset request, or a single wrong token, is not automatically an attack. Context and volume matter.&lt;/p&gt;

&lt;p&gt;Kriosa can add another layer of visibility by helping developers identify suspicious request patterns, unusual timing between requests, and repeated probing of the forgot-password and reset-submission endpoints.&lt;/p&gt;

&lt;p&gt;The distinction is important:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A detection signal is not proof of an attack, and detection is not the same as prevention.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Prevention Comes First
&lt;/h3&gt;

&lt;p&gt;The application should still:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate tokens with &lt;code&gt;random_bytes()&lt;/code&gt;, never &lt;code&gt;md5(time())&lt;/code&gt;, &lt;code&gt;uniqid()&lt;/code&gt;, or &lt;code&gt;mt_rand()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Store only a hash of the token, never the raw value.&lt;/li&gt;
&lt;li&gt;Compare tokens with &lt;code&gt;hash_equals()&lt;/code&gt;, never &lt;code&gt;==&lt;/code&gt; or bare &lt;code&gt;===&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enforce a short expiration window and mark tokens used atomically, in the same query that checks them.&lt;/li&gt;
&lt;li&gt;Return the same generic response regardless of whether the email exists, whether the token was wrong, or whether it expired.&lt;/li&gt;
&lt;li&gt;Build the reset link from a hardcoded or config-driven application URL, never from &lt;code&gt;$_SERVER['HTTP_HOST']&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Rate limit by email and by IP independently, on both the send-link and reset-submission endpoints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These controls address the vulnerability itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Detection Adds Another Layer
&lt;/h3&gt;

&lt;p&gt;A correctly built reset flow closes off the known failure modes, but it does not guarantee that every future change to the flow preserves that correctness.&lt;/p&gt;

&lt;p&gt;An attacker sweeping an endpoint with many emails, or repeatedly submitting near-miss tokens against one account, may be attempting to find a gap in the implementation — or attempting to brute-force it live.&lt;/p&gt;

&lt;p&gt;That activity is useful security telemetry.&lt;/p&gt;

&lt;p&gt;Kriosa is designed to provide additional application-level visibility into suspicious traffic — helping developers &lt;strong&gt;detect, log, investigate, and respond to activity that may indicate an attack on the reset flow&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In Laravel applications, the built-in &lt;code&gt;PasswordBroker&lt;/code&gt; already handles token generation, hashing, and comparison correctly out of the box. But two things it doesn't automatically catch are a forged &lt;code&gt;Host&lt;/code&gt;/&lt;code&gt;X-Forwarded-Host&lt;/code&gt; header ending up in the emailed link, and abnormal request volume against the reset-submission route rather than the send-link route. Both are exactly the kind of traffic pattern a detection layer is positioned to flag.&lt;/p&gt;

&lt;p&gt;The goal is not to make a broken reset flow safe.&lt;/p&gt;

&lt;p&gt;The goal is to &lt;strong&gt;get the flow right first, then add visibility around the traffic attempting to reach it.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Kriosa?
&lt;/h3&gt;

&lt;p&gt;Security controls can fail.&lt;/p&gt;

&lt;p&gt;A developer can copy a token-generation snippet from an old tutorial that used &lt;code&gt;md5(time())&lt;/code&gt;. A custom override of the reset controller can quietly drop the "delete token after use" step. A proxy change can stop stripping a forged &lt;code&gt;Host&lt;/code&gt; header before it reaches the app. A rate limit can get applied to the wrong route.&lt;/p&gt;

&lt;p&gt;That is why defense in depth matters.&lt;/p&gt;

&lt;p&gt;A practical security model can look like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secure flow → Input validation → Detection → Logging &amp;amp; response&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kriosa fits into the detection layer.&lt;/p&gt;

&lt;p&gt;Your reset flow should prevent the vulnerability.&lt;/p&gt;

&lt;p&gt;Your validation should reject malformed tokens and unexpected input.&lt;/p&gt;

&lt;p&gt;Kriosa can provide additional visibility into suspicious application traffic.&lt;/p&gt;

&lt;p&gt;Your logs and monitoring can help you investigate and respond.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kriosa does not replace a secure reset flow. It adds another layer for detecting and monitoring the traffic that reaches it.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Password Reset Flow Security Checklist
&lt;/h2&gt;

&lt;p&gt;Before shipping a "forgot password" feature in PHP or Laravel, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Do I generate tokens with &lt;code&gt;random_bytes()&lt;/code&gt;, not &lt;code&gt;md5(time())&lt;/code&gt;, &lt;code&gt;uniqid()&lt;/code&gt;, or &lt;code&gt;mt_rand()&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;[ ] Do I store only a hash of the token, never the raw value?&lt;/li&gt;
&lt;li&gt;[ ] Do I compare tokens with &lt;code&gt;hash_equals()&lt;/code&gt;, never &lt;code&gt;==&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;[ ] Do I validate the shape of the submitted token before it reaches a query or hash function?&lt;/li&gt;
&lt;li&gt;[ ] Is the token's expiration checked, and short (15–30 minutes)?&lt;/li&gt;
&lt;li&gt;[ ] Is the token marked used atomically, in the same statement that validates it?&lt;/li&gt;
&lt;li&gt;[ ] Does a successful reset invalidate the token and existing sessions?&lt;/li&gt;
&lt;li&gt;[ ] Does the endpoint return the same generic response whether the email exists, the token is wrong, or it's expired?&lt;/li&gt;
&lt;li&gt;[ ] Is the reset link built from a config-driven &lt;code&gt;APP_URL&lt;/code&gt;, never from the request's &lt;code&gt;Host&lt;/code&gt; header?&lt;/li&gt;
&lt;li&gt;[ ] If behind a proxy, is &lt;code&gt;TrustProxies&lt;/code&gt; (or equivalent) configured to trust only the real load balancer?&lt;/li&gt;
&lt;li&gt;[ ] Am I rate limiting by email and by IP independently, on both the send-link and reset-submission endpoints?&lt;/li&gt;
&lt;li&gt;[ ] Are suspicious requests against the reset endpoints being logged and monitored?&lt;/li&gt;
&lt;li&gt;[ ] Do I have an additional detection layer for probing or brute-force patterns on the reset flow?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't simply to use the right primitives once.&lt;/p&gt;

&lt;p&gt;The goal is to make sure &lt;strong&gt;an attacker never gets the chance to guess, replay, or hijack a reset link that should only ever reach one legitimate user.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Try Kriosa
&lt;/h2&gt;

&lt;p&gt;If you want an additional application-level security layer for your PHP or Laravel application:&lt;/p&gt;

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

&lt;p&gt;Install it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require kriosa-ai/kriosa-php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Documentation:&lt;/strong&gt; &lt;a href="https://kriosa.com/documentation.php" rel="noopener noreferrer"&gt;kriosa.com/documentation.php&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;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>security</category>
      <category>laravel</category>
    </item>
    <item>
      <title>Defense in Depth — How Every Layer in This Series Works Together</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Tue, 08 Sep 2026 10:07:29 +0000</pubDate>
      <link>https://dev.to/kriosa/defense-in-depth-how-every-layer-in-this-series-works-together-1i26</link>
      <guid>https://dev.to/kriosa/defense-in-depth-how-every-layer-in-this-series-works-together-1i26</guid>
      <description>&lt;p&gt;This is the twentieth and final article in the PHP and Laravel security series.&lt;/p&gt;

&lt;p&gt;Over the past nineteen articles we 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 — the file that isn't what it claims to be&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;
&lt;a href="https://kriosa.com/post.php?slug=mass-assignment-in-php-and-laravel-when-user-input-becomes-more-than-it-should" rel="noopener noreferrer"&gt;Article 16&lt;/a&gt;: Mass assignment in PHP and Laravel — when user input becomes more than it should&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kriosa.com/post.php?slug=open-redirect-vulnerabilities-in-php-and-laravel-when-your-own-domain-becomes-the-attack" rel="noopener noreferrer"&gt;Article 17&lt;/a&gt;: Open redirect vulnerabilities in PHP and Laravel&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kriosa.com/post.php?slug=php-type-juggling-when-0-equals-admin-6a99f1e6a0762" rel="noopener noreferrer"&gt;Article 18&lt;/a&gt;: PHP type juggling — when 0 equals admin&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kriosa.com/post.php?slug=insecure-deserialization-in-php-when-your-cache-becomes-an-attack-surface" rel="noopener noreferrer"&gt;Article 19&lt;/a&gt;: Insecure deserialization in PHP — the vulnerability hiding in your cache&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each article taught one attack. One defense. One layer.&lt;/p&gt;

&lt;p&gt;This article answers the question that matters after reading all nineteen: how do you put it all together into one application that actually holds?&lt;/p&gt;




&lt;h2&gt;
  
  
  Why One Layer Is Never Enough
&lt;/h2&gt;

&lt;p&gt;Every security control in this series can be bypassed in isolation.&lt;/p&gt;

&lt;p&gt;Parameterized queries stop SQL injection but they do not stop path traversal. Security headers reduce XSS impact but they do not replace output encoding. Rate limiting slows brute force but it does not catch credential stuffing distributed across thousands of IP addresses.&lt;/p&gt;

&lt;p&gt;Security works because controls overlap. Each layer catches what the others miss. When one control fails another is already in place.&lt;/p&gt;

&lt;p&gt;The attacker needs to find one gap. You need to close all of them.&lt;/p&gt;

&lt;p&gt;That is defense in depth. Not a product. Not a checklist you complete once. A set of overlapping layers that together create a posture significantly harder to breach than any single control alone.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Attack Journey — What Each Layer Stops
&lt;/h2&gt;

&lt;p&gt;To understand why every layer matters follow a real attack from reconnaissance to breach and see exactly which control stops it at each stage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 1 — Reconnaissance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before an attacker exploits your application they study it. They look for debug mode left on, &lt;code&gt;.env&lt;/code&gt; files accessible via browser, &lt;code&gt;.git&lt;/code&gt; directories exposed, default error pages revealing framework versions, admin panels at predictable URLs, and developer tools accessible without authentication.&lt;/p&gt;

&lt;p&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; and &lt;a href="https://kriosa.com/post.php?slug=secrets-management-laravel-env" rel="noopener noreferrer"&gt;Article 11&lt;/a&gt; address this. An application that leaks no information during reconnaissance forces the attacker to work blind. They cannot target known vulnerabilities in your framework version. They cannot enumerate routes. They cannot find admin panels to brute force.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 2 — Input Testing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The attacker sends malicious input to every parameter they can find — SQL injection payloads, XSS scripts, path traversal sequences, command separators, type juggling values.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kriosa.com/post.php?slug=parameterized-queries-sql-injection-prevention" rel="noopener noreferrer"&gt;Articles 4&lt;/a&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;5&lt;/a&gt;, &lt;a href="https://kriosa.com/post.php?slug=path-traversal-php-directory-escape" rel="noopener noreferrer"&gt;8&lt;/a&gt;, &lt;a href="https://kriosa.com/post.php?slug=command-injection-php-exec-attack-surface" rel="noopener noreferrer"&gt;9&lt;/a&gt;, and &lt;a href="https://kriosa.com/post.php?slug=php-type-juggling-when-0-equals-admin-6a99f1e6a0762" rel="noopener noreferrer"&gt;18&lt;/a&gt; address this. Parameterized queries mean SQL injection payloads hit prepared statements and return no result. Output encoding means XSS payloads render as text not code. &lt;code&gt;realpath()&lt;/code&gt; validation means traversal sequences resolve to blocked paths. Shell function avoidance and &lt;code&gt;escapeshellarg()&lt;/code&gt; reduce command injection risk. Strict comparison means type juggling payloads fail at the comparison layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 3 — Authentication Attack&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The attacker cannot get in through injection so they target authentication — brute force, credential stuffing, session attacks.&lt;/p&gt;

&lt;p&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; and &lt;a href="https://kriosa.com/post.php?slug=rate-limiting-laravel-php-brute-force" rel="noopener noreferrer"&gt;Article 13&lt;/a&gt; address this. Rate limiting makes brute force significantly slower — 5 attempts per minute means 10,000 passwords takes hours instead of seconds. Session regeneration after login prevents fixation. Cookie security attributes &lt;code&gt;HttpOnly&lt;/code&gt; and &lt;code&gt;Secure&lt;/code&gt; help prevent session theft through certain vectors. Strong password hashing and &lt;code&gt;password_verify()&lt;/code&gt; prevent type juggling bypasses on login.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 4 — Privilege Escalation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The attacker gets in as a regular user. Now they try to reach higher privileges.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kriosa.com/post.php?slug=broken-access-control-laravel" rel="noopener noreferrer"&gt;Articles 10&lt;/a&gt;, &lt;a href="https://kriosa.com/post.php?slug=idor-php-laravel-insecure-direct-object-reference" rel="noopener noreferrer"&gt;15&lt;/a&gt;, &lt;a href="https://kriosa.com/post.php?slug=mass-assignment-in-php-and-laravel-when-user-input-becomes-more-than-it-should" rel="noopener noreferrer"&gt;16&lt;/a&gt;, and &lt;a href="https://kriosa.com/post.php?slug=insecure-deserialization-in-php-when-your-cache-becomes-an-attack-surface" rel="noopener noreferrer"&gt;19&lt;/a&gt; address this. &lt;code&gt;$fillable&lt;/code&gt; and Form Requests mean extra fields in requests are silently ignored. Scoped queries and policies mean resource ID changes return 404 for resources that do not belong to the user. Middleware and authorization checks mean privileged endpoints reject unauthorized requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 5 — Data Exfiltration and Persistence&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The attacker has access. Now they try to steal data or leave a backdoor.&lt;/p&gt;

&lt;p&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;Articles 7&lt;/a&gt;, &lt;a href="https://kriosa.com/post.php?slug=command-injection-php-exec-attack-surface" rel="noopener noreferrer"&gt;9&lt;/a&gt;, &lt;a href="https://kriosa.com/post.php?slug=secrets-management-laravel-env" rel="noopener noreferrer"&gt;11&lt;/a&gt;, &lt;a href="https://kriosa.com/post.php?slug=open-redirect-vulnerabilities-in-php-and-laravel-when-your-own-domain-becomes-the-attack" rel="noopener noreferrer"&gt;17&lt;/a&gt;, and &lt;a href="https://kriosa.com/post.php?slug=insecure-deserialization-in-php-when-your-cache-becomes-an-attack-surface" rel="noopener noreferrer"&gt;19&lt;/a&gt; address this. Files stored outside the web root with random names cannot be executed via URL. Secret management practices mean credentials are not accessible through misconfigured endpoints. Whitelist-based redirects mean your domain cannot be weaponized for phishing. JSON serialization means web shell injection through deserialization has no attack surface.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Complete Layer Map
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attack Stage&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Article&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reconnaissance&lt;/td&gt;
&lt;td&gt;Enumeration and information leakage&lt;/td&gt;
&lt;td&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;6&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reconnaissance&lt;/td&gt;
&lt;td&gt;Secret and environment exposure&lt;/td&gt;
&lt;td&gt;&lt;a href="https://kriosa.com/post.php?slug=secrets-management-laravel-env" rel="noopener noreferrer"&gt;11&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input injection&lt;/td&gt;
&lt;td&gt;SQL injection detection and prevention&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://kriosa.com/post.php?slug=php-logs-sql-injection-attack-6a5fa71fd80b8" rel="noopener noreferrer"&gt;1&lt;/a&gt;, &lt;a href="https://kriosa.com/post.php?slug=url-encoding-breaks-php-security-checks" rel="noopener noreferrer"&gt;2&lt;/a&gt;, &lt;a href="https://kriosa.com/post.php?slug=decode-bomb-url-decoding-vulnerability" rel="noopener noreferrer"&gt;3&lt;/a&gt;, &lt;a href="https://kriosa.com/post.php?slug=parameterized-queries-sql-injection-prevention" rel="noopener noreferrer"&gt;4&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input injection&lt;/td&gt;
&lt;td&gt;XSS prevention&lt;/td&gt;
&lt;td&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;5&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input injection&lt;/td&gt;
&lt;td&gt;Path traversal&lt;/td&gt;
&lt;td&gt;&lt;a href="https://kriosa.com/post.php?slug=path-traversal-php-directory-escape" rel="noopener noreferrer"&gt;8&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input injection&lt;/td&gt;
&lt;td&gt;Command injection&lt;/td&gt;
&lt;td&gt;&lt;a href="https://kriosa.com/post.php?slug=command-injection-php-exec-attack-surface" rel="noopener noreferrer"&gt;9&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input injection&lt;/td&gt;
&lt;td&gt;Type juggling&lt;/td&gt;
&lt;td&gt;&lt;a href="https://kriosa.com/post.php?slug=php-type-juggling-when-0-equals-admin-6a99f1e6a0762" rel="noopener noreferrer"&gt;18&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input file handling&lt;/td&gt;
&lt;td&gt;File upload security&lt;/td&gt;
&lt;td&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;7&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication&lt;/td&gt;
&lt;td&gt;Brute force and credential stuffing&lt;/td&gt;
&lt;td&gt;&lt;a href="https://kriosa.com/post.php?slug=rate-limiting-laravel-php-brute-force" rel="noopener noreferrer"&gt;13&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication&lt;/td&gt;
&lt;td&gt;Session security&lt;/td&gt;
&lt;td&gt;&lt;a href="https://kriosa.com/post.php?slug=session-security-php-developers-get-wrong" rel="noopener noreferrer"&gt;12&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privilege escalation&lt;/td&gt;
&lt;td&gt;Mass assignment&lt;/td&gt;
&lt;td&gt;&lt;a href="https://kriosa.com/post.php?slug=mass-assignment-in-php-and-laravel-when-user-input-becomes-more-than-it-should" rel="noopener noreferrer"&gt;16&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privilege escalation&lt;/td&gt;
&lt;td&gt;IDOR&lt;/td&gt;
&lt;td&gt;&lt;a href="https://kriosa.com/post.php?slug=idor-php-laravel-insecure-direct-object-reference" rel="noopener noreferrer"&gt;15&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privilege escalation&lt;/td&gt;
&lt;td&gt;Broken access control&lt;/td&gt;
&lt;td&gt;&lt;a href="https://kriosa.com/post.php?slug=broken-access-control-laravel" rel="noopener noreferrer"&gt;10&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privilege escalation&lt;/td&gt;
&lt;td&gt;Deserialization&lt;/td&gt;
&lt;td&gt;&lt;a href="https://kriosa.com/post.php?slug=insecure-deserialization-in-php-when-your-cache-becomes-an-attack-surface" rel="noopener noreferrer"&gt;19&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data handling&lt;/td&gt;
&lt;td&gt;Secrets management&lt;/td&gt;
&lt;td&gt;&lt;a href="https://kriosa.com/post.php?slug=secrets-management-laravel-env" rel="noopener noreferrer"&gt;11&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Navigation&lt;/td&gt;
&lt;td&gt;Open redirect&lt;/td&gt;
&lt;td&gt;&lt;a href="https://kriosa.com/post.php?slug=open-redirect-vulnerabilities-in-php-and-laravel-when-your-own-domain-becomes-the-attack" rel="noopener noreferrer"&gt;17&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser layer&lt;/td&gt;
&lt;td&gt;Security headers&lt;/td&gt;
&lt;td&gt;&lt;a href="https://kriosa.com/post.php?slug=security-headers-php-laravel-6a82e46e2e41a" rel="noopener noreferrer"&gt;14&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All stages&lt;/td&gt;
&lt;td&gt;App security and Behavioral monitoring and AI detection&lt;/td&gt;
&lt;td&gt;Kriosa&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No single article covers every stage. Every stage requires multiple articles. That is the point.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Checklist — Everything in One Place
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before first deployment:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;APP_DEBUG=false&lt;/code&gt; and &lt;code&gt;APP_ENV=production&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.env&lt;/code&gt; in &lt;code&gt;.gitignore&lt;/code&gt; before first commit&lt;/li&gt;
&lt;li&gt;No secrets hardcoded anywhere in the codebase&lt;/li&gt;
&lt;li&gt;All database queries use parameterized statements&lt;/li&gt;
&lt;li&gt;All output encoded with &lt;code&gt;{{ }}&lt;/code&gt; in Blade — &lt;code&gt;{!! !!}&lt;/code&gt; reviewed explicitly&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;$fillable&lt;/code&gt; defined on every Eloquent model that accepts user input&lt;/li&gt;
&lt;li&gt;All file uploads validated for content, stored with random names outside the web root&lt;/li&gt;
&lt;li&gt;PHP execution disabled in any directory that stores user uploads&lt;/li&gt;
&lt;li&gt;Security headers middleware registered globally&lt;/li&gt;
&lt;li&gt;HTTPS configured before enabling HSTS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Authentication and sessions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;session_regenerate_id(true)&lt;/code&gt; called after every login&lt;/li&gt;
&lt;li&gt;Session cookie attributes: &lt;code&gt;HttpOnly&lt;/code&gt;, &lt;code&gt;Secure&lt;/code&gt;, &lt;code&gt;SameSite=Lax&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;password_verify()&lt;/code&gt; used for all password comparisons&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hash_equals()&lt;/code&gt; used for all token comparisons&lt;/li&gt;
&lt;li&gt;Rate limiting applied to login, password reset, registration, and OTP endpoints&lt;/li&gt;
&lt;li&gt;Three-step logout: &lt;code&gt;Auth::logout()&lt;/code&gt;, &lt;code&gt;invalidate()&lt;/code&gt;, &lt;code&gt;regenerateToken()&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Authorization:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every resource endpoint has an ownership check — not just authentication&lt;/li&gt;
&lt;li&gt;Scoped queries used as the default pattern&lt;/li&gt;
&lt;li&gt;Policies implemented for complex authorization logic&lt;/li&gt;
&lt;li&gt;API endpoints have the same authorization checks as web endpoints&lt;/li&gt;
&lt;li&gt;Sensitive fields 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;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Input handling:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;===&lt;/code&gt; used for all equality comparisons in security contexts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;in_array()&lt;/code&gt; always called with &lt;code&gt;true&lt;/code&gt; as the third argument&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;json_encode()&lt;/code&gt;/&lt;code&gt;json_decode()&lt;/code&gt; used instead of &lt;code&gt;serialize()&lt;/code&gt;/&lt;code&gt;unserialize()&lt;/code&gt; where possible&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;unserialize()&lt;/code&gt; called with &lt;code&gt;['allowed_classes' =&amp;gt; false]&lt;/code&gt; when unavoidable&lt;/li&gt;
&lt;li&gt;Shell functions avoided where PHP native alternatives exist&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;realpath()&lt;/code&gt; used to validate file paths before filesystem operations&lt;/li&gt;
&lt;li&gt;All redirect destinations validated against a whitelist or restricted to relative URLs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Server and infrastructure:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Directory listing disabled&lt;/li&gt;
&lt;li&gt;Technology headers removed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.git&lt;/code&gt;, &lt;code&gt;.env&lt;/code&gt;, and backup files blocked at web server level&lt;/li&gt;
&lt;li&gt;Developer tools behind authentication or IP restriction&lt;/li&gt;
&lt;li&gt;Redis and queue storage on private networks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;composer audit&lt;/code&gt; running in CI/CD pipeline&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;composer install&lt;/code&gt; used in production — never &lt;code&gt;composer update&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;composer.lock&lt;/code&gt; committed to version control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ongoing:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Security headers scanner run after every deployment&lt;/li&gt;
&lt;li&gt;Logs reviewed for rate limit violations, 403 patterns, and sequential ID requests&lt;/li&gt;
&lt;li&gt;Automated vulnerability alerts enabled for dependencies&lt;/li&gt;
&lt;li&gt;Secret rotation plan tested for every secret in the application&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What This Series Covers
&lt;/h2&gt;

&lt;p&gt;Nineteen articles. Nineteen attack vectors. Nineteen defensive layers.&lt;/p&gt;

&lt;p&gt;A developer who has read and implemented every article in this series has addressed many of the most common vulnerability classes found in PHP and Laravel applications today — including multiple categories from the OWASP Top 10.&lt;/p&gt;

&lt;p&gt;That is not a complete security posture. Security is never complete. New vulnerabilities are discovered. Dependencies introduce risks. Configurations drift. Developer mistakes happen.&lt;/p&gt;

&lt;p&gt;But it is a foundation that makes your application meaningfully harder to breach than the average PHP application running in production today.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Layer This Series Cannot Replace
&lt;/h2&gt;

&lt;p&gt;Every control in this series is preventive. It stops attacks from succeeding at the application layer.&lt;/p&gt;

&lt;p&gt;But prevention alone has a gap — it cannot tell you when someone is trying.&lt;/p&gt;

&lt;p&gt;A developer who has implemented every article in this series has no visibility into which endpoints are being probed right now, whether resource ID enumeration is happening, whether credential stuffing is running below rate limit thresholds, or whether reconnaissance is underway.&lt;/p&gt;

&lt;p&gt;That detection and visibility is what Kriosa is built to provide.&lt;/p&gt;

&lt;p&gt;Kriosa sits in front of your PHP application and inspects incoming requests for attack patterns and behavioral signals before they reach your code. When something is flagged it surfaces an explanation in the dashboard — what was detected, why it was flagged, and what the request looked like.&lt;/p&gt;

&lt;p&gt;It is not a replacement for any article in this series. Parameterized queries, session regeneration, &lt;code&gt;$fillable&lt;/code&gt;, and &lt;code&gt;hash_equals()&lt;/code&gt; are application-layer controls that Kriosa cannot replicate. Those come first.&lt;/p&gt;

&lt;p&gt;Kriosa is the detection layer on top of them.&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;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;This series is twenty articles. Twenty attack vectors. Twenty defensive layers.&lt;/p&gt;

&lt;p&gt;The next step is not more reading.&lt;/p&gt;

&lt;p&gt;It is applying what you have learned to a real PHP application running in production right now.&lt;/p&gt;

&lt;p&gt;Start with the checklist in this article. Run the audit commands from &lt;a href="https://kriosa.com/post.php?slug=parameterized-queries-sql-injection-prevention" rel="noopener noreferrer"&gt;articles 4&lt;/a&gt;, &lt;a href="https://kriosa.com/post.php?slug=path-traversal-php-directory-escape" rel="noopener noreferrer"&gt;8&lt;/a&gt;, &lt;a href="https://kriosa.com/post.php?slug=command-injection-php-exec-attack-surface" rel="noopener noreferrer"&gt;9&lt;/a&gt;, &lt;a href="https://kriosa.com/post.php?slug=idor-php-laravel-insecure-direct-object-reference" rel="noopener noreferrer"&gt;15&lt;/a&gt;, and &lt;a href="https://kriosa.com/post.php?slug=mass-assignment-in-php-and-laravel-when-user-input-becomes-more-than-it-should" rel="noopener noreferrer"&gt;16&lt;/a&gt; against your own codebase. Check your session configuration against &lt;a href="https://kriosa.com/post.php?slug=session-security-php-developers-get-wrong" rel="noopener noreferrer"&gt;article 12&lt;/a&gt;. Verify your rate limiting against &lt;a href="https://kriosa.com/post.php?slug=rate-limiting-laravel-php-brute-force" rel="noopener noreferrer"&gt;article 13&lt;/a&gt;. Run &lt;code&gt;composer audit&lt;/code&gt; against your dependencies.&lt;/p&gt;

&lt;p&gt;One hour of auditing a real codebase teaches more than ten more articles.&lt;/p&gt;

&lt;p&gt;Then install Kriosa — one composer command — and see what is actually hitting your application. Not what you think is hitting it. What is actually there.&lt;/p&gt;

&lt;p&gt;The series taught the mindset. Now use it.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Final Word
&lt;/h2&gt;

&lt;p&gt;This series started with one question — what does a SQL injection attack actually look like in your PHP logs?&lt;/p&gt;

&lt;p&gt;It ended nineteen articles later with defense in depth.&lt;/p&gt;

&lt;p&gt;In between we covered every major attack vector hitting PHP applications today. Every article followed the same principle: understand the attack before you try to stop it.&lt;/p&gt;

&lt;p&gt;That principle does not change after the series ends. New vulnerabilities will be discovered. New attack techniques will emerge. The mindset — understand first, then defend — is what makes security knowledge durable.&lt;/p&gt;

&lt;p&gt;The series taught the mindset. Kriosa watches the traffic.&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 Complete Series
&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 and what to hide&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;
&lt;a href="https://kriosa.com/post.php?slug=mass-assignment-in-php-and-laravel-when-user-input-becomes-more-than-it-should" rel="noopener noreferrer"&gt;Article 16&lt;/a&gt;: Mass assignment in PHP and Laravel — when user input becomes more than it should&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kriosa.com/post.php?slug=open-redirect-vulnerabilities-in-php-and-laravel-when-your-own-domain-becomes-the-attack" rel="noopener noreferrer"&gt;Article 17&lt;/a&gt;: Open redirect vulnerabilities in PHP and Laravel&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kriosa.com/post.php?slug=php-type-juggling-when-0-equals-admin-6a99f1e6a0762" rel="noopener noreferrer"&gt;Article 18&lt;/a&gt;: PHP type juggling — when 0 equals admin&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kriosa.com/post.php?slug=insecure-deserialization-in-php-when-your-cache-becomes-an-attack-surface" rel="noopener noreferrer"&gt;Article 19&lt;/a&gt;: Insecure deserialization in PHP — the vulnerability hiding in your cache&lt;/li&gt;
&lt;li&gt;Article 20: This article — defense in depth and how every layer works together&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>php</category>
      <category>security</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>Insecure Deserialization in PHP — When Your Cache Becomes an Attack Surface</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Fri, 04 Sep 2026 17:10:34 +0000</pubDate>
      <link>https://dev.to/kriosa/insecure-deserialization-in-php-when-your-cache-becomes-an-attack-surface-5835</link>
      <guid>https://dev.to/kriosa/insecure-deserialization-in-php-when-your-cache-becomes-an-attack-surface-5835</guid>
      <description>&lt;p&gt;&lt;code&gt;unserialize()&lt;/code&gt; does more than restore data. With attacker-controlled input, object instantiation and magic methods can turn a data parser into a code-execution path.&lt;/p&gt;

&lt;p&gt;This is the nineteenth 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;li&gt;Open redirect vulnerabilities in PHP and Laravel&lt;/li&gt;
&lt;li&gt;PHP type juggling — when 0 equals admin&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;Insecure deserialization is the most technically complex vulnerability in this series. It is also one of the most severe. When a usable gadget chain exists, insecure deserialization can escalate from attacker-controlled data into arbitrary code execution. Other outcomes are possible too  file manipulation, arbitrary method execution, SSRF, data deletion or modification, or application crashes  but the code-execution scenario is why this class of bug gets so much attention.&lt;/p&gt;

&lt;p&gt;Understanding it requires understanding how PHP serialization works from the inside.&lt;/p&gt;

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

&lt;p&gt;Serialization is converting a data structure — an object, an array, a complex type — into a string that can be stored or transmitted and later reconstructed.&lt;/p&gt;

&lt;p&gt;PHP has two functions for 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="c1"&gt;// Serialize — convert object to string&lt;/span&gt;
&lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$object&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Produces: O:4:"User":2:{s:4:"name";s:4:"John";s:5:"email";s:16:"john@example.com";}&lt;/span&gt;

&lt;span class="c1"&gt;// Unserialize — convert string back to object&lt;/span&gt;
&lt;span class="nv"&gt;$object&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;unserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The serialized string encodes the class name, property names, property values, and the type of each value. When you call &lt;code&gt;unserialize()&lt;/code&gt; PHP reconstructs the original object from that string including instantiating the class.&lt;/p&gt;

&lt;p&gt;That last part  instantiating the class — is where the vulnerability begins. It's worth being precise here: &lt;code&gt;unserialize()&lt;/code&gt; does not itself execute arbitrary PHP code. The danger is that deserializing attacker-controlled objects can instantiate classes and invoke magic methods, and if suitable application "gadgets" exist, that chain of method calls can lead to code execution. PHP's own documentation explicitly warns against passing untrusted data to &lt;code&gt;unserialize()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Magic Methods — The Attack Surface&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PHP classes can define magic methods that execute automatically at specific points in an object's lifecycle:&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;Example&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Called when object is created&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;__construct&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;// Called when unserialize() restores the object (legacy)&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;__wakeup&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;// Called when unserialize() restores the object (PHP 7.4+)&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;__unserialize&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;$data&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;// Called when object is destroyed or goes out of scope&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;__destruct&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;// Called when object is used as a string&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;__toString&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;// Called when accessing undefined property&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;__get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$name&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;// Called when invoking object as function&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;__invoke&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;p&gt;When &lt;code&gt;unserialize()&lt;/code&gt; processes a serialized string it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads the class name from the string&lt;/li&gt;
&lt;li&gt;Creates an instance of that class&lt;/li&gt;
&lt;li&gt;Sets the properties to the values encoded in the string&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;__unserialize()&lt;/code&gt; if the class defines it; otherwise &lt;code&gt;__wakeup()&lt;/code&gt; if available&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The critical insight: the attacker controls the serialized string. They control which class gets instantiated, and they control the property values on that object. They do not control the code inside &lt;code&gt;__wakeup()&lt;/code&gt;, &lt;code&gt;__unserialize()&lt;/code&gt;, or &lt;code&gt;__destruct()&lt;/code&gt; — but if a class already contains one of these magic methods, the attacker-controlled property values can influence what that existing method does when it runs automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Simple Deserialization Attack&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine your application has this class somewhere in the codebase:&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;FileLogger&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$logFile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$logMessage&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;__destruct&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;file_put_contents&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;logFile&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;logMessage&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;(Because the properties are typed, this example requires PHP 7.4 or later.)&lt;/p&gt;

&lt;p&gt;This class looks harmless. It just writes a log message to a file. It might be used legitimately elsewhere in the application.&lt;/p&gt;

&lt;p&gt;Now your application accepts a serialized cookie and unserializes it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;unserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_COOKIE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'user_data'&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 crafts a serialized &lt;code&gt;FileLogger&lt;/code&gt; object:&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;$malicious&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;FileLogger&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$malicious&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;logFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/var/www/public/shell.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$malicious&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;logMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;?php system($_GET["cmd"]); ?&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nb"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$malicious&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// O:10:"FileLogger":2:{s:7:"logFile";s:25:"/var/www/public/shell.php";s:10:"logMessage";s:30:"&amp;lt;?php system($_GET["cmd"]); ?&amp;gt;";}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They set this as their cookie. Your application calls &lt;code&gt;unserialize()&lt;/code&gt;. PHP instantiates a &lt;code&gt;FileLogger&lt;/code&gt; object with their property values. When the object is destroyed at the end of the request &lt;code&gt;__destruct()&lt;/code&gt; runs — writing a PHP web shell to your public directory.&lt;/p&gt;

&lt;p&gt;The attacker now visits &lt;code&gt;/shell.php?cmd=whoami&lt;/code&gt; and has command execution on your server.&lt;/p&gt;

&lt;p&gt;This is PHP object injection through insecure deserialization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;POP Chains — Property Oriented Programming&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Real-world deserialization attacks rarely use a single class. They chain multiple classes together each one's magic method triggering the next to achieve code execution. These are called POP chains (Property Oriented Programming chains) or gadget chains.&lt;/p&gt;

&lt;p&gt;A POP chain abuses classes that already exist in the application. The attacker supplies an object graph whose properties cause existing magic methods and other methods to call one another until a dangerous operation is reached. None of the individual classes need to look dangerous on their own.&lt;/p&gt;

&lt;p&gt;A simplified example:&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;A&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$b&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;__wakeup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Calls __toString on $this-&amp;gt;b&lt;/span&gt;
        &lt;span class="k"&gt;echo&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;b&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;B&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$c&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;__toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Calls __invoke on $this-&amp;gt;c&lt;/span&gt;
        &lt;span class="k"&gt;return&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;c&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;C&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$command&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;__invoke&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Executes arbitrary command&lt;/span&gt;
        &lt;span class="k"&gt;return&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;command&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;An attacker serializes:&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;$c&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;C&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="o"&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;$b&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;B&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$a&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;A&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nb"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the serialized string is unserialized:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;A::__wakeup()&lt;/code&gt; fires — echoes &lt;code&gt;$this-&amp;gt;b&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Echoing a &lt;code&gt;B&lt;/code&gt; object triggers &lt;code&gt;B::__toString()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;__toString()&lt;/code&gt; invokes &lt;code&gt;$this-&amp;gt;c&lt;/code&gt; as a function&lt;/li&gt;
&lt;li&gt;Invoking a &lt;code&gt;C&lt;/code&gt; object triggers &lt;code&gt;C::__invoke()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;__invoke()&lt;/code&gt; calls &lt;code&gt;system($this-&amp;gt;command)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The attacker's command executes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these classes individually look dangerous. The vulnerability comes from how they chain together when an attacker controls the deserialized object graph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where Deserialization Vulnerabilities Appear&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Cookies:&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;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;unserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_COOKIE&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="c1"&gt;// The cookie value is entirely attacker-controlled&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. GET and POST parameters:&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;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;unserialize&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;'data'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nv"&gt;$config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;unserialize&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;'config'&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;3. Database fields:&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 — if the serialized data was ever user-supplied&lt;/span&gt;
&lt;span class="nv"&gt;$preferences&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;unserialize&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;preferences&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;4. Cache:&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;// Potentially dangerous — depends on what was cached and who could influence it&lt;/span&gt;
&lt;span class="nv"&gt;$cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;unserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Redis&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="mf"&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;&lt;strong&gt;5. Session data:&lt;/strong&gt;&lt;br&gt;
Some PHP session configurations and frameworks use serialization to represent session data, but whether PHP's &lt;code&gt;serialize()&lt;/code&gt; format is actually used depends on the session handler and configuration — not every PHP session file is simply the output of &lt;code&gt;serialize()&lt;/code&gt;. If an attacker can write to or influence session storage, and that storage is deserialized in PHP's native format, they may be able to inject malicious serialized objects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Laravel and Deserialization&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Laravel's queue system serializes jobs so they can be stored by the configured queue driver and reconstructed by a worker later:&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="nf"&gt;dispatch&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;ProcessOrder&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important distinction is that normal Laravel queue payloads are generated by the application. An ordinary user does not automatically get to replace those payloads with arbitrary serialized PHP objects.&lt;/p&gt;

&lt;p&gt;The risk appears when an attacker can influence the queue payload or the storage behind it — for example, through compromised queue infrastructure, unauthorized Redis access, a database compromise, or application code that places attacker-controlled serialized objects into a job.&lt;/p&gt;

&lt;p&gt;Laravel's &lt;code&gt;SerializesModels&lt;/code&gt; trait provides special handling for Eloquent models:&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;ProcessOrder&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&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;Dispatchable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;InteractsWithQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Queueable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SerializesModels&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;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;public&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rather than serializing the complete Eloquent model into the job payload, Laravel serializes information needed to retrieve the model again when the job is processed. Loaded relationships can also affect the serialized payload, so large or unnecessary relationships should be avoided.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never do this in a job:&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 — serializing user-supplied data into a job&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProcessUserData&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$userData&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;__construct&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;$rawUserData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// If $rawUserData contains a serialized object this is dangerous&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;userData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;unserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$rawUserData&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 important security rule: do not treat queue storage as a place where untrusted users can write arbitrary serialized objects. Protect Redis, database-backed queues, and other queue infrastructure as security-sensitive systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Safe Alternatives&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use JSON instead of serialize():&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;// Instead of this&lt;/span&gt;
&lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$object&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$object&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;unserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Do this&lt;/span&gt;
&lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$object&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$array&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="nv"&gt;$data&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;// returns array, not object&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;json_decode()&lt;/code&gt; with &lt;code&gt;true&lt;/code&gt; as the second argument returns an associative array never an object instance. JSON does not invoke PHP object magic methods during decoding, which makes it substantially safer than PHP object deserialization for data crossing trust boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When You Must Deserialize Existing PHP Data&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The safest option is to avoid PHP serialization for data that crosses a trust boundary  PHP's own documentation recommends a standard format such as JSON instead of &lt;code&gt;unserialize()&lt;/code&gt; for untrusted data.&lt;/p&gt;

&lt;p&gt;If you have a legitimate reason to deserialize externally stored PHP data, &lt;code&gt;allowed_classes&lt;/code&gt; can reduce the object-instantiation attack surface:&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;// Explicitly allow only the classes you expect&lt;/span&gt;
&lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;unserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'allowed_classes'&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;'MyClass'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'AnotherClass'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// Do not instantiate serialized classes&lt;/span&gt;
&lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;unserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'allowed_classes'&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;allowed_classes =&amp;gt; false&lt;/code&gt;, serialized objects are not instantiated as their original classes PHP represents them as &lt;code&gt;__PHP_Incomplete_Class&lt;/code&gt; objects instead, with no magic methods to trigger.&lt;/p&gt;

&lt;p&gt;However, &lt;code&gt;allowed_classes&lt;/code&gt; should not be treated as a way to make arbitrary attacker-controlled input safe. PHP's documentation explicitly warns against passing untrusted input to &lt;code&gt;unserialize()&lt;/code&gt; regardless of the &lt;code&gt;allowed_classes&lt;/code&gt; setting.&lt;/p&gt;

&lt;p&gt;If serialized data must cross a trust boundary, authenticate it with a strong HMAC and verify the signature &lt;strong&gt;before&lt;/strong&gt; calling &lt;code&gt;unserialize()&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="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;serializeSigned&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;$data&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;$secretKey&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;$serialized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;hash_hmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$serialized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$secretKey&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;$signature&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="nb"&gt;base64_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$serialized&lt;/span&gt;&lt;span class="p"&gt;);&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;unserializeSigned&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;$signed&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;$secretKey&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;mixed&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;explode&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;$signed&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="k"&gt;if&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;$parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Invalid signed data.'&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="nv"&gt;$signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$encoded&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$parts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$serialized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;base64_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$encoded&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$serialized&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Invalid encoding.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;hash_hmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$serialized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$secretKey&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;$expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$signature&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Signature verification failed.'&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;unserialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$serialized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'allowed_classes'&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An attacker who cannot forge the HMAC signature cannot inject malicious serialized data.&lt;/p&gt;

&lt;p&gt;The important principle: do not deserialize attacker-controlled PHP objects simply because you restricted the class list. Prefer JSON. If PHP serialization is unavoidable, authenticate the data first and restrict the classes that can be instantiated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The PHP Version Landscape&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;allowed_classes&lt;/code&gt; has been available since PHP 7.0, and PHP 7.4 added the modern &lt;code&gt;__serialize()&lt;/code&gt;/&lt;code&gt;__unserialize()&lt;/code&gt; magic methods, which take precedence over &lt;code&gt;__sleep()&lt;/code&gt;/&lt;code&gt;__wakeup()&lt;/code&gt; when both are defined. Later versions have improved type safety in various ways, but &lt;code&gt;unserialize()&lt;/code&gt; itself remains dangerous with untrusted input in every PHP version.&lt;/p&gt;

&lt;p&gt;There is no PHP version where &lt;code&gt;unserialize()&lt;/code&gt; on untrusted user input is safe without class allowlisting or input signing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auditing Your Codebase&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="c"&gt;# Find all unserialize() calls&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;"unserialize("&lt;/span&gt; app/ bootstrap/ config/

&lt;span class="c"&gt;# Find serialize() calls that might store to user-accessible locations&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;"serialize("&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;"cookie&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;session&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;cache&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;redis&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;header"&lt;/span&gt;

&lt;span class="c"&gt;# Find base64_decode followed by unserialize — common attack pattern&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;"base64_decode"&lt;/span&gt; app/

&lt;span class="c"&gt;# Find dangerous cookie handling&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;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;_COOKIE"&lt;/span&gt; app/

&lt;span class="c"&gt;# Check for allowed_classes usage&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;"allowed_classes"&lt;/span&gt; app/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For every &lt;code&gt;unserialize()&lt;/code&gt; call ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Where does this data come from?&lt;/li&gt;
&lt;li&gt;Could an attacker influence that data?&lt;/li&gt;
&lt;li&gt;Is &lt;code&gt;allowed_classes&lt;/code&gt; set to restrict which classes can be instantiated?&lt;/li&gt;
&lt;li&gt;Could the data be replaced with JSON?&lt;/li&gt;
&lt;/ol&gt;

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

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

&lt;ul&gt;
&lt;li&gt;Never call &lt;code&gt;unserialize()&lt;/code&gt; on data from cookies, GET, POST, or any user-controlled source&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;serialize()&lt;/code&gt;/&lt;code&gt;unserialize()&lt;/code&gt; with &lt;code&gt;json_encode()&lt;/code&gt;/&lt;code&gt;json_decode()&lt;/code&gt; wherever possible&lt;/li&gt;
&lt;li&gt;If PHP deserialization is unavoidable, use an explicit &lt;code&gt;allowed_classes&lt;/code&gt; allowlist where appropriate, authenticate externally stored data before deserializing it, and never treat &lt;code&gt;allowed_classes&lt;/code&gt; as a substitute for trusting the input source&lt;/li&gt;
&lt;li&gt;Sign serialized data with HMAC before storing in any user-accessible location&lt;/li&gt;
&lt;li&gt;Verify the HMAC signature before calling &lt;code&gt;unserialize()&lt;/code&gt; use &lt;code&gt;hash_equals()&lt;/code&gt; for comparison&lt;/li&gt;
&lt;li&gt;Audit every &lt;code&gt;unserialize()&lt;/code&gt; call in the codebase and trace where the data originates&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;SerializesModels&lt;/code&gt; in queue jobs it stores model identifiers rather than the full model, and reloads the model when the job runs&lt;/li&gt;
&lt;li&gt;Never unserialize user-supplied data inside a queue job&lt;/li&gt;
&lt;li&gt;Never use &lt;code&gt;unserialize()&lt;/code&gt; on cached data that could have been influenced by user input&lt;/li&gt;
&lt;li&gt;Prefer JSON for any data that crosses a trust boundary&lt;/li&gt;
&lt;li&gt;Keep Redis and queue database instances on private networks if an attacker can write to your queue storage they can inject malicious jobs&lt;/li&gt;
&lt;li&gt;Review any package that calls &lt;code&gt;unserialize()&lt;/code&gt; internally check what data it receives&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What Is&amp;nbsp;Kriosa?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kriosa is an application-level and API security layer for PHP and Laravel applications. It sits at the application boundary and inspects incoming requests for suspicious traffic before that traffic reaches sensitive application logic.&lt;/p&gt;

&lt;p&gt;For attacks involving serialized PHP payloads, Kriosa can provide an additional detection layer by identifying suspicious request patterns and payload structures associated with object-injection attempts.&lt;br&gt;
But Kriosa is not a replacement for fixing insecure deserialization.&lt;/p&gt;

&lt;p&gt;If your application accepts untrusted serialized data, the primary fix is still to remove that trust boundary: prefer JSON, authenticate externally stored serialized data, and avoid unserialize() on attacker-controlled input.&lt;/p&gt;

&lt;p&gt;Think of Kriosa as defense in depth, not a substitute for secure application design.&lt;br&gt;
How Kriosa Can Help Detect Suspicious Serialized Payloads&lt;br&gt;
Insecure deserialization is fundamentally an application-layer vulnerability.&lt;/p&gt;

&lt;p&gt;The dangerous condition isn't simply that a request contains O: or a base64-encoded value. Legitimate applications can send encoded or serialized data too.&lt;br&gt;
The real problem is that attacker-controlled data eventually reaches a dangerous deserialization operation.&lt;br&gt;
That means detection should be treated as a layer of defense, not the primary fix.&lt;br&gt;
Serialized PHP data has a recognizable format. It uses type indicators such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;O:   Object
a:   Array
s:   String
i:   Integer
b:   Boolean
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A request containing something 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="nc"&gt;O&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"FileLogger"&lt;/span&gt;&lt;span class="o"&gt;:...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or an encoded representation of a serialized object can become a useful security signal when it appears in a parameter, cookie, API request, or endpoint that normally expects a simple value.&lt;/p&gt;

&lt;p&gt;Kriosa can help surface suspicious request patterns around these endpoints, including unusual encoded payloads and repeated probing of parameters with serialized-object structures.&lt;/p&gt;

&lt;p&gt;The important distinction is:&lt;br&gt;
Detection does not equal prevention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prevention Comes&amp;nbsp;First&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The application should still:&lt;br&gt;
Avoid unserialize() for untrusted data.&lt;br&gt;
Prefer JSON for data crossing trust boundaries.&lt;br&gt;
Authenticate externally stored serialized data when PHP serialization is genuinely required.&lt;br&gt;
Restrict classes when deserialization cannot be avoided.&lt;br&gt;
Review dangerous magic methods and potential gadget chains.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detection Adds Another&amp;nbsp;Layer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once the application has been designed securely, an additional security layer can help identify attempts to exploit weaknesses that may still exist.&lt;br&gt;
An attacker probing an endpoint repeatedly with serialized-object payloads may be attempting to discover an insecure deserialization vulnerability.&lt;br&gt;
That activity is valuable security telemetry.&lt;/p&gt;

&lt;p&gt;Kriosa is designed to provide that additional visibility at the application layer helping developers detect, log, investigate, and respond to suspicious application traffic.&lt;/p&gt;

&lt;p&gt;For example, in applications such as Prolify, data crossing trust boundaries can use JSON rather than PHP object serialization. In Laravel applications, features such as SerializesModels can reduce the amount of model state that needs to be serialized by representing models through identifiers and restoring them when the job is processed.&lt;br&gt;
The goal is not to make dangerous deserialization safe.&lt;/p&gt;

&lt;p&gt;The goal is to reduce the attack surface and add another layer of visibility when someone tries to probe it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Kriosa?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Security controls can fail.&lt;br&gt;
Developers can miss an unsafe unserialize() call. A dependency can introduce an unexpected gadget chain. An old endpoint can remain exposed. A configuration change can create a new trust boundary.&lt;br&gt;
That's why defense in depth matters.&lt;br&gt;
A practical security model can look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Secure code → Input validation → Kriosa detection → Logging &amp;amp; response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer has a different job.&lt;br&gt;
Your application code should prevent the vulnerability.&lt;br&gt;
Your validation should reject unexpected input.&lt;br&gt;
Kriosa can provide additional visibility into suspicious traffic.&lt;/p&gt;

&lt;p&gt;Your logs and monitoring can help you investigate what happened.&lt;br&gt;
Kriosa doesn't replace secure coding. It gives secure applications another layer to defend and monitor their attack surface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Insecure Deserialization Security Checklist&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before shipping a PHP or Laravel application, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do I use unserialize() anywhere in the application?&lt;/li&gt;
&lt;li&gt;Can the serialized data originate from a user or another untrusted source?&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Can an attacker modify cookies, API parameters, cache entries, queue data, or other serialized values?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Can I replace PHP serialization with JSON?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If serialized data must cross a trust boundary, is it authenticated with an HMAC or another integrity mechanism?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Have I avoided treating allowed_classes as a complete security solution?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If classes must be allowed, have I explicitly restricted the permitted classes?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Have I reviewed __unserialize(), __wakeup(), and __destruct() for dangerous behavior?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Have I checked for classes that write files, execute commands, make network requests, modify application state, or perform other sensitive operations?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Have I reviewed third-party dependencies for potential gadget chains?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Have I checked Laravel jobs and other serialized application data for unexpected trust boundaries?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Are suspicious requests being logged and monitored?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Do I have an additional detection layer for malicious application traffic?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't simply to remove one dangerous function.&lt;br&gt;
The goal is to make sure untrusted data never gets the opportunity to become an executable object inside your application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try Kriosa&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require kriosa-ai/kriosa-php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Documentation: &lt;a href="https://kriosa.com/documentation.php" rel="noopener noreferrer"&gt;Kriosa 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;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 &lt;code&gt;{!!&amp;nbsp;!!}&lt;/code&gt; 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 &lt;code&gt;../&lt;/code&gt; escapes your application&lt;br&gt;
Article 9: Command injection in PHP - when &lt;code&gt;exec()&lt;/code&gt; 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 &lt;code&gt;.env&lt;/code&gt; is only the beginning&lt;br&gt;
Article 12: Session security in PHP - what most developers get wrong&lt;br&gt;
Article 13: Rate limiting in Laravel and PHP - how to stop brute force before it starts&lt;br&gt;
Article 14: Security headers in PHP and Laravel - the lines that harden every response&lt;br&gt;
Article 15: IDOR in PHP and Laravel - when changing one number exposes someone else's data&lt;br&gt;
Article 16: This article - mass assignment in PHP and Laravel and when user input becomes more than it should&lt;br&gt;
Article 17: This article - open redirect vulnerabilities in PHP and Laravel&lt;br&gt;
Article 18: PHP type juggling - when 0 equals admin&lt;br&gt;
Article 19: This article - insecure deserialization in PHP and when your cache becomes an attack surface&lt;/p&gt;

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