Skip to content

Denial of service

The registration form has a name field. Nothing stops a name being ten million characters long.

js
// What gets sent as `name`
'a'.repeat(10_000_000)

Nothing in that string is code. It won't be interpreted as anything. It's only enormous, and enormous turns out to be enough.

Store it and the database grows. Query it and the query slows. Back it up and the backup grows too, every night, forever. Do that a few thousand times and the service stops being able to answer anyone.

A request doesn't have to be clever to be harmful. It only has to be expensive.

Send this at your own service only

Volume tests are indistinguishable from an attack while they're running, and they affect every other user of whatever they're pointed at. Your own build, or a system you have written permission to test.

Availability is a security property

Security tends to get discussed as keeping secrets and keeping data correct. There's a third one, and this is its chapter: availability, whether the service answers at all.

A denial of service attack, DoS for short, goes after that third property. It doesn't read anything, change anything or steal anything. It makes the service unable to do its job, which for a business is often the more expensive outcome.

JunoAvailability is a security property Attacks are usually pictured as theft, so this one can feel like it doesn't count. Nothing gets taken.

Think about what a shop loses on a day it can't open. Nothing was stolen there either.

JunoAvailability is a security property This one shows up differently in review. There's no dangerous function to grep for, no sink where a value becomes code. The question is about cost: for each input, what's the most expensive thing a request can make the server do, and what's stopping someone asking for it repeatedly?

Every unbounded field, every unpaginated list endpoint, every synchronous file operation is an answer to that question.

JunoAvailability is a security property The three properties are usually named together as confidentiality, integrity and availability. Availability is the one the infrastructure team designs for and the application team forgets.

That's unfortunate, because the cheapest denial of service attacks are almost always application bugs rather than traffic volume.

A single request triggering an unindexed full table scan, or an export loading a year of rows into memory, buys more damage per packet than any flood will.

Traffic you can absorb by spending money. An endpoint costing ten seconds of CPU per call has to be fixed.

Four ways a service goes down

The course groups these by what the attacker exploits, and the grouping is worth keeping because each one is answered differently.

ShapeHow it worksWhat it exhausts
Magic bulletOne precisely crafted message that violates a rule in the protocol, such as sending more data than a field was built to holdA specific defect, immediately
Volume basedEnough traffic or large enough input to saturate the connection or fill the diskBandwidth, memory, storage
ProtocolAbuse of how a protocol is specified to work, so the server holds resources it never gets to releaseConnection tables, timers, sockets
DistributedThe same traffic, arriving from thousands of separate machines at onceAll of the above, from everywhere

The protocol one is worth seeing in detail, because it shows how little traffic an effective attack can need. A TCP connection, the handshake underneath most internet traffic, opens in three steps:

  1. The client sends a SYN packet, meaning "I'd like to connect".
  2. The server replies SYN-ACK, meaning "go ahead", and starts holding a slot open while it waits.
  3. The client replies ACK, and the connection is established.

In a SYN flood, the attacker sends step one with a forged return address. Step two goes to a machine that never asked for it and has nothing to say. Step three never arrives.

The server holds an open slot and a running timer, while the attacker has already moved on to the next forged address.

Nothing here is high volume. It's cheap for the sender and expensive for the receiver, over and over, until the connection table is full and a real visitor can't get a slot.

A distributed denial of service, or DDoS, is any of these coming from a botnet: a network of ordinary machines the attacker controls, usually infected without their owners noticing.

That's what makes it hard. Each machine is a real device making requests that look real, so there's no single address to block and no signature to filter on.

JunoFour ways a service goes down The pattern shared by all four is that the attacker spends a little and the server spends a lot.

A forged handshake costs one packet to send and ties up a slot for seconds. That imbalance is the whole game, and it's why volume isn't the only thing to watch.

JunoFour ways a service goes down Of the four, the two you own as an application developer are magic bullet and volume based. Protocol and distributed attacks are answered at the network edge, by your host or a provider in front of you.

That split is useful when an incident starts. If requests are arriving and your service is choking on them, that's yours. If connections aren't completing at all, that's a layer below you and a different phone call.

JunoFour ways a service goes down The category the table doesn't name, and the one most likely to be in your code, is algorithmic. Some inputs are cheap to send and superlinear to process, so cost climbs far faster than input size.

