Updated August 2026. This note has been around since 2005; the idea underneath it has not aged.
If you remember one thing about web application security, make it this: almost every vulnerability is the same shape. Untrusted input reaches a place that trusts it. SQL injection, cross-site scripting, file inclusion, command execution, insecure deserialization. They look like different bugs, but they are all input arriving somewhere it should not have been allowed to go. Once you see that shape, security stops being a list of tricks to memorise and becomes a single question you ask over and over.
Why input is the root of it
PHP is a thin, flexible layer sitting on top of dozens of separate libraries: a database driver, an HTML renderer, a filesystem, a shell, a serializer. Each of those has its own idea of what a dangerous character is. A quote mark is harmless in a paragraph of text and catastrophic in an unescaped SQL string. A pair of angle brackets is nothing in a database column and an attack in an HTML page. Data that is perfectly safe for one destination is a weapon at another. So there is no such thing as “clean” input in the abstract. There is only input that is safe for a specific destination.
Three questions to ask of any input
When you are reviewing code, or writing it, run every piece of external data through three questions:
- Where did it come from? Anything from outside your own code is untrusted: request parameters, headers, cookies, uploaded files, API responses, even values you stored earlier that originally came from a user.
- Where is it going? The database, an HTML page, the shell, the filesystem, a template, a serializer. The destination decides what “safe” means.
- What does the destination trust? Match the defence to the sink. The database wants parameter bindings. HTML wants escaping. The filesystem wants validated, constrained paths. The shell wants you to not be there at all.
The sinks and their defences
- The database. Never build a query by pasting values into a string. Use prepared statements with bound parameters, so the value can never be read as SQL. This single habit removes the entire class of SQL injection.
- HTML output. Escape on the way out, at the point of rendering, so a value that contains markup is shown as text rather than executed as a script. Escape for the context (HTML body, attribute, URL, JavaScript), because each has different rules.
- The shell and filesystem. Avoid passing user input to the shell at all. When you must touch the filesystem with a user-supplied name, validate it against an allow-list and never trust it to stay inside the directory you expect.
- Deserialization. Do not unserialize untrusted data. A crafted payload can instantiate objects you did not intend and trigger code through their lifecycle methods.
How frameworks changed the game
The reason a modern framework feels safer is not magic. It is that the framework makes the safe path the default path. Laravel’s query builder binds parameters for you, so ordinary code is injection-safe unless you deliberately drop to raw SQL. Blade escapes output with {{ }} by default, so XSS mostly returns only when you reach for the unescaped {!! !!}. Form request validation gives you an allow-list of acceptable input before a controller ever runs. The framework did not remove the underlying danger; it moved the danger to the moments when you step outside the defaults, and it made those moments visible. Good security practice is theory made into a default you can see.
That is the whole discipline in one sentence: treat all input as untrusted, and defend it at the destination, not at the door. Everything else, across every framework and every year, is a variation on that theme.
Related
- Laravel Security Review: a practical walk through reviewing and hardening a Laravel application, area by area, with every guide in this cluster in one place.
- Preventing the OWASP Top 10 in Laravel: injection, XSS and the rest, mapped to concrete Laravel practice.
- A Laravel Security Checklist for Production