Varol Cagdas Tok

Personal notes and articles.

Application-Layer Denial of Service

Volumetric attacks are operationally straightforward: send more traffic than the target can receive. Protocol attacks exploit specific state machine vulnerabilities at L4. Application-layer denial of service is a different problem. The traffic volume may be modest. The packets complete valid protocol handshakes. The requests conform to application protocol syntax. From every angle below the application layer, the traffic looks legitimate. What exhausts the target is what happens inside the application when it processes the request.

Moving from volume to computation cost changes the attack surface fundamentally. A volumetric attack can be mitigated upstream without examining application semantics. An application-layer attack cannot be distinguished from legitimate traffic without understanding the application, which requires processing the traffic at the application layer, which is precisely what the attacker is trying to overload.


The Cost Asymmetry in Application Processing

Every application operation has a cost profile: how much CPU it requires, how much memory it allocates, how long it holds database connections, how many I/O operations it performs. These costs are not uniform. A static file read may cost microseconds. A complex search across an unindexed table may cost seconds.

The attack surface is the gap between the cost of requesting an operation and the cost of performing it. If a request costs the attacker 500 bytes of network traffic and costs the server 500 milliseconds of CPU, the attacker can exhaust server CPU with roughly 2 requests per second per core.

Three factors determine the size of this gap:

Operation complexity: operations requiring traversal of large data structures, cryptographic computation, or external service calls are more expensive than simple memory reads. Applications exposing expensive operations without authentication or rate limiting maximize the gap.

Input-dependent cost: operations whose cost depends on input provided by the requester are particularly dangerous. A search query whose performance degrades with input length, a regex whose backtracking is input-dependent, or a deserialization routine whose cost scales with nesting depth all allow the attacker to maximize cost by crafting inputs that trigger the expensive case.

Lack of per-client cost accounting: an application that does not track how much resource a given client has consumed in a time window cannot enforce per-client limits. Without per-client accounting, rate limiting is imprecise: you can limit global request rates, but you cannot prevent a single client from consuming a disproportionate share.


HTTP as the Dominant Attack Surface

HTTP is the most common application-layer attack vector because it is the most widely exposed application protocol and because HTTP request processing in typical web applications is computationally expensive. A request triggers routing, middleware execution, database queries, template rendering, and response serialization. The cost of this pipeline is several orders of magnitude greater than the cost of transmitting the request.

HTTP request floods, sending many HTTP requests at high rate from distributed sources, are the blunt version. Effective HTTP floods target resources that bypass caching and reach the application server: search endpoints, authenticated user-specific pages, real-time data feeds, checkout flows. These cannot be served from cache and must be processed by the application for each request. A crawler or automated tool that systematically requests these resources at high rate achieves significant damage at modest traffic volume.

Search Endpoint Exhaustion

Search is a particularly exposed operation in most web applications. Full-text search requires inverted index traversal. Relational database search without index coverage requires sequential table scans. Either operation scales with dataset size, and the cost is borne entirely by the server regardless of how many results are found.

Wildcard queries, prefix queries, and queries involving boolean combinations across multiple fields are typically more expensive than simple exact-match lookups. A search for %a%b%c% in a LIKE clause causes a full table scan on most database engines. An Elasticsearch query with deeply nested boolean structure forces multi-pass evaluation.

