Zoomed Evidence
April 21, 2026 Critical Severity Status: Resolved

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.

Evidence of malicious PHP files disguised as images in the public directory

EXPLOIT MECHANISM

  1. 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.

  2. Persistence: The attacker then triggered this script via its public URL (e.g., public/assets/images/users/edith.php56).

  3. Malicious Logic: The script was programmed to:

    • Rewrite resources/views/templates/bento/blades/user/deposits/new.blade.php with a malicious crypto payment gateway ("GuardPay").

    Malicious GuardPay overlay on the hijacked deposit page

    • Change permissions of new.blade.php to 444 (read-only) to prevent administrators from reverting the changes.
    • Re-infect the file periodically if triggered by automated bots.

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

  1. Security Patch: Both User/AccountController.php and Admin/AccountController.php have been patched to use the extension() method (which detects extension based on MIME type) and secure, randomized filename generation.
  2. 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:

  1. Login to your website admin dashboard.
  2. Navigate to Update in the sidebar.
  3. 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:

  1. Locate the file: resources/views/templates/bento/blades/user/deposits/new.blade.php
  2. If permissions are locked (444), change them back to 644 (or your standard writable permission).
  3. 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

Your Cart

Cart is empty

Subtotal $0.00