In this blog post, we're gonna take a closer look at some common Laravel security mistakes that I've encountered in projects, personally made or even came across on Stack Overflow approved answers. We'll briefly talk about file validation, mass assignment, and Laravel’s query builder. The goal of this blog is to demonstrate how small, easy to avoid mistakes can have a big impact on your application’s security. And to hopefully prevent you from making them in the future. So, let’s get started!
Securing Laravel 101
File validation
Laravel offers a robust and secure file system that enables developers to store and retrieve files from either local or cloud storage. Below is some code, straight from Stackoverflow, to handle a user uploading an avatar to their profile:
Can you spot how an attacker could abuse the following snippet?
public function storeImage(Request $request)
{
$request->validate([
'image' => 'required|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
]);
$imageName = Str::random(16) . '.' . $request->image->getClientOriginalExtension();
Storage::disk('public')->putFileAs("avatars", $request->image, $imageName);
return back()->with('success','You have successfully uploaded your avatar!');
}
Say, an attacker wanted to upload a .html file with an XSS (Cross-site scripting) payload to your server. You’d expect Laravel’s server-side validation image
and mimes:jpeg,png,jpg,gif,svg
to take care of this.
Sidenote: This isn’t a bug in Laravel but simply how mime type checking works.
The crucial mistake being made is using Laravel’s getClientOriginalExtension
method to retrieve the file’s extension directly from the request. Instead, in this scenario we want to use:
$request->image->extension();
This will guess the file’s mime type based on the contents of the actual file rather than what’s being received from the client.
Another safe way to handle this is to use Laravel’s put
method to store files:
Storage::disk('public')->put(Str::random(16), $request->image);
This will automatically replace your filename with a random hash and append the correct extension.
The key takeaway here is to never trust user input, especially when it comes to file uploads.
Mass assignment
Can you spot the security flaw in this code?
User model
class User extends Model {
protected $fillable = ['username', 'email', 'password', 'role'];
}
Register controller
class RegisterController
{
public function create(Request $request)
{
$request->validate([
'username' => 'required|string',
'email' => 'required|email|unique:users',
'password' => 'required|string|min:12|confirmed',
]);
$user = new User();
$user->role = 'guest'
$user->fill($request->all());
$user->save();
return response()->json(['success' => true],201);
}
}
If a malicious user were to forge the register request and add "role": "admin"
to the form payload. Laravel would happily grant this user access to your entire system.
This is due to $request→all()
not only taking validated data from the request but ALL data. In combination with role
being added to the $fillable
property of the model, the user would be created with the role "admin"
.
Prevent this by either using $request→validated()
to only retrieve validated input from your request. Or ensure your fillable properties are set up correctly.
Bonus points if you spotted the password being stored in plain text ;)
Query builder parameter binding
SQL injection is a common attack vector for web applications, and Laravel is no exception. It occurs when an attacker is able to input malicious SQL code into a web application's input fields or query strings.
Here is an example of SQL injection using Laravel's query builder:
//User input
$search = "1; DROP TABLE users;";
DB::table('users')->whereRaw("name = " . $search)->get();
This code is vulnerable to SQL injection because it allows the attacker to execute arbitrary SQL code by setting the $search
variable to a string that includes SQL commands. In this case, the attacker could delete the entire users
table by setting $search
to "1; DROP TABLE users;"
.
To prevent SQL injection attacks, always use Laravel's query builder or parameter binding when constructing SQL queries.
Example of parameter binding:
$search = "1; DROP TABLE users;";
DB::table('users')->whereRaw("name = ?", $search)->get();
Conclusion
Though the examples above might be quite obvious to some, I hope this article illustrates the importance of having a solid understanding of the inner workings of a framework, even when it appears to handle security seamlessly. Despite Laravel's beginner-friendly nature and minimized overhead for developers, don’t forget to remain cautious and avoid complacency.