TL;DR
- You will not build something unbreakable. The goal is to shrink the exploitable surface and limit the blast radius when something does get through.
- A handful of issues cause most real breaches: broken access control, injection through code that bypasses the framework, leaked secrets, and outdated dependencies.
- Frameworks block most injection for free — until you reach for raw SQL or unescaped output. The danger is in the code that opts out of the safe path.
- Authorize every action on the server. Hiding a button is not access control; an attacker calls the endpoint directly.
The worst security incident I have had to help clean up was not a clever exploit. It was a database password sitting in a committed .env file in a repository someone assumed was private. No zero-day, no skilled attacker — just a credential that should never have been in git, found and used. That is what most real breaches look like: not movie hacking, but a boring, preventable gap that nobody owned.
“Security is important” is not advice. This is the version I would actually give a PHP, Laravel, or WordPress developer: a short threat model and the specific gaps that get sites compromised, in roughly the order they actually happen.
Think in terms of surface and blast radius
Perfect security is not the target, because it does not exist. Two questions get you most of the value. First, what is the exploitable surface — every place untrusted input reaches your code, every credential, every dependency you did not write? Second, when one of those fails, how far does the damage spread? A leaked read-only API key is a bad day; a leaked database root password is a catastrophe. Most good security decisions come from shrinking the first and containing the second, not from chasing a perfect score.
Broken access control: the quiet number one
The most common serious flaw is also the least dramatic: code that checks whether you are logged in but not what you are allowed to touch. The classic shape is an endpoint that trusts an ID from the request:
// Anyone logged in can read anyone's invoice
public function show($id)
{
return Invoice::findOrFail($id);
}Change the ID in the URL and you are reading someone else’s data. Hiding the link in the UI changes nothing, because the attacker calls the endpoint directly. Authorization has to happen on the server, for every action, against the current user:
public function show(Invoice $invoice)
{
$this->authorize('view', $invoice); // policy checks ownership
return $invoice;
}Hiding the UI is not access control
If the only thing stopping a user from an action is that the button is not rendered, there is no control at all. Every endpoint must verify the current user is allowed to act on the specific resource — server-side, every time.
Injection: frameworks protect you until you opt out
SQL injection and cross-site scripting are old, and modern frameworks block them by default — which is exactly why the remaining cases come from code that steps off the safe path. Eloquent and query bindings parameterize SQL for you; the vulnerability appears when someone concatenates input into a raw query:
// Vulnerable: input concatenated into SQL
DB::select("SELECT * FROM users WHERE email = '$email'");
// Safe: parameter binding
DB::select('SELECT * FROM users WHERE email = ?', [$email]);XSS is the same story on output. Blade’s {{ }} escapes automatically; the danger is {!! !!}, which prints raw HTML. In WordPress it is echoing user data without esc_html() or esc_attr(). The rule that prevents most of it: never trust input on the way in, always escape on the way out, and treat every {!! !!} or unescaped echo as a place that needs justifying.
Secrets and dependencies: the boring breaches
The two gaps behind a large share of real-world compromises have nothing to do with application logic. The first is leaked secrets — credentials in a committed .env, an API key pasted into a repo or a chat. Keep secrets out of version control, and if one leaks, rotate it immediately and treat it as fully compromised; a private repo is not a safe place for a live credential. I wrote about exactly this failure mode in accidentally pushing secrets to git.
The second is outdated dependencies — and for WordPress specifically, plugins are the single largest breach vector. A known vulnerability in an unpatched plugin is a published, scriptable exploit. Treat updates as security work, not chores: run composer audit, keep plugins current, and remove the ones you do not use. On a Bedrock-style setup, locking down browser-side file modifications in production (DISALLOW_FILE_MODS) keeps the plugin set under version control where you can audit it.
Trust no payload from outside
Any request from an external system is untrusted until proven otherwise, including webhooks. A payment or integration webhook that you act on without verifying its signature is an open endpoint anyone can POST forged events to. Verify the signature before doing anything with the body — the same point I make about Stripe in the Laravel Cashier guide. This gets dramatically easier when external integrations live behind one boundary you can audit, rather than scattered across controllers — the structural argument in dependency injection and dependency inversion.
Headers and transport: real, but defense in depth
HTTPS everywhere, HSTS, and a content security policy are worth having — they are a meaningful layer, and CSP in particular limits the damage of an XSS that slips through. But they are defense in depth, not the main event. A strict CSP does not save an endpoint with broken access control. Put the headers in place, then spend your attention on authorization, injection, secrets, and dependencies, where the actual breaches happen.
Mistakes that lead to real breaches
Authorizing in the UI only
Hiding a control is not a check. Verify permissions server-side on every endpoint against the specific resource.
Raw SQL with interpolated input
Concatenating request data into a query reopens SQL injection the framework had closed. Use bindings or the query builder.
Unescaped output
{!! !!} in Blade or an unescaped echo in WordPress prints raw user input. Escape on output by default.
Secrets in the repository
A committed credential is compromised, private repo or not. Keep secrets out of git and rotate any that leak.
Stale plugins and packages
Unpatched dependencies are published exploits. Update routinely and remove what you do not use.
Trusting webhook payloads
An unverified webhook endpoint accepts forged events. Verify the signature before acting on the body.
Security is also a process
None of this survives as a one-time “security pass” before launch. The teams that stay secure bake it into the routine: secrets in a manager and out of git, dependency updates on a schedule, least-privilege access for credentials, and backups you have actually tested restoring. Tested restores matter — an untested backup is a guess, and you find out which it was during the incident. Built into normal delivery, this produces safer products without a separate security phase that slows everything down.
FAQ
- What is the most common web application security vulnerability?
- Broken access control — endpoints that confirm a user is authenticated but not that they are authorized to act on the specific resource. Changing an ID in a request to read someone else’s data is the classic case. Authorization must be enforced server-side on every action.
- Does using Laravel or WordPress make my site secure by default?
- They prevent a lot — Eloquent parameterizes SQL, Blade escapes output — but only while you stay on the safe path. Raw queries with interpolated input,
{!! !!}unescaped output, leaked secrets, and outdated plugins all bypass those protections. The framework reduces risk; it does not remove your responsibility. - What should I do if I accidentally committed a secret to git?
- Treat it as fully compromised and rotate it immediately — change the password or revoke the key, even if the repository is private. Removing the file in a later commit does not help, because the value remains in git history and may already have been copied.
- Are security headers like CSP enough to secure a site?
- No. Headers such as HSTS and a content security policy are valuable defense in depth and limit the damage of some attacks, but they do not fix broken access control, injection, or leaked credentials. Use them in addition to, not instead of, server-side authorization and safe input handling.
Related articles
- Accidentally pushed secrets to git walks through the leaked-credential failure mode and how to recover from it.
- Laravel Cashier and Stripe covers verifying webhook signatures so external payloads cannot be forged.