The defense at the application layer involves query cost limits (Elasticsearch's indices.breaker.fielddata.limit and query circuit breakers; query timeout settings in relational databases), strict input validation that rejects structurally expensive queries, and authentication requirements for search functionality.


Slow HTTP Attacks

Slow HTTP attacks emphasize state exhaustion over computation exhaustion. The attacker does not attempt to overload server CPU; they occupy server connection slots by holding connections open for as long as possible while sending as little data as possible.

Slowloris

Slowloris was demonstrated by Robert "RSnake" Hansen in 2009. The attack targets HTTP servers that use a threaded or process-per-connection model, where each connection occupies a thread or process for its entire duration.

The attack procedure:

  1. Open a connection to the target server.
  2. Send a partial HTTP request header, enough to indicate a request is in progress, but not the final \r\n\r\n that terminates the headers.
  3. At regular intervals, send another partial header line (e.g., X-a: b\r\n) to prevent the connection from timing out.
  4. Repeat from step 1 until the server's connection pool is exhausted.
  5. The server holds each connection open waiting for complete request headers. By sending a partial header line every 10-15 seconds, the attacker keeps the connection alive indefinitely. Each connection occupies a thread in Apache's prefork or worker MPM. When all threads are occupied, new connection attempts queue or are rejected.

    The attacker can maintain hundreds of such connections from a single IP address. Total inbound traffic is minimal: a few bytes per connection every few seconds. The server's network bandwidth and CPU are largely idle; only its thread pool is exhausted.

    Slowloris is ineffective against asynchronous or event-driven servers (nginx, Node.js with event loop, modern Apache with event MPM) that do not hold a thread per connection. These servers handle connections using non-blocking I/O; an idle connection waiting for headers consumes a file descriptor entry but not a thread. The file descriptor limit is typically much larger than the thread pool: a modern Linux system may have an fd limit of several hundred thousand, while a threaded Apache installation may have a few hundred threads. However, event-driven servers can be exhausted by connections that have completed headers but are sending request bodies slowly.

    R.U.D.Y. (R-U-Dead-Yet?)

    R.U.D.Y. targets the request body submission phase. The attacker sends a complete, valid HTTP POST request header including a large Content-Length value, then transmits the body at one byte every few seconds. The server must read the entire declared body before processing the request; this is standard HTTP behavior. As with Slowloris, the connection remains open, the server allocates the handler context, and legitimate connections are excluded.

    The distinction from Slowloris is that R.U.D.Y. targets servers that handle headers asynchronously but block waiting for request body data. Event-driven servers that handle headers asynchronously are not immune to R.U.D.Y. if they process request bodies synchronously within a handler context.

    Slow Read Attack

    The slow read attack reverses the direction: instead of sending slowly, the attacker receives slowly. The attacker announces a small TCP receive window, causing the server to buffer the response and wait for the client to acknowledge received data before sending more. The server holds the connection open with the response queued, occupying memory and socket state.

    This targets the server's outbound buffer. If the server uses a fixed pool of send buffers or thread contexts that block on socket writes, occupying these with slow-reading connections prevents the server from handling new requests.

    Mitigations include server-side receive window tracking and timeout enforcement. nginx's send_timeout directive controls this: if a client has not acknowledged data after a timeout, the connection is closed.


    Asymmetric Computation Attacks

    XML and JSON Processing

    XML parsing is expensive. Deep nesting, large attribute sets, and extensive entity reference resolution all increase parsing cost. The "billion laughs" attack (CVE-2003-1564 in the libxml context) exploits XML entity expansion:

    <?xml version="1.0"?>
    <!DOCTYPE lolz [
      <!ENTITY lol "lol">
      <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
      <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
      ...
      <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
    ]>
    <lolz>&lol9;</lolz>

    Each entity reference expands to ten copies of the entity it references. With nine levels of nesting, the final expansion produces 10^9 copies of the string "lol", approximately 3 gigabytes of memory from a few hundred bytes of input. A parser that fully expands entity references without a depth or expansion size limit typically crashes or exhausts available memory.

    Modern XML parsers implement configurable limits on entity expansion depth and total expansion size. Applications using XML parsing without configuring these limits remain vulnerable.

    JSON does not have entity expansion, but deeply nested structures create recursion depth that can overflow the parser's call stack in recursive-descent implementations. Python's json module has a maximum nesting depth configurable via json.decoder.MAXDEPTH (added after the vulnerability was known). The default is 128 levels in CPython, not something normally encountered with legitimate data, but trivially triggered by an attacker.

    Zip Bombs

    A zip bomb is a compressed file that expands to a much larger file upon decompression. The record case, a 42-kilobyte zip file that expands to approximately 4.5 petabytes, is achieved through nested compression with the innermost files consisting of repeated null bytes that compress extremely well. Most zip parsers detect this through depth limits or extraction size limits, but applications decompressing user-supplied files without these limits can be crashed by decompression. The same principle applies to gzip, bzip2, and lz4.

    Hash Collision Attacks (HashDoS)

    Hash tables are ubiquitous in language runtimes for mapping string keys to values. If many keys hash to the same bucket, the table degrades to a linked list scan, with O(n) lookup instead of O(1).

    In 2011, Alexander Klink and Julian Wälde demonstrated that most web frameworks of the time used hash functions without randomization, making it possible to construct a large set of strings that all hashed to the same value in a specific language's hash table implementation. An HTTP POST request body with thousands of form fields with names chosen to collide in the hash table consumed CPU proportional to the square of the number of fields during POST body parsing.

    A request with 100,000 colliding keys could consume several minutes of CPU time to parse. This was a critical vulnerability because POST body parsing happens before request routing and authentication: the application cannot decide not to parse it.

    The fix was hash randomization: using a randomly seeded hash function for runtime hash tables (Java's HashMap randomized starting with JDK 7u6; Python added hash randomization in 3.3 with PYTHONHASHSEED; Perl and Ruby adopted similar changes). With a random seed, an attacker cannot precompute collisions.


    Database Query Exhaustion

    Database queries are expensive relative to most other application operations. Applications that allow user input to influence query structure, through legitimate query parameterization rather than SQL injection, can expose expensive operations.

    Sort and pagination: an unbounded sort request against a large table may require the database to sort the full result set before returning the first page. LIMIT clauses applied after sorting do not prevent the sort from completing.

    Aggregation: queries involving GROUP BY, COUNT, SUM, or similar aggregation against large tables without appropriate indexes exhaust database CPU and connection pool capacity. Applications allowing users to request aggregated reports without authentication or rate limiting are particularly exposed.

    JOIN depth: a query joining many tables requires the database optimizer to evaluate join strategies and execute the plan, which scales non-linearly with the number of joins.

    Full-text search without limits: wildcard prefix searches and searches for very common terms may require large index scans. Limiting search query complexity and enforcing per-user query rate limits are the primary defenses.


    The Detection Problem

    Application-layer attacks are hard to detect automatically because the traffic is syntactically legitimate. The distinguishing characteristics are behavioral:

    • Request rate per IP higher than the distribution of legitimate users
    • Requests concentrated on a small set of high-cost endpoints
    • Absence of the supporting requests that accompany legitimate browsing (images, CSS, JS, analytics beacons)
    • User agent strings inconsistent with claimed browser behavior
    • No session state or cookie handling consistent with a real browser session

    None of these characteristics are definitive individually. Rate limiting by IP fails against distributed attacks. User agent analysis fails against tools that send realistic user agent strings. Session analysis requires state maintenance across requests.

    Challenge-response mechanisms, JavaScript execution challenges, CAPTCHA, invisible behavioral biometrics, attempt to distinguish automated clients from human ones. These work because legitimate browsers execute JavaScript; simple HTTP libraries do not. Headless browsers execute JavaScript too, and sophisticated attackers use them. As challenge mechanisms improve, evasion follows. But challenges still raise the cost of attack for the majority of unsophisticated attackers, which is most of them.