Who actually attacks web applications, what they're after, and the threat models that matter for SaaS, e-commerce, and B2B platforms in 2026.
Web applications carry more business value, store more customer data, and accept more attacker-controlled input than any other class of software you run. Every day on the internet, your login form is touched by credential-stuffing botnets. Your /api/ endpoints are probed by automated scanners. Your password reset flow is tested for enumeration.
This series is about understanding those threats at the technical depth required to defend production web apps — not generic "awareness training" content. We're starting with the threat landscape because everything else depends on knowing who's actually attacking and why.
The largest volume of attack traffic against any internet-facing web app. Operators rent or build botnets to:
/wp-admin/, /.env, /.git/HEAD, common admin URLs)You will see this traffic on day one of going live. The defenses are basic but mandatory: WAF + rate limiting + patched dependencies + MFA. Get them in place before launch.
A specialized criminal economy: attackers whose only job is to break in and sell access to others. They don't deploy ransomware themselves; they sell credentials and shell access to the gangs that do.
What they target in web apps:
Access to a compromised SaaS admin account is traded as a commodity, and its price rises with the victim organisation's apparent revenue and the sensitivity of the data behind it.
The crime groups that pay IABs. Their web app angle is access-to-pivot — once they get into your admin panel or developer accounts, they pivot to internal infrastructure where the real damage happens (file servers, domain controllers, backup systems).
Web apps are increasingly the initial vector because:
Specifically dangerous: any app where compromising an admin account also compromises connected systems (SSO providers, Git repositories, cloud infrastructure).
Most web apps aren't worth this level of attention. The exceptions: defense contractors, journalists' tools, dissident communications, government suppliers, financial services with high-value transactions. If you fall into one of these, your threat model includes professional, well-resourced adversaries with custom tooling.
Indicators you're a target:
If this is your context, you need a different conversation than this tutorial series alone.
Mapped to monetization paths covered in tutorial 2:
The defense priorities depend on which of these matters most for your specific app. A small content site mainly defends against #5 and #7. A SaaS with payment data defends primarily against #1, #2, and #6.
Most successful breaches against web apps follow a recognizable pattern:
1. Reconnaissance — find the target, map the attack surface
2. Initial access — credential reuse, phishing, exploit a vuln
3. Persistence — get a foothold that survives password changes
4. Privilege escalation — go from low-priv user to admin
5. Lateral movement — pivot to other systems/accounts
6. Action on objective — exfiltrate data, deploy ransomware, fraudulent transfers
Each step has tools and techniques (covered in tutorials 7-9). Each step has detection opportunities (covered in tutorial 10). The defenses you build should slow the attacker at multiple stages so detection has time to catch up.
The chain is only useful to a defender if each stage maps to something you can observe. For a Django app behind nginx and PostgreSQL, the evidence lives in a few places: the proxy access log, the application's security log, the admin LogEntry table, and your hosting provider's audit trail.
| Stage | What it looks like from the inside | Control that buys time |
|---|---|---|
| Reconnaissance | Bursts of 404s on paths your app never served | WAF rules, rate limits, DEBUG = False |
| Initial access | Failed logins spread across accounts, then a success from a new network | MFA, breached-password checks, auth rate limiting |
| Persistence | New staff user, API token or OAuth grant; changed email on an admin account | Alert on any credential or privilege change |
| Privilege escalation | is_staff or is_superuser flipped, group changes | Few superusers, change approval |
| Lateral movement | App-host secrets used elsewhere, unexpected outbound connections | Scoped credentials, egress filtering |
| Action on objective | Large exports, payout or bank-detail changes | Export thresholds, dual approval |
The goal is not to model the attacker's cleverness; it is to make sure at least two of these rows would produce a signal that a human sees within hours rather than months.
These patterns repeat across countless web app breaches:
User reuses password from old breach. Attacker tries it on your admin login. Account has no MFA. Attacker now has full admin access. Exports customer database. Posts it for sale on dark forums.
Defense layers that would have stopped it: MFA, breach password blocking (HaveIBeenPwned check on login), rate limiting + anomaly detection on auth, audit logs of admin actions, alerts on bulk export operations.
Attacker sends spear-phishing email to a target with a link to a malicious OAuth app. Target clicks "Authorize." Attacker now has long-lived OAuth token, no need for password or MFA. Pivots through connected services (Slack, GitHub, Gmail).
Defense layers: OAuth app approval workflows, monitoring of token grants, principle-of-least-privilege on scopes, ability to revoke tokens centrally, user training on OAuth consent screens.
Your app uses a popular library with a fresh CVE. Within hours of disclosure, internet-wide scanners are exploiting it. Your app gets a web shell installed. Attacker uses it to read secrets, then pivots to your cloud account via the IAM role.
Defense layers: automated dependency scanning + patching SLA, restricted server permissions (process can't read arbitrary files), egress filtering (web shells need C2 to be useful), runtime intrusion detection.
Phishing compromises the CFO's email account. Attacker doesn't ransomware — they sit silently for a week, learn the wire transfer process, then send a properly-styled email to AP requesting a vendor payment to a controlled account. AP processes it. €240K gone.
Defense layers: MFA on email, DMARC enforcement, dual approval for transfers, out-of-band verification for changes to vendor banking, training that doesn't shame people for being targeted.
A 20-minute exercise for any web app you maintain. Answer in order:
That gap is what to fix this quarter. Repeat the exercise quarterly.
Here is what a finished answer looks like, so you can calibrate the level of detail. The app is myproject at app.example.com: small businesses create invoices, store their customers' contact details, and connect Stripe so invoices can be paid by card. Staff use the Django admin for support.
| Threat path | Likely attacker | Likelihood | Impact | Current gap |
|---|---|---|---|---|
| Credential stuffing against tenant logins | Botnets | High | Medium | No per-account throttling, MFA optional |
| Staff admin takeover via reused password | IAB | Medium | High | Admin MFA not enforced |
| Tenant bank-detail change after account takeover | Fraud crews | Medium | High | No re-authentication or notification on change |
| Dependency RCE | Scanners | Medium | High | Ad hoc patching, unrestricted egress |
| Leaked Stripe key from CI logs | Opportunistic | Low | High | Restricted keys not used |
Coarse levels are enough; precise scores are false precision. The output is a short, ordered backlog with owners and dates: enforce staff MFA, require re-authentication plus an email to the account owner whenever bank details change, add per-account login throttling, and set a patching window for security releases. The bank-detail control is the one teams usually miss, because it is a business-logic weakness rather than a classic vulnerability, yet it is exactly where account takeover turns into money.
Two failure modes to avoid: modelling the architecture diagram instead of the real deployment (the forgotten staging server with production data, the cron job using a superuser token), and stopping at vulnerabilities when many costly incidents abuse features working as designed, such as exports, password resets and invitations. Re-run the model whenever you add a payment flow, a public API, or an integration with write access.
If your app is on the internet today, these are non-negotiable:
Beyond this floor, the rest of this tutorial series goes deep on advanced techniques and defenses. But start here. The fancy stuff doesn't help if these basics are missing.
Effective defense starts with a realistic picture of who you are defending against, and it is rarely the lone hooded genius of stock imagery. The real landscape includes automated bots scanning indiscriminately for known vulnerabilities, opportunistic criminals monetizing whatever they can, organized groups running credential stuffing and fraud at scale, and occasionally targeted attackers with a specific goal. Most attacks are automated and opportunistic, which means basic hygiene defeats the majority of them. Understanding the spectrum of attackers — their motivations, capabilities, and methods — lets you size your defenses to the threats you actually face rather than to an imagined worst case.
The overwhelming motivation behind web attacks is financial, and seeing this clarifies what attackers want and how they operate. Stolen data is sold, access is ransomed, fraud is committed, and resources are hijacked for profit. Even attacks that look like vandalism often have a monetary angle. This economic reality means your highest-value assets — payment data, personal information, account access, anything that converts to money — are what attackers most want, and therefore what you must protect most. Following the money predicts attacker behavior remarkably well, and aligning your defenses with where the financial incentive points is one of the most practical ways to prioritize security effort.
A crucial distinction shapes your defensive strategy: most threats are automated and indiscriminate, while a few are targeted and persistent. Automated attacks scan the entire internet for known weaknesses and exploit whatever they find, so keeping software patched and avoiding common vulnerabilities defeats the vast majority of them with basic diligence. Targeted attacks, where someone specifically wants into your system, are rarer but more determined and require deeper defenses. Knowing which you are likely to face — most organizations face mostly automated threats — tells you where to invest, and for most, getting the fundamentals right blocks the bulk of real-world attacks.
Automated traffic tends to be wide and shallow: many source addresses, generic paths that do not exist in your app, no session cookies, and a constant round-the-clock rate. Targeted activity tends to be narrow and deep: few sources, requests that follow your real routes, valid sessions, human-looking pauses, and interest in features specific to your business such as invitations, exports, or payout settings. Neither pattern is proof on its own, but the second should always raise the priority of an investigation, because it means someone has spent effort learning how your application works.
A sound defensive posture is proportionate to your actual threat landscape rather than driven by fear or fashion. For most applications that means nailing the fundamentals — patching, strong authentication, the OWASP basics, monitoring — which collectively defeat the automated majority, then layering additional defenses according to the value of what you protect and the likelihood of being targeted. Security is about managing risk sensibly, not achieving an impossible perfection. Grounding your effort in a realistic understanding of who attacks web applications and why lets you build defenses that are strong where it matters and not wastefully elaborate where it does not.
Most of the minimum viable defense maps to settings you can verify in code review. Run python manage.py check --deploy against your production settings, then make the choices explicit so nobody relies on remembering defaults.
# myproject/settings/production.py
DEBUG = False
ALLOWED_HOSTS = ["app.example.com"]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 60 * 60 * 24 * 365
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_AGE = 60 * 60 * 8 # stolen cookies expire within a working day
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
"OPTIONS": {"min_length": 12}},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
]
SECURE_PROXY_SSL_HEADER is only safe if nginx always overwrites that header and Gunicorn is reachable only through nginx (bind it to a Unix socket or 127.0.0.1). Below the application, connect as a database role that does not own the schema, and run the service under a systemd unit with ProtectSystem=strict and NoNewPrivileges=true, so a compromised process cannot drop tables or rewrite its own code.
Every ranked threat path needs at least one structured event that is shipped off the host. Django already emits the key authentication events as signals in django.contrib.auth.signals; route them to a dedicated security logger.
# myproject/security/signals.py (imported from AppConfig.ready)
import logging
from django.contrib.auth.signals import user_logged_in, user_login_failed
from django.dispatch import receiver
security_log = logging.getLogger("security")
@receiver(user_login_failed)
def on_login_failed(sender, credentials, request=None, **kwargs):
# Django masks the password in credentials; log the username only
security_log.warning("auth.login_failed", extra={
"event": "auth.login_failed",
"username": credentials.get("username", ""),
"ip": request.META.get("REMOTE_ADDR") if request else None,
})
@receiver(user_logged_in)
def on_logged_in(sender, request, user, **kwargs):
security_log.log(logging.WARNING if user.is_staff else logging.INFO,
"auth.login_success",
extra={"event": "auth.login_success", "user_id": user.pk,
"ip": request.META.get("REMOTE_ADDR")})
Common failure modes: trusting the left-most X-Forwarded-For value (the client chooses it; only trust addresses your own proxy wrote), logging session IDs or reset tokens, and keeping logs only on the host an intruder may control. Remember too that the admin LogEntry table records admin additions, changes and deletions, but not reads, exports, or changes made through your own views or the shell. Protect the tests as well: an assertLogs("security") test around a failed self.client.login() catches the refactor that silently renames a logger.
Keep alerts few and tied to threat paths; an alert nobody can act on gets muted, and a muted channel creates false confidence.
| Alert | Threat path | First response |
|---|---|---|
| Staff login from a network not seen for that user | Admin takeover | Confirm with the staff member out of band |
is_staff or is_superuser granted | Persistence, escalation | Verify a matching change request |
| Bank or payout details changed | Takeover to fraud | Hold payouts, contact owner via known channel |
| Export volume above a per-user hourly threshold | Data theft | Identify the session, decide whether to revoke |
Set export thresholds from a month of real usage, above what legitimate heavy users do, and back the hourly tripwire with a daily query so slow exfiltration spread across hours is still caught.
A one-page playbook is only useful if the capabilities it names already exist and have been tried. Build these before you need them:
SECRET_KEY without listing the old key in SECRET_KEY_FALLBACKS logs everyone out and invalidates outstanding password reset tokens.Rehearse quarterly with a tabletop exercise: take one archetype from this tutorial and ask, step by step, what alert fires, who sees it, and what they do. The gaps are usually mundane, such as nobody knowing who can rotate the Stripe key, and each becomes a backlog item beside the threat-model findings.
Tutorial 2 — How attackers monetize web app compromise specifically Tutorial 3 — The dark side of web app data after a breach Tutorial 4 — HTTP, TLS, cookies, headers — the attack surface in depth Tutorial 5 — Beyond OWASP Top 10 Tutorial 6 — WAF bypass and application-layer defenses Tutorial 7 — Advanced web app reconnaissance Tutorial 8 — Authentication attacks (sessions, JWT, OAuth, SSO) Tutorial 9 — Red team tactics specific to web apps Tutorial 10 — Investigating a live web app compromise
The whole series assumes you're already comfortable with Django, HTTP, and the basics of web security. The point isn't to introduce concepts — it's to go deep enough to build defenses that actually hold under realistic attack.