Top 10 Laravel Audit Security Issues
Published on by Stephen Rees-Carter
Since the start of 2022, I’ve spent my time conducting Security Audits and Penetration Tests for Laravel applications. I’ve audited apps of all sizes, from tiny apps with a handful of controllers, right through to huge apps with a multitude of modules, and many different coding styles and app structures.
Amongst all of that, there has been a pretty clear trend that holds true for the apps that I audited: Laravel is pretty secure, but it’s easy to overlook the little things, the extra layers of defence, and leave a weakness hiding somewhere.
In fact, it’s fair to say that the vast majority of issues I’ve found during my audits were either something simple that was overlooked in a single route, or are extra layers of security that developers were either not aware of, or not comfortable implementing.
So let’s dive into it and check out the Top 10 most common security issues I discovered doing my security audits.
#10 → Insufficient Input Validation
It’s common to find insufficient input validation scattered throughout controllers. Often there will be a validator checking expected input keys (although sometimes there isn’t even that), and then request()->all()
will be in use immediately after, submitting data into models, events, etc. Without proper validation, it’s easy to inject malicious values to cause errors, injection attacks, and more.
For example, the usual mass-assignment defence for this is considered to be $fillable
on the model, but if $fillable
includes the admin
flag for the admin portal, then anyone can create a new admin user from the registration page… (and yes, this is a real example.)
#9 → Missing Subresource Integrity (SRI)
Subresource Integrity (SRI) adds a layer of protection against compromised 3rd party scripts affecting your site, and in my opinion is seriously underused. The risk is that 3rd party scripts and styles can be modified to include malicious code, such as Magecart, keyloggers, cryptominers, etc. If you have the compromised script included on your site, then your users are at risk even though your app wasn’t compromised.
SRI works by defining an integrity hash on each <script>
& <style>
tag that loads a 3rd party resource, which the browser verifies before loading the resource. If the hash doesn’t match, the resource is blocked.
Most of the sites I audited didn’t load resources from 3rd parties and thus didn’t need SRI anyway, otherwise I’d expect this to be much higher on the list!
#8 → Insufficient Rate Limiting
Rate limiting is essential for limiting bot attacks, and preventing abuse, and yet it's common to find endpoints that are lacking in rate limiting. This is of particular concern when it comes to routes like authentication, or querying for sensitive information like the existence of user accounts. For example, I discovered an SMS-based Multi-Factor Authentication (MFA) route was lacking rate limiting - and the 6-digit code expired in 5 minutes. It was trivial to brute-force a valid code and force my way in. 😈
That said, rate limiting can be tricky to get right - do you base it on IP, or username, or both? Or something else? It depends on the route, but having some is definitely better than none. So don’t overlook it!
#7 → Cross-Site Scripting (XSS)
One of the most unsurprising entries in the Top 10, but it’s probably a lot lower than you were expecting! XSS usually popped up on a single route through a single input, due to something like Markdown (which is insecure by default) or limited formatting that suffered from a subtle escaping bypass.
The best strategy here is to look out for those pesky unescaped blade tags ({!! … !!}
) and raw HTML directives like v-html
, and ensure all uses are properly escaped, that Markdown’s security features are enabled, and user-supplied HTML is purified. I recommend avoiding these tags as much as possible, so any uses really stand out and can be easily identified and reviewed.
#6 → Outdated & Vulnerable Dependencies
Another unsurprising entry… Just when WAS the last time you ran composer/npm update?
It’s common to delay dependency updates, to avoid breaking things and keep systems stable, but vulnerabilities in dependencies can leave your apps exposed without you doing anything. It’s also common to include a multitude of dependencies, but every dependency increases the potential vulnerabilities you need to be aware of.
I recommend keeping everything updated weekly or monthly, and using tools like composer audit --locked
and npm audit
in your build system to block deploys when vulnerabilities are discovered. I also suggest trimming down your dependencies to the minimum, and anything that can be easily replaced by a simple in-house middleware or wrapper, should be.
#5 → Insecure Function Use
I’ve lost count of the number of times I’ve seen md5(time())
(and md5(microtime())
!) used since I started working in PHP - I probably even wrote a few of them in my early years! And disappointingly, it’s a trend that continues to this day. Worse, it’s often used for generating random tokens or unique filenames. Except it’s neither random, nor unique. It’s incredibly easy to guess and brute-force, and collision attacks can also be quite trivial too.
I bet if you go into your codebase right now and search for md5(
, you’ll find it used insecurely somewhere… Seriously, I have trauma over this one!
It’s not just md5(time())
specifically, but other functions like rand()
and array_rand()
which aren’t cryptographically secure and should not be trusted for anything that requires security.
Always use proper secure random number generators, random_int()
and Str::random()
, to safely generate random values.
#4 → Missing Security Headers
Now we’re getting into the additional layers of protection that aren’t well understood. Web browsers include a bunch of really awesome security features, you just need to enable them on your site through the use of response headers. While their absence doesn’t directly open your site to be hacked, they help prevent things like clickjacking, referrer information leaking, XSS and other injection attacks, HTTPS downgrade attacks, etc. Enabling many of these security headers is trivial, but most sites don’t even enable the easy ones… 😭
The best suggestion I can give you is to head over to securityheaders.com and scan your site. It’ll list all of the headers you’re missing, and give you links to resources to learn more about adding them.
#3 → Missing Content Security Policy (CSP)
Content Security Policies are so important I’ve given them their own spot in the Top 10. CSPs are a secondary line of defence against XSS and clickjacking, and give you visibility over what scripts, styles, fonts, forms, frames, etc, run on your app. The CSP tells the browser what resources are allowed to be used on the site, and it will block and/or report any violations of the policy - preventing XSS attacks and providing you visibility of what’s happening on your site.
They are often dismissed as too hard or “break stuff”, however deploying a Report-Only policy provides you with full visibility without breaking anything. So this is definitely the way to go when you’re starting out! The easiest way to set one up is to use the CSP Wizard in Report URI.
I’ve published my CSP middleware as a simple way to get started with CSPs: https://gist.github.com/valorin/d4cb9daa190fdee90603efaa8cbc5886
#2 → Missing Authorisation
Oh boy, this one encompasses a bunch of stuff around Authorisation (and authentication in a way). I’ve seen: Insecure Direct Object References (IDOR), missing signed
, auth, and policy middleware, forgotten authorize()
calls, webhooks without validation, etc… Ultimately, the code wasn’t actually checking if the requestor was allowed to do what it wanted to do.
I was actually surprised to see this so high on the list, but it turns out that it’s quite common for most projects to have a single one of these hiding somewhere, waiting to be exploited. It’s usually a simple case of the developer forgetting to add in a line, but it potentially leaves a massive hole.
My best recommendation here is to include tests for authentication and authorization on each route, alongside your other tests. That way you’re checking valid and invalid permissions as part of your standard testing flow, and you’ll notice if authorization is missing because a test will pass when it’s supposed to fail.
#1 → Exposed API Keys & Passwords
How many times do I have to say this? Don’t commit secrets into Git! 😡
This was the unsurprising champion of the list: API keys and passwords committed into version control and scattered across codebases. It is so ridiculously common, I’m honestly surprised when I don’t come across this during an audit.
Consider where your code goes… It’s on all of your developers machines, shared with contractors, up on GitHub, Bitbucket, GitLab, etc, in 3rd party services and build tools. It’s scattered across many different places. While your API keys unlock Billing, file storage, Personally Identifying Information, backups, infrastructure, etc. So many pieces of sensitive information and access, and if it falls into the wrong hands, your reputation is gone.
I know there are tutorials out there which recommend committing these things, so I can see why it happens. But it’s something we need to work hard to stop as a community.
If you do accidentally commit credentials, make sure you revoke it at the source. This will prevent anyone from using it if they find it. I also suggest using tools like Gitleaks and TruffleHog to find secrets in your codebases.
BONUS → Missing security.txt file!
It's not a security issue as such, so I’ve included it as a bonus, but adding a security.txt
file to your app is one of the simplest and best things you can do for your security.
security.txt
is a text file that you put at /.well-known/security.txt
, which provides your (security) contact details in case someone finds a security issue with your site. This makes it super easy for security folks to contact you, reducing the time and effort it takes to report issues so you can get them fixed faster.
Generate one here: https://securitytxt.org
Summary
Ok my friends, that’s my top 10! Were there any surprises? Things you need to check in your own apps? Maybe there was something you disagree with? Head over to the thread on Twitter, Fediverse, or Substack Notes to join the discussion!
The key takeaway for me is that while Laravel is secure, it’s all the little things which are overlooked and missed. It’s easy to assume you’ve checked authorization on every route, and your output is fully escaped, but there might be that one instance you’ve missed. This is the benefit of security audits - getting someone who doesn’t make assumptions to review your code. It’s also what I teach over in my Practical Laravel Security course.
Friendly Hacker, Speaker, and PHP & Laravel Security Specialist.🕵️ I write Securing Laravel and hack stuff on stage for fun. 😈