Protect JSON REST APIs with Azure API Management
- Published on
- Reading time
- Authors
- Name
- Simon Waight
- Mastodon
- @simonwaight
Publication of APIs, particularly for consumer applications, requires some next-level attention to protecting your service. In this post I am going to look at you can use Azure API Management to help protect new or existing RESTful APIs that receive and process JSON objects.
We're going to look at two steps we can take:
- Validating the JSON object being recieved against a schema.
- Ensuring that the size of the payload doesn't exceed a safe limit.
Let's start be creating a sample payload for an object that sent via POST to an API endpoint.
{
"customerId": "d4f6a3c8-9d7e-4a5e-8c7d-9f5a9d6b7c8e",
"customerName": "Siliconvalve Consulting Services",
"customerAddress": {
"streetNumber": "24",
"streetName": "Example Street",
"locale": "Sydney",
"state": "New South Wales",
"postalCode": "2000"
},
"billedAmount": 1847.37
}
Validating the object
If you've been around long enough, or you've built enough systems, you have used XML. One benefit of XML is that you can define an XML Schema Definition (XSD) which can be used validate XML documents you receive.
JSON came along as a solution to the verbose nature of XML which leads to larger-than-required payloads even for simple scenarios. The downside of JSON, given it's relatively unstructured nature, was it was difficult to validate objects sent using it. Thankfully, given how popular JSON is, there is now the concept of a JSON schema that you can use for this purpose.
Let's define a schema for our sample object above.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://siliconvalve.com/customerbillingrecord.schema.json",
"title": "CustomerBillingRecord",
"description": "A billing record for a customers.",
"type": "object",
"required": ["customerId", "customerName", "customerAddress", "billedAmount"],
"properties": {
"customerId": {
"type": "string",
"format": "uuid"
},
"customerName": {
"type": "string"
},
"customerAddress": {
"type": "object",
"required": ["streetNumber", "streetName", "locale", "postalCode"],
"properties": {
"streetNumber": {
"type": "string"
},
"streetName": {
"type": "string"
},
"locale": {
"type": "string"
},
"state": {
"type": "string"
},
"postalCode": {
"type": "string"
}
}
},
"billedAmount": {
"type": "number"
}
}
}
Now we have the schema defined, we need to make it available for Azure API Management to use for incoming API requests. We do this using the APIs > Schemas option in the Azure Portal or in our Bicep template.
Let's do this one in the Portal.
When this page opens, we click on the "Add" option at the top of the screen and can then add our schema definition. The schema's first line will generate a warning, but we're OK to go ahead and save this definition.
Once you click save you will now have a Schema you can use to validate an incoming request. Next up we can take a look at how to do this in an Azure API Management Policy.
Define our APIM Policy
When we setup the Policy to validate our object type and properties we can also define the maximum size of the received object. Validating size is actually an important step as it reduces the chances of overflow or out-of-memory style errors by a bad actor sending you a massive payload to validate.
When we setup the validate content policy for an API endpoint we can ensure we are receiving JSON, that it doesn't exceed a certain size (in bytes) and that the payload adheres to a known schema. You'll notice that for the Schema ID
field we have specified the title
from our schema definition.
There's one additional check you may want to add that you can't do via the UI - that there are no additional properties on the JSON payload. This is more important if you are using a dynamic language in your API implementation and don't want unexpected data in memory, or you wish to mask fields from external consumers. Add the allow-additional-properties
attribute and set it to false
.
<validate-content
unspecified-content-type-action="prevent"
max-size="4096"
size-exceeded-action="prevent"
errors-variable-name="contentValidationErrors">
<content type="application/json"
validate-as="json"
action="prevent"
schema-id="CustomerBillingRecord"
allow-additional-properties="false" />
</validate-content>
Now with this check in place if we try and send a malformed object to our API we will receive a 400 Bad Request response back. This one for a missing field.
{
"statusCode": 400,
"message": "Body of the request does not conform to the definition which is associated with the content type application/json. Required properties are missing from object: streetName. Line: 1, Position: 206"
}
Or this one for an oversized request.
{
"statusCode": 400,
"message": "Requestbody size exceeds the configured limit of 4096 bytes."
}
We also get telemetry in Application Insights that tells us these errors are happening and can either work with vendors to ensure they use the API correctly, or identify someone attempting to abuse our API.
So, there we have it! A small amount of work means we can have confidence that payloads being passed to our API backend meet an object definition and don't exceed a size we expect.
Does this mean we don't need validation on our backend API as well? No, it doesn't! However, a common use case for a service such as Azure API Management is to protect APIs that we may not be able to modify the implementation or behaviour of, so we'd benefit from upstream protection regardless!
Happy days! 😎