From the first alert through containment, eradication, forensics, and post-incident review — a structured playbook for handling web app breaches.
Incidents will happen. The companies that survive them well aren't the ones with the fewest incidents — they're the ones with the most practiced response. This tutorial is the operational playbook for handling a web app breach end-to-end.
Standard model (NIST SP 800-61):
Each has specific actions, specific dangers, specific deliverables.
The single highest-leverage investment in IR. Before an incident:
A written guide covering common scenarios. For a web app shop:
Each scenario has: trigger, who's involved, first 30 minutes of actions, decision points, escalation criteria, communication templates.
Small teams: one person wears multiple hats. The roles still need to be assigned and acknowledged.
Quarterly restore tests. Documentation of what's where. Immutable copies (so attackers can't tamper).
Annual at minimum. Run hypothetical incidents through the team. Find gaps.
"Is this an incident?" Initial triage.
Document timestamps for each:
Decide in advance. Examples:
When a trigger fires: declare. Convene the response team. Open a dedicated incident channel. Start the clock.
Stop the active damage. Two goals: prevent expansion AND preserve evidence.
The first hour. Actions taken under uncertainty, with the goal of limiting blast radius.
django_session (or all rows to log everyone out), or rotate SECRET_KEY without fallbacks. Note that python manage.py clearsessions only removes expired sessions and logs no one out.Be precise about what each revocation mechanism actually does, because a half-revocation gives false confidence. clearsessions only purges sessions that have already expired; it does not log anyone out. To invalidate every live session with the database or cached-database backend, delete the rows directly. To target only the accounts you believe were abused, decode sessions and match the user id, which keeps everyone else logged in while you investigate.
# python manage.py shell
from django.contrib.sessions.models import Session
from django.utils import timezone
suspect_ids = {"42", "317"} # user pks as strings, as stored in the session
live = Session.objects.filter(expire_date__gte=timezone.now())
doomed = [s.pk for s in live
if s.get_decoded().get("_auth_user_id") in suspect_ids]
Session.objects.filter(pk__in=doomed).delete()
# Nuclear option: log out everyone
# Session.objects.all().delete()
Iterating and decoding every session is slow on large tables, so for big sites prefer a full purge or rely on the password change: Django's default authentication verifies a session hash derived from the password, so setting a new password (or set_unusable_password()) invalidates that user's existing sessions on their next request. Signed-cookie sessions cannot be deleted server-side at all; the only global revocation is rotating SECRET_KEY without adding the old value to SECRET_KEY_FALLBACKS, which also invalidates password-reset links and anything else signed with it.
Sessions are rarely the only credential. API tokens (DRF TokenAuthentication rows, JWT signing keys, OAuth access and refresh tokens you issued) survive a session purge. Before you declare containment, list every bearer credential type the application accepts and record, in the timeline, which ones were revoked and when.
After initial chaos settles (hours to days). Goals: maintain controlled state while preparing eradication.
Remove the attacker entirely. Common mistake: declare victory too early.
Before eradication, you must know:
Without this, eradication is whack-a-mole. The attacker is back tomorrow via the same or a similar vector.
Before running anything on a suspect host, decide whether you trust its binaries. A capable attacker with root can replace ps or hide files from ls. Where possible, snapshot the disk and analyze the snapshot from a clean instance; where live triage is unavoidable, run commands from a known-good static toolkit mounted read-only and write every output to external storage, never to the suspect disk. The goal of live triage is to capture volatile state that a snapshot will not contain: processes, network connections, logged-in users.
# Capture volatile state first, to an evidence volume
OUT=/mnt/evidence/web01-$(date -u +%Y%m%dT%H%M%SZ)
mkdir -p "$OUT"
ps auxwwf > "$OUT/ps.txt"
ss -tanp > "$OUT/sockets.txt"
{ who -a; last -Fw; } > "$OUT/logins.txt"
systemctl list-units --type=service --all > "$OUT/services.txt"
systemctl list-timers --all > "$OUT/timers.txt"
crontab -l -u deploy > "$OUT/cron-deploy.txt" 2>&1
ls -la /etc/cron.* /var/spool/cron > "$OUT/cron-dirs.txt" 2>&1
# Files changed in the app tree since the last known-good deploy
find /srv/app -type f -newermt "2026-03-01 00:00" -printf '%TY-%Tm-%Td %TT %p\n' \
| sort > "$OUT/app-changed.txt"
# Hash what you collected
sha256sum "$OUT"/* > "$OUT/SHA256SUMS"
Treat modification times as hints, not proof: they are trivially forged with touch. A stronger check is comparing the deployed tree against the build artifact you shipped. If you deploy from a versioned release archive or container image, hash every file in the known-good artifact and diff the result against the live tree; any file present only on the server, especially a .py file in a static or media directory, or a template with injected script tags, is a priority lead. For containerized deployments, docker diff on the running container lists every added, changed, or deleted path relative to its image.
Web access logs usually answer the initial-access question. Look for the first request to any file that should not exist, rare user agents, POST requests to endpoints that normally only receive GET, and bursts of 4xx responses from one address immediately preceding a 200 on an unusual path. That 4xx-then-200 shape is the signature of someone probing until something worked, and the timestamp of the first 200 is often your intrusion start time.
Application-level persistence is easy to overlook because it lives in data, not files. Rebuilding servers does nothing about a rogue superuser in the database.
# python manage.py shell
from datetime import datetime, timezone as tz
from django.contrib.auth import get_user_model
from django.contrib.admin.models import LogEntry
since = datetime(2026, 3, 1, tzinfo=tz.utc)
User = get_user_model()
User.objects.filter(is_superuser=True).values("username", "date_joined", "last_login")
User.objects.filter(is_staff=True, date_joined__gte=since).values("username", "email")
LogEntry.objects.filter(action_time__gte=since).values_list(
"action_time", "user__username", "content_type__model", "object_repr", "change_message")
-- PostgreSQL: roles and live connections
SELECT rolname, rolsuper, rolcreaterole, rolcanlogin, rolvaliduntil
FROM pg_roles ORDER BY rolname;
SELECT pid, usename, application_name, client_addr, backend_start, state, left(query, 120)
FROM pg_stat_activity WHERE backend_type = 'client backend';
Remember that the admin LogEntry table only records actions made through the admin interface and can itself be edited by anyone with database access, so absence of entries proves nothing. Also review database-level objects an attacker could plant: unexpected functions, triggers on user or payment tables, and extra roles with login rights. Finally, compare installed Python packages against your lock file; a package installed outside the deploy pipeline, or an installed version that differs from the pinned one, deserves investigation as a potential supply-chain or persistence vector.
After eradication, validate. Re-scan. Re-image where appropriate. Don't trust a "cleaned" system without verifying.
Bring systems back. Two priorities: business resumption AND defending against the same attack happening tomorrow.
Critical systems first, broader systems later. Monitor each tier as it comes online.
This is the boring, painful work. It's also where many recoveries fail — the attacker had a credential you forgot to rotate.
The post-incident review. Done within 1-2 weeks of incident closure, before memory fades.
The goal is improvement, not punishment. People who fear consequences hide information. The review must explicitly be blameless.
Often time-sensitive. Don't wait for full investigation.
Financial services (your national financial regulator, e.g. DNB in the Netherlands), healthcare, critical infrastructure — sector-specific timelines.
Your customer contracts may have stricter notification windows than law requires (24-48 hours common in enterprise B2B). Check before you need to know.
What NOT to say:
For when an alert just fired and you need a playbook on the wall:
That's enough structure to avoid the worst mistakes while you figure out what's actually happening.
Incidents are inevitable, and what separates organizations that survive them well from those that suffer lasting damage is not how few incidents they have but how prepared they are to respond. The middle of a breach is the worst time to figure out who does what, where the logs are, and how to contain the spread. Preparation — a plan, defined roles, accessible logs, practiced procedures — is what turns a chaotic scramble into a controlled response. Investing in readiness before anything happens is the highest-leverage incident-response work there is, because it determines how quickly and cleanly you can act when the alert finally fires.
When a compromise is confirmed, the instinct to immediately clean everything up can backfire — acting too fast can destroy evidence, tip off the attacker, or miss the full extent of the intrusion. The disciplined sequence is to first contain the damage, stopping the bleeding and preventing further spread, while preserving evidence, then to thoroughly understand the scope, and only then to eradicate the attacker and recover. Rushing to eradication before you understand how the attacker got in and how far they reached often leaves them a way back. Methodical response — contain, investigate, eradicate, recover — is what ensures you actually remove the threat rather than merely disturbing it.
The final and most often skipped phase of incident response is the lessons-learned review, and it is what makes each incident strengthen rather than merely scar you. Honestly examining how the attacker got in, why it was not caught sooner, and what would have limited the damage produces concrete improvements — a closed gap, a new alert, a better process. Organizations that treat incidents as expensive lessons get steadily harder to compromise; those that just clean up and move on repeat the same mistakes. Building the post-incident review into your process, without blame, is how a breach becomes an investment in resilience rather than only a loss.
The timeline is the investigation's central artifact. Every question you will be asked later, by leadership, regulators, customers, or insurers, is answered from it: when did access begin, what was touched, when did we know, what did we do. Build it as you go, not afterwards, and keep two distinct streams in it: attacker activity (reconstructed from evidence) and response actions (what your team did and when). Mixing them makes it impossible to tell whether a log line shows the attacker or a responder poking around.
Normalize every timestamp to UTC at the moment you record it, and note the source's original timezone alongside. Most timeline errors come from a handful of predictable pitfalls:
| Source | Typical pitfall | Mitigation |
|---|---|---|
| Web server access logs | Logged in server local time; request time recorded at completion, not start | Configure UTC or ISO 8601 with offset; note request duration for long requests |
| Application logs / Sentry | Sampling drops events; retention shorter than dwell time | Record sampling rate; export before retention expires |
| Load balancer / CDN logs | Client IP is real, but origin logs only show the proxy address | Correlate by request ID; ensure the app logs a trusted forwarded-for value |
| Cloud audit logs | Delivery delay of several minutes; per-region trails | Query all regions; do not treat an empty recent window as proof of inactivity |
| Host logs (journald, auth.log) | Clock drift on hosts without NTP; logs erasable by root | Record measured drift per host; prefer copies already shipped off-host |
| Database logs | Statement logging usually disabled; only errors and slow queries recorded | Rely on audit extensions if deployed; otherwise infer from application logs |
A request ID propagated from the edge through the application into logs is the single most useful correlation key you can have during an investigation. If you do not have one, adding it is a cheap lessons-learned action item.
Also record evidence gaps explicitly. "No log data for 2 March to 9 March because retention is 14 days" is a finding. It changes what you can claim in notifications: you cannot state that no data was accessed during a window for which you have no records, and saying so honestly early is far better than retracting it later.
Rotation is only effective after the entry vector is closed and persistence is removed; rotate earlier and the attacker simply reads the new secrets from the same foothold. Rotate in order of blast radius, starting with credentials that grant access to other credentials.
| Order | Credential | Why first / gotcha |
|---|---|---|
| 1 | Cloud IAM keys, CI/CD tokens, secret-manager access | These unlock everything else. Check for keys the attacker created, not just ones you know about. |
| 2 | Deploy SSH keys and authorized_keys on every host | Audit files for extra entries rather than only replacing your own key. |
| 3 | Database passwords | Requires coordinated app restart; plan a brief maintenance window. |
| 4 | Django SECRET_KEY | If leaked, do not keep it in SECRET_KEY_FALLBACKS; accept that all sessions and reset links die. |
| 5 | Third-party API keys (payment, email, storage) | Review each provider's own logs for use of the old key before revoking it; that log is evidence. |
| 6 | User passwords, sessions, issued API tokens | Last, once attackers can no longer observe the reset flow. |
Keep a rotation ledger: each secret, where it is used, who rotated it, when, and how the rotation was verified (for example, confirming the old key is rejected). Secrets that nobody can locate the owner or consumer of are themselves a finding.
Most failed responses fail in familiar ways. Knowing them in advance lets the Incident Commander recognize them in the moment.
These ten tutorials cover the web application security landscape from threat models to incident response. The thread connecting them: defense in depth, applied with discipline, tested under realistic adversary pressure.
You will not prevent every incident. You will not detect every attack. Your goal is to make attacks expensive enough that most adversaries pick another target, detect the ones who persist before they finish, and respond effectively when something does succeed.
That outcome is achievable. The companies that achieve it are not the ones with the biggest security budget — they're the ones that prioritize, practice, and improve continuously. Tools matter. Discipline matters more.
Good luck out there.