Blog /

The Importance of Safe Defaults: How I Learned the Hard Way

April 24, 2025 · by Nils Caspar · 3 min read

Many years ago, when I was working at a company transitioning from a small startup into a not-so-small startup, I learned a crucial lesson about safe defaults and API design. We were using a JWT (JSON Web Token) to pass data between two services. For those who might not be familiar, JWT allows a server to create a token containing arbitrary data that a user passes between services. The token is cryptographically signed with a shared secret, which allows the receiving service to verify that the data has not been tampered with, preventing the user from altering this information.

The Code

Our implementation on the receiving service looked roughly like this (note: exact API details may vary slightly as this is from memory):

decoder = JWTDecoder(key: ENV['JWT_SECRET'])
token = request.get('token')
data = decoder.load(token)
user = load_user(data['user_id'])
...

At first glance, this code looks perfectly fine; it initializes the JWT decoder with our secret key, retrieves the token from the request, decodes the data, and then uses it to load user information. However, we discovered later that this was not working as intended. Specifically, it turned out that our implementation allowed any JWT token, even ones not correctly cryptographically signed with our shared key.

The Problem

How was that possible?

Well, we found out the hard way that the JWT decoder’s load function had an optional parameter verify which, shockingly, defaulted to false. This meant that, by default, the library did not check if the token was properly signed and just blindly trusted whatever data was provided.

To actually ensure that the JWT token is correctly signed with our shared secret, the code needed to explicitly set this parameter:

data = decoder.load(token, verify: true)

Why This Matters

This is a classic example of unsafe default behavior. A well-designed API should always have secure defaults. In our case, the verify parameter absolutely should have defaulted to true. Had that been the case, our initial code would have been secure right from the start, rather than vulnerable. Without explicit documentation or previous knowledge of the library’s behavior, a developer would naturally assume that verification would be performed by default. After all, if you’re initializing a decoder with a cryptographic secret, why wouldn’t it automatically verify?

The core lesson here: Always design APIs with safe defaults. Secure behavior should never require explicit action, it should be the implicit standard. Unsafe behaviors should only be enabled through explicit, intentional opt-ins. This experience made a lasting impression on me and now influences every API decision I make.

Hopefully, you won’t have to learn this the hard way, as I did.

© 2025 Smartinary LLC