Unrestricted File Upload Vulnerability and Malicious Script Injection (GuardPay)
A critical Unrestricted File Upload vulnerability was identified in the user and admin profile update functionality, allowing potential malicious script injection.
INCIDENT REPORT - SECURITY BREACH INVESTIGATION
Date: 2026-04-21
Subject: Unrestricted File Upload Vulnerability and Malicious Script Injection (GuardPay)
SUMMARY
During a security audit of a compromised installation, a critical Unrestricted File Upload vulnerability was identified in the user and admin profile update functionality. This vulnerability allowed attackers to upload polyglot PHP scripts (disguised as images) to the public web directory.
VULNERABILITY DETAILS
The vulnerability resided in AccountController.php (both User and Admin versions). While the system performed standard MIME type validation, it relied solely on Laravel's input validation rules which can be bypassed in certain server environments or via polyglot file techniques. The secondary failure was using getClientOriginalExtension() to name the saved file without first cross-verifying it against a strict whitelist of allowed extensions.
This allowed attackers to upload polyglot files—binary files that contain valid image data (rendering as actual images in browsers) but also embed malicious PHP code. Because the system trusted the user-provided metadata, these files were saved with their original executable extensions (e.g., .php56, .phtml) in the public web directory.

EXPLOIT MECHANISM
-
Attacker Action: An attacker would register or log in and upload a malicious polyglot file (e.g.,
edith.jpg). This file was a functional binary image that appeared legitimate when viewed, but contained embedded PHP code. Due to the naming flaw, it would be saved as an executable script (e.g.,edith.php56) through the user profile settings. -
Persistence: The attacker then triggered this script via its public URL (e.g.,
public/assets/images/users/edith.php56). -
Malicious Logic: The script was programmed to:
- Rewrite
resources/views/templates/bento/blades/user/deposits/new.blade.phpwith a malicious crypto payment gateway ("GuardPay").

- Change permissions of
new.blade.phpto444(read-only) to prevent administrators from reverting the changes. - Re-infect the file periodically if triggered by automated bots.
- Rewrite
ROOT CAUSE
Over-reliance on standard MIME type validation and the use of unvalidated, client-provided metadata (getClientOriginalExtension) for file naming, allowing executable scripts to be saved with their original extensions.
REMEDIATION APPLIED
- Security Patch: Both
User/AccountController.phpandAdmin/AccountController.phphave been patched to use theextension()method (which detects extension based on MIME type) and secure, randomized filename generation. - We have disabled license verification in update to allow verification
AUTOMATIC UPDATE GUIDE (RECOMMENDED)
The fastest and safest way to secure your installation is to use the built-in automatic updater:
- Login to your website admin dashboard.
- Navigate to Update in the sidebar.
- Click Start Automatic Update to download and apply the latest security patches.
MANUAL PATCH GUIDE FOR INFECTED USERS
If your installation shows signs of infection (e.g., unauthorized changes to the deposit page), follow these steps IMMEDIATELY:
STEP 1: CLEANUP MALICIOUS FILES
Delete any suspicious PHP files in your public images directory.
Location: public/assets/images/users/
Look for: Files with extensions like .php, .php56, .phtml, .phps, or files that look like images but contain PHP code.
STEP 2: RESTORE VIEW INTEGRITY
If your deposit page has been modified:
- Locate the file:
resources/views/templates/bento/blades/user/deposits/new.blade.php - If permissions are locked (444), change them back to 644 (or your standard writable permission).
- Restore the file from a clean backup.
STEP 3: APPLY CODE PATCH
Update your AccountControllers to prevent re-upload.
File: app/Http/Controllers/User/AccountController.php
Replace your profileUpdate method with the following securely patched version:
/**
* Update user profile settings.
*/
public function profileUpdate(Request $request)
{
$user = Auth::user();
$rules = [
'lang' => 'required|string|max:10',
'photo' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
];
// Only allow username update if it's currently unset in DB (bypass accessor)
if (empty($user->getRawOriginal('username'))) {
$rules['username'] = [
'required',
'string',
'alpha_dash',
'min:3',
'max:255',
Rule::unique('users')->ignore($user->id),
];
}
$validated = $request->validate($rules);
if ($request->hasFile('photo')) {
$photo = $request->file('photo');
// Extra precaution: check the client-reported extension
$allowedExtensions = ['jpeg', 'png', 'jpg', 'gif'];
if (!in_array(strtolower($photo->getClientOriginalExtension()), $allowedExtensions)) {
return response()->json(['status' => 'error', 'message' => __('Invalid file extension.')], 422);
}
$extension = $photo->extension();
$filename = time() . '.' . $extension;
$photo->move(public_path('assets/images/users/'), $filename);
$validated['photo'] = 'assets/images/users/' . $filename;
// Delete old photo if it exists
if ($user->photo && file_exists(public_path($user->photo))) {
@unlink(public_path($user->photo));
}
}
$user->update($validated);
if ($request->ajax()) {
return response()->json([
'status' => 'success',
'message' => __('Profile updated successfully.'),
]);
}
return back()->with('success', __('Profile updated successfully.'));
}
File: app/Http/Controllers/Admin/AccountController.php
Replace your profileUpdate method with the following securely patched version:
/**
* Update admin profile.
*/
public function profileUpdate(Request $request)
{
$admin = Auth::guard('admin')->user();
$request->validate([
'name' => 'required|string|max:255',
'username' => 'required|string|max:255|unique:admins,username,' . $admin->id,
'email' => 'required|email|max:255|unique:admins,email,' . $admin->id,
'lang' => 'required|string|in:' . implode(',', array_keys(config('languages'))),
'image' => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
]);
$admin->name = $request->name;
$admin->username = $request->username;
$admin->email = $request->email;
$admin->lang = $request->lang;
if ($request->hasFile('image')) {
$image = $request->file('image');
// Extra precaution: check the client-reported extension
$allowedExtensions = ['jpeg', 'png', 'jpg', 'gif'];
if (!in_array(strtolower($image->getClientOriginalExtension()), $allowedExtensions)) {
return response()->json(['status' => 'error', 'message' => __('Invalid file extension.')], 422);
}
// Delete old image
if ($admin->image) {
Storage::disk('public')->delete('profile/' . $admin->image);
}
$extension = $image->extension();
$filename = time() . '_' . bin2hex(random_bytes(5)) . '.' . $extension;
$image->storeAs('profile', $filename, 'public');
$admin->image = $filename;
}
$admin->save();
if ($request->ajax()) {
return response()->json([
'status' => 'success',
'message' => __('Profile updated successfully.')
]);
}
return back()->with('success', __('Profile updated successfully.'));
}
Report generated by Lozand Security Audit Team
Report generated by Lozand Security Audit Team