Razi Rais
All writing
October 3, 2026 13 min read Attack Digital Identity

Anatomy of a Cyberattack: Bot Attack Patterns and Server-Side Defenses

A request can be valid HTTP, contain ordinary form fields and still be part of an attack. The problem may not be a malformed payload. It may be a legitimate workflow exercised repeatedly, across many accounts or from many sources, for an outcome the service never intended.

That is why bot defense starts with the business operation, not the browser checkbox.

Anatomy of a Cyberattack series: this is the bots installment. The companion AI article covers a different threat model: prompt injection and agent authorization.

My course repository includes a small Turnstile demonstration that makes one important boundary visible: the browser supplies a token, but the application server must verify it. This article uses that implementation as a starting point, then separates its demonstrated behavior from the additional controls a production application needs.

Scope: implementation links pin the review to commit 48a70ef. The lab uses public testing credentials by default. It does not implement a botnet, a credential-stuffing engine, a fraud classifier or a complete production mitigation service.

The attack is the workflow, repeated

Automation is not inherently abusive. Monitoring systems, accessibility tools, integrations and search engines all automate interactions. A useful security policy distinguishes permitted operations from abuse rather than treating every non-human client as hostile.

Botnets coordinate many devices, often compromised, under common control. Automated abuse does not require one: a single client can cause damage, while a distributed campaign can spread activity across sources. Network distribution changes what defenders can observe; it does not define the business intent.

Consider four common patterns:

PatternWhat is being abusedWhat defenders need to observe
Credential stuffingPreviously exposed username/password pairs are tried against another serviceAuthentication outcomes across accounts and sources
Fraudulent registrationAccount creation is repeated to obtain incentives or create disposable identitiesAccount creation velocity and subsequent account behavior
Unauthorized scrapingData retrieval exceeds the service’s permitted access or usage modelResources accessed, identities involved and aggregate extraction patterns
Resource exhaustionExpensive operations consume application capacityRequest cost, concurrency and downstream saturation

These categories are not interchangeable. Password spraying tries a small set of candidate passwords across many accounts; credential stuffing reuses obtained credential pairs. A response delay caused by scraping may be a side effect, whereas a denial-of-service campaign may aim to exhaust capacity directly.

OWASP’s Automated Threats project is useful here because it frames unwanted automation as misuse of valid application functionality, not only exploitation of software defects.

Repeated or distributed requests reach legitimate login, registration or resource endpoints. The business impact may be account takeover, fraudulent accounts or resource exhaustion. Controls belong at multiple boundaries: ingress capacity, identity and session checks, and business-operation policies.
Figure 1. A conceptual threat model for automated web abuse. These business endpoints and layered controls are not implemented by the repository's verification demo. Select a diagram to open its full-size SVG.

An endpoint returning 200 OK tells you little about whether its use was legitimate. Connect transport-level observations to the account, resource and business outcome.

Why one IP limit is not enough

A per-IP request limit can reduce straightforward bursts, but it is not an account-level policy. Distributed traffic can keep each source’s volume modest while producing substantial aggregate activity. Conversely, a shared network can place many legitimate people behind one address.

Design limits around the operation and the evidence available: source, account identifier, session, resource and overall service budget. Use multiple time windows to distinguish brief bursts from sustained low-rate behavior. Bound expensive work before it reaches a database or external verification dependency.

The measurements should remain interpretable. An account experiencing failures from several sources may need a different response from one source attempting many account names. Neither observation should automatically become permanent account lockout: an attacker can deliberately trigger a policy that denies service to the legitimate owner.

For authentication, OWASP’s credential-stuffing guidance recommends layered defenses and highlights stronger authentication. Phishing-resistant authentication, secure recovery and session protections address identity risk that a browser challenge alone cannot resolve.

Do not mistake an authenticated session for unlimited permission either. An account may still violate transaction limits, access rules or the service’s acceptable automation policy.

Follow the demonstration’s actual request path

The Python server uses the standard library and binds to localhost:8001. It serves index.html and exposes a single verification endpoint, /verify.

The browser code renders Turnstile widgets for three demonstration configurations. When a widget’s callback receives a token, JavaScript sends it to the local server. The demo also provides controls that deliberately choose predictable test outcomes.

An illustrative request for the simulated rejection path has this shape:

{
  "token": "<demonstration token>",
  "real": false,
  "simulate": "reject"
}

