Laravel application security: CSRF, XSS, SQL injection and OWASP
Author:
Paweł Matusiak
·
Site and panel breaches are not bad luck. They come from skipped defences. Laravel ships with protection against the most common attacks — if someone actually uses it.
Business owners rarely ask about CSRF tokens. They ask whether someone can steal the customer database, peek at the price list, or knock the panel over on a Friday night. Those questions map to a few classic attack types — and Laravel defends against them out of the box, if the app is built properly. The framework is not an amulet. A misconfigured Laravel with debug on in production, secrets in Git and a shared office account will leak just like an old PHP script.
At GESOFT security is not a leftover budget line. It is a checklist on every panel that holds orders, invoices or personal data. Below: three attacks that still work on cheap sites, the full OWASP Top 10 2025 map (the current list in 2026) and what we add beyond framework defaults. If you already have an old panel and do not know whether these things are in place, start with a PHP security audit, not another feature.
Three attacks that still work on cheap websites
SQL injection — stealing data through a form
An attacker pastes a malicious SQL fragment into search, login or a URL parameter. A poorly written app concatenates that text into a query and runs it. Result: a leaked customer table, a login bypass, sometimes wiped data. In Laravel, Eloquent and the query builder use bound parameters (PDO prepared statements). Raw SQL from a form never reaches the engine as code. We use raw queries (DB::raw, whereRaw) only rarely — always with bindings, never with string glue.
Request validation is the second line: type, length, an allow-list of values. We do not trust what arrived from the browser, even if the Vue frontend does not let you type letters into a number field. The API will reject a clever call from another client. The same applies to sorting and filters — a column name for ORDER BY never comes straight from the query string without an allow-list.
XSS — a malicious script in the user browser
If the app renders a customer name, a comment or a ticket body without escaping, someone can inject a script that steals a session, swaps the account number on an invoice or fires a request as the logged-in salesperson. Laravel Blade escapes variables in {{ }} by default. We use raw {!! !!} only for content we generated ourselves. Vue.js also escapes unless you deliberately use v-html. We never do that with user data — not in the panel, not in HTML mail built from form fields.
CSRF — an action in your name
The user is logged in. They open another site that silently sends change password, delete invoice or transfer funds to your app. The browser attaches the session cookie because it is the same origin or SameSite is wrong. Laravel requires a CSRF token on forms and state-changing requests (POST, PUT, PATCH, DELETE) — without it the server rejects the call. In a Sanctum cookie SPA the same mechanism works with the X-XSRF-TOKEN header; we do not expose mutating endpoints without that pair. GET does not change state — an old rule that is still broken.
OWASP Top 10:2025 — a checklist, not marketing
OWASP Top 10 is the standard awareness document for web-app builders. The 2025 edition (the one that applies when you read this in 2026) shifted the emphasis: broken access control is still on top, but misconfiguration and software supply-chain failures moved high. At GESOFT we treat the list as a checklist in review and in an audit, not as a slogan.
- A01 Broken Access Control — still number one. Laravel roles and policies, permission tests on CI, no secret URLs as the only defence. An obvious id in the path (/invoices/1842) does not mean you may see it.
- A02 Security Misconfiguration — jumped from 5th (2021) to 2nd. Debug off in production, separate environments, least-privilege database users, no directory listing, secrets only in .env.
- A03 Software Supply Chain Failures — a new, broad category: Composer/npm dependencies, the CI pipeline, updates. Regular composer audit, no abandoned packages, lockfile in Git, secrets outside the repo.
- A04 Cryptographic Failures — bcrypt/argon2id passwords, HTTPS everywhere, no passwords or API keys in code or in a dump. Especially sensitive fields are encrypted at rest when the model requires it.
- A05 Injection — ORM, validation, allow-lists. Not only SQL: also OS commands and headers. File uploads are checked by content, not by the extension the client sent.
- A06 Insecure Design — threats are thought through in analysis, not after go-live. A shared office account, no retention, a one-click full-database export without a second factor — those are design flaws, not a missed hotfix.
- A07 Authentication Failures — login rate limits, 2FA where the stakes are high, expiring sessions, password reset with a one-time link. More in authentication and 2FA in Laravel.
- A08 Software or Data Integrity Failures — we do not trust inbound deserialization, we do not pull packages from an unknown source, we do not run an unsigned webhook.
- A09 Security Logging and Alerting Failures — an audit log (who, when, which IP) plus an alert, not only a file nobody reads. Passwords and national IDs do not land in logs.
- A10 Mishandling of Exceptional Conditions — new in 2025. An error must not dump a stack trace in production or leave a payment in an undefined state. Transactions, payment idempotency, a readable message with no infrastructure detail.
What we add beyond Laravel defaults
- Rate limiting on login, forms and APIs — makes password guessing and spam harder. Per IP and per account, with a growing backoff, not an infinite wait.
- Security headers: HTTPS and HSTS, frame restrictions (clickjacking), Referrer-Policy, a sensible CSP where the panel allows it.
- Audit logs: who changed sensitive data, when, from which IP. Log retention decided up front, no passwords or full identifiers in the body.
- Database and file backups off the production server, with a tested restore. An untested backup is not a backup.
- Two-factor login where the panel touches money or personal data — mandatory for admin and accounting.
- Server hardening: firewall, SSH keys only, OS updates, a dedicated least-privilege database user, .env outside the document root.
The supply chain (A03) in practice means: lockfile in the repository, updates after CVEs, no leftover scripts in public/, no forgotten phpMyAdmin. Configuration (A02) means a separate production .env, APP_DEBUG=false, and we never commit .env to Git. Those two 2025 items close more real SME incidents than an exotic crypto attack.
Panels, APIs and bugs you notice too late
Hiding a button in the Vue menu is not access control. A Laravel policy must reject the request even if someone calls /api/payroll/export by hand. The automated test a salesperson cannot download the payroll report should be green on CI. That is A01 in practice — and the most common issue we find in audits of old panels.
Exceptions (A10) are underrated until the payment gateway times out and the system issues an invoice twice or not at all. We design idempotency: the same webhook does not book a second payment. The user sees try again, not a PDOException dump. In production we log detail on the server, not in the HTTP response.
GDPR and security overlap: encryption in transit, least privilege, breach reporting. Having SSL does not close Article 32. How we design consents, retention and the right to erasure is in GDPR in web applications. A new Laravel project gets these controls from day one, with Git and 6 months of warranty.
Hosting and deployment sit on the same list. The public/ directory must not serve .env, .git or SQL dumps. PHP on an unsupported version (7.2, 7.4 after EOL) is an open door regardless of how tidy the business code is. HTTPS with a redirect from HTTP, auto-renewed certificates, SSH keys only — boring controls that stop mass scans. In WordPress the attack surface grows with every plugin; in Laravel it grows with every careless dependency and every endpoint without a policy. The scale is different, the habit is the same: do not add what you will not maintain.
If you already have a PHP app and are not sure these things are in place, tell us what you need to protect. We will say exactly where to start: a quick review, a full audit or a repair plan, before someone else finds the same hole. A new project gets these controls from day one, with Git and 6 months of warranty, without security as an optional extra at the end of the quote.
Mass assignment, uploads and what the ORM will not close for you
Eloquent stops SQL injection on ordinary where/create. It does not stop mass assignment: request->all() poured into Model::create without $fillable, or with $guarded = [], will set is_admin=1 if someone adds a field in JSON. At GESOFT models have an explicit field list and the Form Request has a separate allow-list of keys. That is A01 and A05 at once: broken access control plus logic injection, not only SQL. Same for sorting: an ORDER BY column name never comes straight from the query string.
Uploads are classic RCE on cheap panels: file.php.jpg, double extensions, client-supplied MIME, a script in /public/uploads. We check content, not the declaration. Files land outside the document root or behind a one-time signed URL. Thumbnails and PDFs are built by a worker, not by the user request with Imagick on an untrusted blob (the ImageMagick / Ghostscript CVE history is long). Size and count limits on the endpoint — rate limits are not only for login.
Headers, cookies and config that leaks by itself
A02 2025 (Security Misconfiguration) outranked injection because debug, directory listing, default passwords and CORS * are more common than hand-glued SQL. In production: APP_DEBUG=false, HTTPS with HSTS, session cookie Secure + HttpOnly + SameSite=Lax (Strict where the panel is not reached from mail links), the server does not advertise a PHP version. CORS only for known SPA origins. phpinfo() and Telescope do not exist in production. .env, .git and storage/logs are not served from public/.
Secrets: APP_KEY, the database password, gateway keys — environment only, rotated after an admin leaves and after an incident. APP_KEY in Git equals the ability to decrypt cookies and encrypted casts. We commit composer.lock; composer audit on CI. An abandoned package (last commit three years ago, no PHP 8) is removed or replaced. That is A03 supply chain — the same class of risk as a WordPress plugin, on a smaller surface.
IDOR, BOLA and the test you do not see in the menu
A hidden Vue button is not access control. /api/invoices/1842 with a guessed or sequential id is BOLA/IDOR, straight under A01. A Laravel policy (InvoicePolicy@view) must reject the request even when the token is valid. Automated test: user A does not read user B invoice; a salesperson does not export payroll; a guest does not hit /api/admin. We run that on CI, not by eye after a click. In audits of old panels this finding lands more often than broken bcrypt.
A10 2025 (Mishandling of Exceptional Conditions) is a gateway timeout, a duplicate webhook, a full disk mid-upload, a worker that died halfway through a job. A database transaction, a payment idempotency key, retries with backoff, a dead-letter queue with an alert. The user sees try again, not a PDOException stack. The server log has a request-id, not a password and not a national ID. GDPR and security meet here: a leak in the log is a breach even when SQL injection never happened.
Frequently asked questions
- Is Laravel secure out of the box?
- It has CSRF, XSS and SQL injection defences if you use the ORM, templates and validation. Security is also config, updates, permissions and the supply chain — the framework will not switch those on for you. Debug in production and secrets in Git wipe the rest of the benefit.
- What is OWASP Top 10 2025?
- The current list of the most serious web-app risks. In 2025 the top items are: broken access control, security misconfiguration, software supply-chain failures, cryptography, injection, insecure design, authentication failures, integrity, logging/alerting, and mishandling of exceptional conditions.
- Can an old PHP panel be secured without a rewrite?
- Often yes: PHP update, login limits, HTTPS, backups, headers. We rewrite when the framework is abandoned or nobody can add a feature safely. We start with an audit, not a rewrite everything slide.
- How much is a security audit?
- It depends on code size and whether we check the server. A typical panel — a report in a few working days, quoted after we see the scope. No single catalogue price. Tell us which system you want checked.
- Do I need 2FA on every panel?
- For admins and accounting — yes. Low-privilege accounts can stay optional. We pick TOTP or SMS for the risk, not the trend. Details in the 2FA article.
- What about Composer packages?
- Every dependency is attack surface (A03). We keep a lockfile, update after CVEs, and skip abandoned packages. It is a different scale from WordPress with 40 catalogue plugins, but ignoring composer audit ends the same way.
- Is SSL enough for security and GDPR?
- No. HTTPS closes transport. You still have permissions, secrets, backups, retention, logs and the supply chain. SSL is necessary and not sufficient.
- Do I get the code and a warranty after go-live?
- Yes — a Git repository, documentation and 6 months of warranty on the delivered work. Security updates after that period can sit in a care package.
Related service:
Laravel and Vue.js applications
Describe your project