# The First Hour After You Find a Compromised Laravel Server

> The instinct is to delete the web shell and move on. That destroys your evidence, leaves the way back in, and tells the attacker you noticed. Here is a sequence that preserves what you need while actually closing the door.

**Author:** Matt King | **Published:** August 21, 2026 | **Category:** Security

---

Somebody finds a PHP file in `storage/app/public` that nobody wrote. The instinct is immediate and almost always wrong: delete it, breathe out, carry on.

That deletes your evidence of how it arrived, leaves whatever else was installed alongside it, and signals to whoever put it there that they should use their quieter access instead. An hour later the file is back under a different name.

What follows is a sequence that keeps your options open. It assumes a single Laravel application on a server you control, and it assumes you are the person who found it rather than a dedicated incident response team.

## Minutes 0 to 5: contain, do not clean

Get the application out of public traffic without destroying state.

Remove the instance from the load balancer, or block inbound traffic at the firewall or security group. If it is a virtual machine, take a snapshot right now, before you touch anything. A snapshot is the cheapest forensic copy you will ever make and you cannot take one retrospectively.

What not to do, and each of these is a real reflex worth naming:

Do not power off. Memory contains running processes, open connections, and sometimes the only copy of a payload that never touched disk. Isolation stops the bleeding, shutdown burns the record.

Do not delete files yet.

Do not restore from backup yet. You do not know when this started, so you do not know which backup is clean.

Do not redeploy. A deploy overwrites the application directory, which is where most of your evidence lives.

If you have legal or regulatory obligations, this is the point to start the clock and tell whoever owns that process. Notification windows under GDPR and similar regimes are measured from awareness, and awareness started when you found the file.

## Minutes 5 to 20: work out what you are dealing with

Three questions: what is running, what changed, and who has been talking to it.

Running processes and connections first, since this is the state you lose if the box restarts:

```bash
ps auxf
ss -tupn | grep ESTAB
lsof -p $(pgrep -d, php-fpm) 2>/dev/null | grep -v " REG "
```

You are looking for anything spawned by the web server user that should not be there: a shell, an outbound connection to an address you do not recognise, a process with a name that imitates a system daemon.

Then what changed on disk. The most reliable method is a comparison against a clean checkout of the commit you believe is deployed, because it catches modified legitimate files as well as added ones:

```bash
git -C /var/www/app status --porcelain
git -C /var/www/app diff --stat HEAD
```

Uncommitted modifications to tracked files in a production deployment are a strong signal on their own. A modified `public/index.php` or a modified vendor file is a more sophisticated foothold than a dropped file, and it is the one that survives most cleanups.

If the deployment is not a git checkout, fall back to modification times, remembering that these can be forged:

```bash
find /var/www/app -name "*.php" -newermt "-14 days" -not -path "*/vendor/*" -ls
find /var/www/app/storage /var/www/app/public -name "*.php" -ls
```

That second command deserves attention regardless of your current situation. There should be no PHP files in `storage`, and usually none in `public` beyond `index.php`. Anything else there is worth explaining.

Then the access logs, filtered to the paths you found:

```bash
grep -F "suspicious-file.php" /var/log/nginx/access.log*
awk '$9 ~ /^(200|500)$/ && $7 ~ /\.php$/ {print $1, $4, $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -40
```

The first request to that file, and what the same source address did in the twenty minutes before it, is usually the entry point. That is the single most valuable fact in the whole exercise, because everything else you do depends on knowing it.

## Minutes 20 to 40: find the persistence

Assume more than one way back in. Anyone competent installs redundancy, and the loud artefact you found is often the decoy rather than the mechanism.

Scheduled tasks, for every user rather than just root:

```bash
for u in $(cut -f1 -d: /etc/passwd); do crontab -l -u "$u" 2>/dev/null | sed "s/^/$u: /"; done
ls -la /etc/cron.d/ /etc/cron.daily/
systemctl list-timers --all | head -30
```

Authentication paths:

```bash
cat ~/.ssh/authorized_keys /home/*/.ssh/authorized_keys 2>/dev/null
awk -F: '$3 >= 1000 || $3 == 0 {print $1, $3}' /etc/passwd
grep -E "Accepted|Failed" /var/log/auth.log | tail -50
```

Then the Laravel-specific places, which generic checklists miss:

**Application users.** New administrator accounts, or existing accounts with escalated roles.

```sql
SELECT id, email, created_at, updated_at FROM users ORDER BY created_at DESC LIMIT 20;
```

Look at `updated_at` as well as `created_at`. Promoting an existing dormant account is quieter than creating a new one and much easier to miss.