Regular expressions are the usual culprit. Against the pattern /^([a-z0-9]+-?)+$/, feeding a run of letters followed by an exclamation mark forces the engine to try every way of splitting the string.

Measured on Node 24, one call per fresh process: 21 characters takes 38 milliseconds, 25 takes 606 milliseconds, 29 takes 9.8 seconds, 33 takes 105 seconds. Four extra characters buy roughly a tenfold increase, from a request small enough that no size limit would flag it.

The name for it is ReDoS, regular expression denial of service. Nested quantifiers, a repeat inside a repeat, are the shape to look for, and the fix is usually to rewrite the pattern rather than to bound the input.

The defences stack

There's no single control here. The course lists six, and they sit at different layers on purpose:

  • Redundancy. Run more than one of everything: servers, data centres, network paths, name servers. The rule of thumb is three, so one can be failing and one can be under attack while a third still serves.
  • Rate limiting and throttling. Rate limiting refuses requests past a ceiling. Throttling slows them down. A hundred API calls a minute, five login attempts in ten minutes, a megabyte a second.
  • Filtering. Decide what to accept and from where. Block addresses or regions, reject requests carrying obviously malicious payloads, require authentication, screen on headers.
  • Hardening. Remove what you don't need. Every running service is a way in, and so is every account nobody uses: default admin logins, leavers who kept access, permissions granted once for a migration.
  • Patching. The magic bullet attack depends on a specific known defect. Applying the vendor's fix is what removes it.
  • Monitoring. You can't spot abnormal without knowing normal. A service that usually takes a thousand requests an hour and suddenly takes a hundred thousand is only obviously wrong if someone recorded the thousand.

For the ten-million-character name specifically, the answer is a length limit, checked on the server before the value is stored. That's schema validation, and it's the layer this section builds toward.

JunoThe defences stack None of these six is the answer on its own, and that's the point of having six.

Redundancy buys time. Rate limits cap the damage. Monitoring is how you find out. Patching removes the specific hole. They cover different moments, so you want all of them rather than the best one.

JunoThe defences stack Monitoring is the one teams skip and then regret, because it's the only one that's useless retroactively. You cannot establish what normal looked like after the incident starts.

The baseline worth having is unglamorous: requests per minute per endpoint, response time percentiles, error rate. Record them before you need them, and an alert becomes possible when traffic doubles rather than when the service falls over.

JunoThe defences stack Body size limits in Express are worth knowing about before you reach for them, because they fail quietly.

A global express.json({ limit: '32kb' }) consumes the body and rejects oversized ones with a 413 Payload Too Large before any route-level parser runs. A per-route express.json({ limit: '2mb' }) added afterwards does nothing: body-parser sets req._body, so the second parser sees the work as done.

Verified on Express 4 and 5.2.1. The upload route you gave a larger limit is still rejecting at the global one, and the pattern that works is no global parser at all, with each route naming its own.

Try it

The registration form accepts name, email and password, stores them, and returns the first two. No limits anywhere.

Work out three things:

  1. Which single request costs the server the most, and what makes it expensive?
  2. Which of the six defences would stop that request, and which would only reduce the damage?
  3. What's the cheapest change that closes it?
Compare your answers

1. The most expensive request. A very long name or password. It costs memory to parse, storage to keep, time on every query touching the row, and space in every backup from then on.

password is the worse of the two if the app hashes it. Hashing is deliberately slow, so a huge value turns a costly operation into a far costlier one.

2. Which defences bite.

  • Stop it outright: filtering and a length limit. The server rejects the request before doing the expensive work.
  • Reduce the damage: rate limiting and throttling. They cap how often it repeats, so the individual request still lands.
  • Neither: redundancy, patching and monitoring. They help you survive it, remove unrelated holes, and find out it's happening.

3. The cheapest change. A maximum length on every string field, enforced on the server. It's one line per field in a schema and it removes the whole class, which is why the section spends its remaining chapters on getting that schema right.

Where this goes next

Two chapters in, and both bugs come from the same missing step. Nothing checked what arrived before the application acted on it.

SQL injection is the third and the most direct. Input that doesn't sit in the database so much as tell it what to do.