Build and test a protocol

Validate before changing state

Parse syntax, validate shape and limits, authenticate, authorize, then perform one bounded operation.

10 minute lesson

~~~

A network message is untrusted input. By the time a line reaches your command handler it has survived framing and JSON.parse — and neither says anything about whether the request is safe to execute.

Treat message handling as a pipeline with distinct stages: parse syntax, validate shape and limits, authenticate, authorize, then perform one bounded operation. Separate parsing from validation and side effects so malformed data cannot partially mutate state.

Validation goes first, in one place:

function validate(message) {
  if (typeof message.key !== 'string') return 'invalid_key'
  if (message.key.length > 256) return 'key_too_long'
  if (message.type === 'set' && typeof message.value !== 'string') return 'invalid_value'
  if (message.type === 'set' && message.value.length > 4096) return 'value_too_long'
  return null
}

Only after validation passes, dispatch. Use an explicit command switch:

switch (message.type) {
  case 'get':
    return readValue(message.key)
  case 'set':
    return writeValue(message.key, message.value)
  default:
    return sendError('unknown_command')
}

The explicit switch with a default is deliberate. The tempting shortcut — handlers[message.type](message) on a plain object — lets a client send "type": "constructor" or "type": "toString" and reach inherited properties you never meant to expose. Listing commands by hand keeps the attack surface exactly as large as your protocol.

Attack your own server

Send missing keys, oversized values, unknown commands, and repeated requests:

nc 127.0.0.1 4000
{"type":"set","key":"color"}
{"ok":false,"code":"invalid_value"}
{"type":"drop_everything"}
{"ok":false,"code":"unknown_command"}

Every invalid message should return a stable error without changing data. Stable means the same bad input always yields the same code — clients can branch on it and your tests can assert it.

The property this buys you is no partial mutation. A set with a valid key but an invalid value must not create the key, touch a counter, or leave any trace. Validate everything, then act once.

One rule with no exceptions: never construct shell commands or file paths directly from client values. exec('cat ' + message.key) or reading ./data/${message.key} hands remote callers your filesystem — ../../etc/passwd is the classic payload. If a client value must reach a path, match it against a strict allowlist pattern first.

Lesson completed

Take this course offline

Get every free book and course as PDF and EPUB files.

Get the download library →