Never Trust the Client
Architecting a Resilient API
When architecting a web application, it's easy to trust the client to do the right thing, but that's a mistake. In this post, we'll look at how to architect a resilient API that can't be spoofed by a mischievous client application.
I'll use the example of an API that creates an og-image for a blog post. An og-image is used by social media sites to display a preview of the article. The URL to the generated image is used in a meta
tag in the HTML for the blog post — something like this.
<meta
property="og:image"
content="https://example.com/og?title=Never%20Trust%20the%20Client&author=Donavon"
/>
For example, here is the og-image for this post as it appears on Twitter and other social media sites. Go ahead and tweet it to see for yourself.
A simplistic approach might be to have the client send the og-image API the blog metadata (i.e. title, author, etc) directly to the API. The API would then use those values to generate the image, but that's not such a good idea.
The Problem
The problem with our example is that the client can send whatever title and author image URL it wants. It could send a title and author image URL that are malicious, or it could send a title and author image URL that are too long and break the layout of the image.
Here's an example. In the image below, I've sent another site's og-image generator information for this blog post instead.
If I were a less than honorable person, I might simply alter my code to send the other site's og-image generator information for all of my posts, effectively hijacking their og-image generator and using it on my site for free. Thanks! (I would never do that, but you get the idea.)
This is a real site in the wild. The site's identity has been obscured to protect the innocent, but you know who you are. 😉
Here's a diagram showing this approach. As you can see, the client is hitting the third-party API directly, exposing the URL to prying eyes.
The Solution
The solution is to have the client send the API the slug of the article. The API can then use the slug to look up the title and description in a database. This way, the client can't send whatever title and description it wants.
The meta tag for this approach would look something like this.
<meta
property="og:image"
content="https://example.com/og/never-trust-the-client"
/>
It also means that other people can't use my API to generate og-images for their own articles.
This pattern and would require that the client hit a proxy API that then hits the third-party API. This is a good pattern to use when you want to hide the third-party API from the client.
API keys
We've hidden the API from the client by placing it behind a proxy, but if someone knew or guessed the API's URL, they could still use it.
"Security Through Obscurity" is not security.
A common pattern is to use an API key/secret to authenticate the client. The proxy would send the API key and secret along with the title, etc. to the third-party API. The third-party API would then check the API key/secret to make sure it's valid.
In many cases the API key/secret are stored in an environment variable or some kind of "secret store".
Sample Remix proxy code
This is the actual code that I'm using on my site. It uses the slug to look up the title, author, etc from a database. You can see that I include an API key in the x-api-key
HTTP header. In my case, the API key is validated by the third-party API that I'm using.
I'm using Remix which makes adding a proxy server a breeze, but you could use any server framework.
const apiKey = process.env.OG_IMAGE_API_KEY;
const ogImageBaseUrl = process.env.OG_IMAGE_ORIGIN;
export const loader: LoaderFunction = async ({ request, params: { slug } }) => {
// get the metadata for the post (i.e. title, author, etc.)
const ogImageParams = getImageParams(request, slug);
if (!ogImageParams) return json('blog post not found', { status: 404 });
// build the url for the image service
const ogImageUrl = new URL(ogImageBaseUrl);
ogImageUrl.search = new URLSearchParams(ogImageParams).toString();
// fetch and return the raw image response from the image service
return fetch(ogImageUrl.href, {
headers: { 'x-api-key': apiKey },
});
};
Conclusion
Even though something as innocuous as an og-image generator API might not seem like a big deal — it's not like it's guarding nuclear launch codes or anything — it's important to architect your APIs in a way that can't be spoofed by a mischievous client application.
Keep information in the client on a need to know basis. This might be accomplished by proxying your API through a server that you control. This is especially important for API codes and API keys.
Kent Beck is attributed with the phrase "Make it work, make it right, make it fast". But for me, your work isn't complete until you "Make it secure".