Those last two properties are demo controls, not fields that should let a production caller select the server’s verification policy.

The server rejects an empty token with HTTP 400. It then selects a verification credential and calls Cloudflare Siteverify. The outgoing request contains the required secret and response fields; the token becomes response, while the credential remains server-side.

The core exchange in siteverify() is small:

data = urllib.parse.urlencode({
    "secret": secret,
    "response": token,
}).encode("utf-8")
request = urllib.request.Request(SITEVERIFY_URL, data=data)
with urllib.request.urlopen(request, timeout=timeout) as response:
    return json.loads(response.read())

The default timeout is 15 seconds. The returned JSON is passed back to the educational UI alongside a verified boolean derived from success.

The browser widget receives a token and posts it to the local server's verify endpoint. The server sends the token and a server-held credential to Cloudflare Siteverify. Cloudflare returns JSON, and the demo server returns a verified boolean and diagnostics to the browser. No business transaction is executed.
Figure 2. The implemented browser-to-server-to-Siteverify exchange. The demo reports verification status; it does not create an account, authenticate a user or execute a protected transaction.

That final distinction prevents a common architectural mistake. A verification helper only protects an operation if the endpoint performing that operation actually enforces the result. A browser showing a successful check is not a substitute for a server-side gate.

A token is evidence to verify, not a permission slip

Cloudflare’s server-side validation documentation makes Siteverify mandatory. A caller can submit an arbitrary token string without following the intended widget flow, so the application must not trust the presence of a field alone.

The documented token properties are specific: a maximum length of 2,048 characters, validity for 300 seconds and single-use validation. Expired or reused tokens produce the timeout-or-duplicate error code.

These are service semantics. The lab does not implement its own token-signature parser, expiry checker or replay database. It sends the token to the provider and interprets the response.

In production, verify successful validation and compare relevant response context with server-owned expectations. That includes the expected hostname and, where configured, the expected action. A token intended for one workflow should not be casually accepted as evidence for another.

Labels supplied through the browser are not authorization credentials. Do not infer account ownership, payment approval or administrative privilege from an echoed label. Business identity and permissions come from the application’s trusted authentication and authorization mechanisms.

Likewise, treat the token as opaque rather than inventing an undocumented local decoder. The repository’s teaching UI describes the flow; it does not establish vendor internals or provide a portable cryptographic verification algorithm.

Keep simulation separate from production behavior

The credential-selection function has three paths. A real-mode request uses TURNSTILE_SECRET from the server environment. The simulated rejection path selects a public fail test secret. The ordinary demonstration selects a public pass test secret.

The browser’s “human” and “bot” controls also choose test sitekeys. Cloudflare documents these keys specifically for predictable development and automated-testing outcomes.

That makes the demo useful for learning request flow and error handling. It does not measure how accurately a production service distinguishes legitimate visitors from abusive automation. Selecting a fail key and observing rejection proves that the failure path is wired up, not that a detection model recognized an attacker.

Production configuration should be chosen by the deployment, not by request fields. Keep real credentials outside client bundles and source control. Separate development and production configuration so a public test credential cannot silently become the verifier for a real business operation.

The same distinction applies to marketing language in a UI. A valid token does not prove that a person is trustworthy, that an account is uncompromised or that the next transaction is authorized.

Put verification inside the protected operation

A production implementation needs a complete decision path. The following is architectural pseudocode, not functionality already supplied by the lab:

Bound request size, rate and expensive work
Validate the token field before contacting the provider
Call Siteverify with server-owned configuration
Require success and the expected hostname/action
Apply the workflow's authentication and authorization rules
Enforce resource and business-operation limits
Execute the permitted operation with duplicate protection
Record a privacy-conscious decision event

Every protected entry point needs equivalent enforcement. A checked browser route is insufficient if a second API path performs the same operation without the required controls. Conversely, machine-to-machine APIs may need authenticated service identities and explicit quotas rather than a browser widget.

A proposed production pipeline first bounds request size and cost, then verifies the token, validates expected context, applies identity and business policies, and executes an allowed operation with duplicate protection. A failed gate must not execute the operation. These controls are recommendations beyond the teaching demo.
Figure 3. Recommended production enforcement, separate from the implemented demo. Verification, identity and business policy answer different questions; none replaces the others.

Failure handling is part of the security design

The demo returns verified: false when its verification call raises an exception. It uses HTTP 200 for that result and includes a diagnostic note. This is an educational response contract, not a recommendation that every production failure should look like a successful HTTP operation.

