Skip to main content

Barricade + choke point 🛡️

In software development, we often have to deal with untrusted data – data that comes from a user or an external system. Before we can safely process such data, it may need to be verified or sanitized.

Risks with untrusted data #

The typical example is SQL injection, a security problem often illustrated by XKCD’s classical “Bobby Tables” comic.1 If we embed user input into an SQL query, we better make sure that the input does not change the meaning of the query.

XKCD “Exploits of a mom” comic

Untrusted data is not only relevant for SQL queries. Including untrusted data in web applications can lead to XSS attacks, where attackers inject scripts to steal user data. Malformed data can cause excessive resource consumption, leading to system crashes or slowdowns. Improper handling of input lengths can lead to buffer overflow, potentially allowing remote code execution.

Helpful design patterns #

I would like to describe two design patterns that I’ve found helpful when designing systems to deal with untrusted data. They are called “Barricade” and “Choke point”, and nicely complement each other.

  • Barricade2 draws a line between parts of the code where data may be untrusted, and where it is trusted. Inside the barricade we are safe, but outside all bets are off. Data cannot pass the barricade unless it is explicitly let through. This relieves the majority of the code from the responsibility of checking for untrusted data.
  • Choke point is a term which is derived from military strategy, where it is a geographical feature through which an opposite force is forced to pass, typically on a narrow front. It could conceptually be described as a funnel. In software development, it refers to a piece of code through which every call or bit of data of a certain type has to pass.

A barricade and a choke point can be used together in a very natural way. The barricade keeps dirty data out of the clean parts of the system, and data is only let in through a choke point at which it can be validated appropriately.

Common use cases #

Some examples of how it can be used in practice.

  • As for the SQL injection problem, it is typically handled by using prepared statements. In this case, the prepared statement is the choke point through which all potentially dangerous SQL must pass.
  • Most APIs available on the Internet requires authentication to control who can use them. To ensure that we do not forget this, and to avoid having to manually add authentication to every endpoint, we commonly use an API gateway and/or server middleware to implement this.
  • Similarly, we often use a reverse-proxy in front of one or more other servers to terminate TLS (HTTPS) communication. This ensures all services behind the reverse-proxy gets the benefit of secure communication without having to deal with it themselves.
  • When it comes to user input validation, it can be helpful to draw a clear line in the code behind which no unvalidated user input is allowed. Then at that border, ensure each piece of user input is validated before let through.

Forcing the use of a choke point #

In general, it should be easy to tell where the barricade line is drawn in your system. It should be clear on which side each piece of the code belongs. In the same way, it should be hard or impossible to bypass the choke point.

One way to make it hard to bypass the choke point is using the type system.3 Define different types for unvalidated data and for validated data. For example, an unvalidated username maybe handled as a plain String. Once it is validated, it is handled through a specific Username type. The Username constructor is then inaccessible to most code, so new Username instances can only be created through a validateUsername function.4 Here is an illustrative example in Kotlin.

// A specific type to represent the validated data
// The constructor is protected by reduced visibility
data class Username internal constructor(val value: String)

// This is our choke point
fun validateUsername(username: String): Username {
    require(username.isNotEmpty())
    require(username.all { it.isLetter() })
    return Username(username)
}

Given the above definitions, usage could look as follows.

// This is untrusted territory
val unvalidatedUsername: String = "admin"
// Force the untrusted data through our choke point
val username: Username = validateUsername(unvalidatedUsername)
// We have now passed the barricade
performLogIn(username, /* ... */) // We know that username is valid here

Hopefully this post has showed how the barricade and choke point patterns can be helpful to safely handle untrusted data. I encourage you to experiment with them, and see what you can come up with.

Anton Ekblad at : Nice write-up! The choke point pattern is also known as "parse, don't validate" in FP circles, closely related to type-driven design in general.

  1. I can’t mention the “Bobby Tables” comic, without also mentioning the lovely GenAI-themed joke by Dan Hon:

    Can’t believe Little Bobby Tables is all grown up and has had their first kid, Ignore All Previous Instructions.

     ↩︎
  2. I first learned about the “Barricade” pattern in Code Complete by Steve McConnell. It is one of the book that shaped me as a programmer. ↩︎

  3. As a commenter suggested, the maxim “parse, don’t validate” is used in the functional programming community to describe the idea of using the type system to differentiate between valid and invalid data. ↩︎

  4. For more thoughts on the idea of using types to ensure valid data, see my old post Convert guard clauses to value objects↩︎