# Malware scan

> Scan hosting accounts for webshells, backdoors and injected code, quarantine or ignore findings, restore a file, and clean up a hacked website step by step.

Source: https://zopanel.net/docs/malware-scan  
Updated: 2026-10-09

**Malware scan** checks the website files of hosting accounts for webshells, backdoors, obfuscated PHP and injected JavaScript. Every account is scanned automatically once a week, and administrators, resellers and customers can start a scan at any time. Findings can be moved to a quarantine that the account cannot reach, or marked as false positives.

## Who can use it

| Role | What they see and do |
| --- | --- |
| Administrator | All accounts. Scan one account or all of them, quarantine, ignore for good. |
| Reseller | Their own customers' accounts. Scan, quarantine, ignore (as **Ignored by the customer**). |
| Customer | Their own account. Scan, quarantine, ignore (as **Ignored by the customer**). |

A package can leave the feature out: untick **Malware scan** under **Features** in **Packages**. Customers on that package then cannot open the page or use its API, but their administrator or reseller can still scan their account. Limited administrators need the **Security** area.

## What is scanned

The scanner reads files under `domains/` in each account's home directory (all websites of the account) and checks them against a set of signatures:

- **File types:** `.php`, `.phtml`, `.php3`, `.php4`, `.php5`, `.php7`, `.phar`, `.inc`, `.suspected`, `.js`, `.html` and `.htm`. JavaScript and HTML files are checked only for JavaScript injection and long hex-encoded strings.
- **Size:** files up to 4 MB.
- **Skipped:** `node_modules`, `.git` and the Git working copy of deployed apps (`repo`). For deployed apps only the live release is scanned, not older releases.
- **Ownership:** only regular files that belong to the account. Symbolic links are not followed, and hard links to files of other accounts or of root are not read.

Databases, mailboxes, backups and files outside `domains/` (for example `~/tmp`) are not scanned.

### Detections

| Detection | Severity | What it matches |
| --- | --- | --- |
| **Encoded eval** | High | `eval` or `assert` of `base64_decode`, `gzinflate`, `str_rot13` and similar |
| **Eval of user input** | High | `eval`, `assert` or `create_function` on `$_POST`, `$_GET`, `$_REQUEST`, `$_COOKIE` or HTTP headers |
| **Command execution** | High | `system`, `exec`, `shell_exec`, `passthru`, `popen`, `proc_open` on request data |
| **Shell execution** | High | Backticks around request data |
| **Dynamic function call** | High | A function name taken from request data, called with request data |
| **Known webshell** | High | Markers of known shells such as FilesMan, WSO, c99, r57, b374k, IndoXploit, AnonymousFox, ALFA |
| **preg_replace /e** | Medium | `preg_replace` with the `/e` modifier on request data |
| **File uploader** | Medium | `move_uploaded_file` driven by request data |
| **Writes user input to file** | Medium | `file_put_contents`, `fwrite` or `fputs` of request data |
| **Obfuscated (chr)** | Medium | Long chains of `chr()` calls |
| **Obfuscated (hex)** | Medium | Long `\x..` strings that decode to readable text (not in `vendor/` or `wp-includes/`) |
| **Large encoded payload** | Medium | `eval`, `assert` or `base64_decode` of a string of 1,000+ base64 characters |
| **JavaScript injection** | Medium | `document.write(unescape(...))` writing a script or iframe, or long `String.fromCharCode` chains |
| **PHP in uploads folder** | Medium | A PHP file with code inside an `uploads` folder (including `wp-content/uploads`) |

Some legitimate patterns are filtered out: hex strings that decode to binary tables, `document.write(unescape(...))` that loads no external script or iframe, and comment-only placeholder files such as WordPress's `<?php // Silence is golden.`

## Run a scan

1. Open **Malware scan**.
2. Administrators and resellers choose an account in the account picker, or all accounts.
3. Click **Scan now**. The task log lists each account, the number of files checked and every finding, and ends with a line such as `Done: 5230 files, 2 findings in 14.2s`.

The scanner uses a quarter of the server's CPU cores (between 1 and 8), so websites stay responsive during a scan.

### Weekly scan

Every Sunday at 03:00 (server time), ZoPanel scans all hosting accounts automatically. The schedule cannot be changed; run **Scan now** whenever you need an extra scan.

## Review findings

The page shows three counters (**Open findings**, **High severity**, **Quarantined**) and a table with one row per finding:

- **File**: the path relative to the account's home and the line number, with the matching line below it.
- **Owner** (administrators and resellers): the account.
- **Detection**: the rule, coloured by severity, and the date it was first found.
- **Status**: **Open**, **Quarantined**, **Ignored by the customer** or **Ignored**.

Open the file in the [File Manager](/docs/file-manager) or over SFTP to look at it before you act. Open findings, including those ignored by the customer, also lower the score in **Security Center** and appear on the dashboard.