Distinguish malformed requests, rejected verification and an unavailable dependency. Log a bounded diagnostic category, return an appropriate application error, and do not execute the protected operation when required verification is unavailable.

Bound the external call with timeouts and concurrency limits. Otherwise, a verification step can itself become an expensive bottleneck. A retry policy must also account for single-use tokens: after expiry or consumption, the client may need a fresh token.

Siteverify offers an optional idempotency_key for safe validation retries. That is separate from idempotency for the business operation. Preventing duplicate validation is not the same as preventing duplicate orders or payments after a lost response.

Use trustworthy context

If a policy uses client IP, derive it from a trusted ingress path. Do not accept an arbitrary forwarded header as verified client identity. Shared networks and changing addresses also mean IP is a signal, not a durable user identifier.

Keep authorization tied to the relevant account and resource. Verification can help manage automated access, but it cannot decide whether a user owns an order or qualifies for an incentive.

Test control flow without pretending to benchmark detection

The repository’s server tests separate deterministic checks from optional live Siteverify tests.

During the code review, eight deterministic tests passed; two live Siteverify tests were deliberately skipped. External-provider requests were disabled, while local HTTP requests remained available for mocked endpoint tests. No production challenge or detection performance was measured.

The deterministic coverage establishes specific behaviors:

CheckEvidence provided
Credential selectionThe intended real, pass-test or fail-test branch is selected
Empty tokenThe endpoint rejects the request with HTTP 400
Mocked provider success or rejectionThe application maps selected responses to its verification result
Simulated connection failureThe endpoint returns a non-verified result with a diagnostic

That is a useful foundation, but production tests should go further: unexpected hostname or action, expired or reused tokens, oversized inputs, dependency latency, alternate entry points and attempts to repeat a completed operation.

Test the final side effect. A rejected verification result is not enough if account creation or order processing happens before the check. Assert that denial produces no business operation and that retries cannot duplicate an authorized one.

Evaluate legitimate-user impact as well. Challenge completion, abandonment, accessibility and support incidents matter alongside blocked requests. An overly aggressive defense can reduce both abuse and the service’s usefulness.

Observe outcomes, not just challenge counts

A high rejection count is not automatically success. It may reflect misconfiguration, a dependency outage or friction imposed on legitimate users. Conversely, successful challenges do not mean subsequent activity is benign.

Correlate verification outcomes with application results: authentication success and failure, registration patterns, resource consumption, transaction policy decisions and user-reported problems. Examine both aggregate behavior and the entities targeted by a campaign.

Record the decision reason and the policy version. Avoid logging credentials, complete tokens or unnecessary personal data. Protect telemetry and apply a retention policy; the monitoring system should not become another source of sensitive information.

For high-cost operations, measure downstream saturation and cost as well as request volume. An application-level challenge is not a complete DDoS defense, and it cannot replace network or service-capacity protections.

Run the lab with its boundaries intact

From a checkout of the reviewed repository:

cd Anatomy-of-Cyberattack/exercises/bots
python3 server.py

The demo listens on http://localhost:8001 and attempts to open the page. It needs internet access for the widget and Siteverify. Public test keys are the default, so a real Cloudflare account is not required for the teaching flow.

Opening the demo can initiate external requests through its widget callbacks. The repository’s test module also probes Siteverify availability when imported; do not assume that selecting a deterministic test class makes an ordinary test invocation offline. The review results above used network blocking around the test run.

Keep this an isolated teaching application. A production adaptation needs deployment-owned configuration, appropriate request validation, observability and enforcement around the real operation.

The practical takeaway

Bot defense is not one universal human-versus-machine decision. It is a collection of controls around a workflow: who may access it, at what rate, against which resources and with what business effect.

The Turnstile lab makes one of those controls concrete: the browser presents evidence; the server verifies it before trusting it. The rest of the application must still decide whether the requested operation is allowed.

For the distinct problem of attacker-controlled text influencing an AI system, read the AI installment. The course overview and permanent archive connect these topics to the wider live course. Current session and registration details appear above.

The Python excerpt is adapted from the repository’s MIT-licensed code, copyright Razi Rais. Diagrams and explanatory prose are original to this article. General production recommendations are not claims that the demonstration implements those controls.


Worth reading again?

Get the next one in your inbox.

No noise. Whenever something's worth saying.

Unsubscribe any time. No marketing, no noise.