**Personal access tokens and API keys.** If you use Sanctum or Passport, tokens issued during the window are a durable way back in that survives a password reset.

```sql
SELECT id, tokenable_id, name, created_at, last_used_at FROM personal_access_tokens ORDER BY created_at DESC LIMIT 20;
```

**Queued jobs.** A payload sitting in the `jobs` or `failed_jobs` table that executes on the next worker run, long after you have cleaned the filesystem.

**Cached and compiled files.** `bootstrap/cache/*.php` and compiled Blade views in `storage/framework/views` are PHP that Laravel executes and that no deploy necessarily regenerates.

## Minutes 40 to 60: rotate everything the application could read

If an attacker had file read access, they had your `.env`. Treat every value in it as public.

`APP_KEY` first, because in Laravel it is not just one secret among many. It signs and encrypts things your application then trusts, and an attacker holding it can forge payloads that look legitimate. The rotation has real consequences for sessions and encrypted columns, and the sequence for doing it without breaking your application is in [how to rotate a leaked Laravel APP_KEY](/blog/laravel-app-key-rotation-guide).

Then database credentials, mail credentials, queue and cache credentials, and every third-party API token in that file. Payment processor keys especially, since those have direct financial consequences and their own notification requirements.

Then revoke sessions and tokens:

```bash
php artisan session:flush
php artisan sanctum:prune-expired --hours=0
```

Rotating `APP_KEY` invalidates encrypted session cookies anyway, but be explicit rather than relying on a side effect.

## After the hour

The hour buys you containment, a timeline, and closed credentials. It does not give you a clean system, and this is where teams tend to declare victory early.

Do not put the compromised host back into service. Rebuild from a known-good image and deploy the application fresh. Determining that a compromised system is clean is much harder than rebuilding it, and you will never fully trust it again, which has its own cost every time something behaves oddly for the next year.

Close the entry point before restoring anything. Restoring a clean backup onto an unpatched vulnerability means being compromised again by the same automated tooling, frequently within hours. If your timeline pointed at an unpatched dependency, patch it in the rebuild. If it pointed at an exposed `.env` or a debug page, fix the configuration that allowed it.

Then work out what data was accessible during the window, because that determines your notification obligations and it is a question you will be asked repeatedly.

## The uncomfortable part

Most compromises I see did not need sophistication. They needed an exposed environment file, a debug page rendering credentials in a stack trace, or a dependency with a public advisory that nobody applied. The [State of Laravel Security data](/blog/state-of-laravel-security-five-numbers) put debug mode in production at 18% of applications and exposed `.env` files at 12%, which is a large population of applications one HTTP request away from the start of this post.

The hour above is worth rehearsing precisely so that you rarely need it. If you have never checked what your production deployment exposes from outside, [a free scan](/free-scan) takes about a minute and is a considerably better use of an hour than the one described here.

---

## Frequently Asked Questions

### What is the first thing to do when a Laravel app is compromised?

Contain without destroying evidence. Take the application out of public traffic by removing it from the load balancer or blocking inbound traffic at the firewall, but do not delete files, do not reboot, and do not restore from backup yet. Those actions destroy the information you need to work out how the attacker got in and whether they can get back in, which is the question that determines whether your cleanup actually worked.

### Should you shut down a compromised server immediately?

Isolate it rather than shutting it down. Powering off destroys everything in memory, including running processes, open network connections, and in some cases the only copy of an in-memory payload. Cutting it off from the network stops the damage while preserving that state. If the host is a virtual machine, taking a snapshot before you change anything gives you a forensic copy for free.

### How do you find a web shell in a Laravel application?

Look for recently modified PHP files outside your deployment path, particularly anywhere writable such as storage and public uploads, then look for the dangerous callables inside them. Comparing the filesystem against a fresh checkout of the deployed commit is far more reliable than searching by pattern, because it finds modified legitimate files as well as added ones, and modified files are what most people miss.

### Do you need to rotate APP_KEY after a compromise?

Yes, and every other credential the application could read. If an attacker had file read access they had your .env, which means the application key, database credentials, mail credentials, and every third-party token in it. Rotating APP_KEY invalidates sessions and encrypted values, so plan for the disruption, but treating the key as still-secret after a compromise is a serious mistake because it underpins anything your application signs.

### Is restoring from backup enough to recover from a compromise?

Only if you know when the compromise started and the backup predates it, and only if the entry point is closed. Restoring to a point that still contains the original vulnerability means being compromised again, often within hours, by the same automated tooling. Restore is a step in recovery, not the recovery itself, and the sequencing matters: close the entry point first.