When an account is scanned again:

- an **Open** finding whose file is gone or no longer matches is removed from the list;
- a **Quarantined** finding is set back to **Open** if the same file matches again (it was put back, or the attacker recreated it);
- an **Ignored** finding stays ignored.

## Quarantine a file

1. Click **Quarantine** on the finding and confirm.
2. The file is moved out of the website to `/var/lib/zopanel/quarantine/<account>/` on the server. Only root can read that folder: the account, and anyone who broke into it, can no longer run, restore or delete the file.

The quarantined copy is named after the time and the original path, for example `20261012-031502_a1b2c3d4_domains__example.com__public_html__wp-content__uploads__x.php`.

**Note:** If the file was legitimate, the website may break where it used the file. Check the site after quarantining, and restore the file if needed.

### Restore a quarantined file

There is no restore button. An administrator restores the file over SSH as root:

```bash
# List the account's quarantined files
ls -l /var/lib/zopanel/quarantine/USER/

# Put one back, owned by the account
install -m 0640 -o USER -g www-data \
  /var/lib/zopanel/quarantine/USER/20261012-031502_a1b2c3d4_domains__example.com__public_html__file.php \
  /home/USER/domains/example.com/public_html/file.php
```

The next scan will find the file again and set the finding back to **Open**. If it is a false positive, click **Ignore** then.

## Ignore a false positive

Click **Ignore** on the finding.

- When an **administrator** ignores it, the status is **Ignored** and the finding no longer counts anywhere.
- When a **customer or reseller** ignores it, the status is **Ignored by the customer**. It still counts in **Security Center** and on the dashboard, and the administrator still sees **Quarantine** and **Ignore** on it, so a compromised customer login cannot hide a backdoor.

Common false positives: security, backup and file manager plugins that legitimately use `eval` or write uploaded files; minified libraries with encoded data; and PHP files that a plugin keeps in its own `uploads` subfolder. If a file belongs to a known plugin, compare it with a fresh copy of the same version before you ignore it.

## After an infection

A webshell is a symptom: the attacker got in through something else. Quarantining the file is the first step, not the last.

1. **Quarantine every high-severity finding** of the account, then review the medium ones.
2. **WordPress sites:** on the website's WordPress tab, run **Verify file integrity** to find modified core and plugin files, update everything with **Update all**, remove plugins and themes you do not use, then **Rotate security keys** (signs everyone out) and use **Reset password** for administrator users. Click **Apply all** in the **Security** card. See [WordPress](/docs/wordpress).
3. **Change passwords** of the hosting account, its SFTP/FTP accounts and its database users, and update the application configuration with the new database password.
4. **Look for persistence:** unknown [cron jobs](/docs/cron-jobs), unknown SSH keys, and new administrator users in the application.
5. **If in doubt, restore** files and database from a backup taken before the infection. See [Backups](/docs/backups).
6. **Turn on the web application firewall** for the website in its **Tools** tab. See [Security](/docs/security).
7. **Check outgoing mail:** hacked sites often send spam. Look at the mail queue and sending limits in [Email](/docs/email).
8. **Run Scan now again** until no open findings remain.

## Notifications

- **Administrators:** the **Malware detected** event in **Settings → Alerts** (e-mail, Telegram or webhook). It reports the number of **Open** and **Ignored by the customer** findings in the scanned accounts. See [Panel settings](/docs/panel-settings).
- **Customers and resellers:** **Suspicious files are found** in **My account → Notifications**, by e-mail or Telegram, with the number of findings still **Open** in their own account.

Alerts are checked after every scan, including the weekly scan. Findings that are **Quarantined** or **Ignored** by an administrator do not trigger alerts again, and a finding the customer ignored no longer alerts the customer.

## Troubleshooting

| Message or symptom | Cause and fix |
| --- | --- |
| "No threats found" right after installing | No scan has run yet, or nothing matched. Click **Scan now**. |
| Quarantine fails with `mv: cannot stat …` | The file was already moved or deleted. Run **Scan now** to refresh the list. |
| "the file does not belong to the account" | The file is owned by another user (for example root). Investigate it as root on the server; the panel only quarantines the account's own files. |
| "invalid path" | The finding points outside `domains/`. Rescan the account. |
| A known infected file is not reported | It may be larger than 4 MB, have another extension (for example a `.jpg` with PHP code), sit outside `domains/`, or use a technique the signatures do not cover. Inspect the site manually and restore from a clean backup. |
| The website broke after a quarantine | The file was needed. Restore it (see above) and ignore the finding if it is clean. |

## Related

- [Security](/docs/security)
- [WordPress](/docs/wordpress)
- [Backups](/docs/backups)
- [File Manager](/docs/file-manager)
- [Panel settings: alerts](/docs/panel-settings)
- [WordPress: FAQ My site was hacked](https://wordpress.org/documentation/article/faq-my-site-was-hacked/)
