How to validate request using Apache APISIX Gateway request-validation Plugin

How to validate request using Apache APISIX Gateway request-validation Plugin

apache_apisix_request_validation

Here in this article we will we see how we can validate a request before its sent to the backed api services to serve the request. We will be enabling the request-validation plugin at the APISIX gateway level and try to validate the data within the request to ensure it conforms as per our standards if any.

Test Environment

Fedora 37 server
Docker
Docker Compose
curl

What is Request Validation

Request validation is a process through which the request header and body content is validated such that it conforms to the API schema. It helps in providing a layer of protection against malicious content from being introduced into the request header or body which can potentially compromise the backend applications. This is only an additional layer of protection but further validations should be carried out within the code for the backend application to ensure that their is no malicious code or string introduced.

If you are interested in watching the video. Here is the YouTube video on the same step by step procedure outlined below.

Procedure

Step1: Ensure APISIX services running

As a pre-requisite step, we need to ensure that the APISIX and related services are up and running. Please follow “How to use Opensource Apache APISIX as an API Gateway” to install and configure APISIX service using Docker Compose file and ensure that its up and running.

Step2: Launch Microservices Flask Applications

Here in this step we are going to install python flask package and create a flask based microservices application. This application will be serving on port 2121 with context /postjson. It’s a very basic application which accepts a JSON request body and returns the same as response in JSON format.

Ensure flask python package is installed

[admin@fedser apisix]$ mkdir flask; cd flask
[admin@fedser flask]$ pip install Flask

Create a flask based application which accepts JSON payload data

[admin@fedser flask]$ cat postjson.py 
from flask import Flask, request

app = Flask(__name__)

@app.route('/postjson', methods=['POST'])
def process_json():
    content_type = request.headers.get('Content-Type')
    if (content_type == 'application/json'):
        json = request.json
        return json
    else:
        return 'Content-Type not supported!'

Launch flask applications

[admin@fedser flask]$ flask --app postjson run --host=0.0.0.0 --port=2121 &

Create a json payload

[admin@fedser flask]$ cat postdata.json 
{
"name": {
	"firstname": "Alice",
  	"middlename": "Wonder",
	"lastname": "land"
},
"age": 20,
"gender": "Male"
}

Verify flask application using the postdata.json payload

[admin@fedser flask]$ curl -X POST -H "Content-type: application/json" 'http://192.168.29.117:2121/postjson' -d @postdata.json
192.168.29.117 - - [04/Mar/2023 05:38:47] "POST /postjson HTTP/1.1" 200 -
{"age":20,"gender":"Male","name":{"firstname":"Alice","lastname":"land","middlename":"Wonder"}}

Step3: Create upstream for backend flask application

Upstream is the service to forward your requests to. They can be configured to a Route or abstracted out to an Upstream object.

Request

[admin@fedser apisix]$ curl "http://192.168.29.117:9180/apisix/admin/upstreams/3" -H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" -X PUT -d '
{
  "type": "roundrobin",
  "nodes": {
    "192.168.29.117:2121": 1
  }
}'

Response

{"key":"\/apisix\/upstreams\/3","value":{"id":"3","scheme":"http","type":"roundrobin","hash_on":"vars","update_time":1677889633,"pass_host":"pass","nodes":{"192.168.29.117:2121":1},"create_time":1677889633}}

Step4: Create route for backend flask application

Routes specify how requests to APISIX are forwarded to the Upstream. They match a client’s request based on defined rules and loads and executes the configured Plugins.

Request

[admin@fedser apisix]$ curl "http://192.168.29.117:9180/apisix/admin/routes/3" \
-H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" -X PUT -d '
{
  "methods": ["POST"],
  "host": "example.com",
  "uri": "/postjson",
  "upstream_id": "3"
}'

Response

{"key":"\/apisix\/routes\/3","value":{"upstream_id":"3","priority":0,"update_time":1677890214,"id":"3","methods":["POST"],"create_time":1677889764,"uri":"\/postjson","host":"example.com","status":1}}

Step5: Test the Services

We can validate that we are able to reach the backend application throug the API gateway as shown below.

[admin@fedser flask]$ curl -X POST -H "Content-type: application/json" -H "Host: example.com" 'http://192.168.29.117:9080/postjson' -d @postdata.json
172.20.0.3 - - [04/Mar/2023 06:07:24] "POST /postjson HTTP/1.1" 200 -
{"age":20,"gender":"Male","name":{"firstname":"Alice","lastname":"land","middlename":"Wonder"}}

Step6: Generate the JSON Schema for the Payload

Here in this step we are going to generate a JSON schema for the below JSON input data. There are different online tools available to generate the JSON schema. I have used the following online-json-to-schema-converter to generate the schema.

Input JSON data

{
"name": {
	"firstname": "Alice",
  	"middlename": "Wonder",
        "lastname": "land"
},
"age": 20,
"gender": "Male"
}

Output JSON schema

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "object",
  "properties": {
    "name": {
      "type": "object",
      "properties": {
        "firstname": {
          "type": "string"
        },
        "middlename": {
          "type": "string"
        },
        "lastname": {
          "type": "string"
        }
      },
      "required": [
        "firstname",
        "middlename",
        "lastname"
      ]
    },
    "age": {
      "type": "integer"
    },
    "gender": {
      "type": "string"
    }
  },
  "required": [
    "name",
    "age",
    "gender"
  ]
}

Step7: Enable request-validation plugin for Route

Plugins play a very important role here in Apache APISIX gateway. Here we are going to use request-validation plugin for API Gateway Apache APISIX to secure our API from being bombarded with invalid json payload.

The request-validation Plugin can be used to validate the requests before forwarding them to an Upstream service. This Plugin uses JSON Schema for validation and can be used to validate the headers and body of the request.

We will be using the generated JSON schema in the “plugins – body_schema” section and update our properties with the required constraints that we want to apply and save it to a file named “postdata.json.schema”.

Here i have updated the “age” property to accept values between 1 and 100 as an example constraint.

[admin@fedser flask]$ cat postdata.json.schema 
{
  "methods": ["POST"],
  "host": "example.com",
  "uri": "/postjson",
  "plugins": {
	"request-validation": {
        	"body_schema": {
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "object",
  "properties": {
    "name": {
      "type": "object",
      "properties": {
        "firstname": {
          "type": "string"
        },
        "middlename": {
          "type": "string"
        },
        "lastname": {
          "type": "string"
        }
      },
      "required": [
        "firstname",
        "middlename",
        "lastname"
      ]
    },
    "age": {
      "type": "integer",
      "minimum": 1,
      "maximum": 100
    },
    "gender": {
      "type": "string"
    }
  },
  "required": [
    "name",
    "age",
    "gender"
  ]
}
        }
    },
  "upstream_id": "3"
}

We need to now apply the JSON Schema with Constraints to the Route as shown below.

Request

[admin@fedser flask]$ curl "http://192.168.29.117:9180/apisix/admin/routes/3" -H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" -X PUT -d @postdata.json.schema

Response

{"key":"\/apisix\/routes\/3","value":{"methods":["POST"],"status":1,"uri":"\/postjson","plugins":{"request-validation":{"body_schema":{"properties":{"gender":{"type":"string"},"name":{"properties":{"lastname":{"type":"string"},"firstname":{"type":"string"},"middlename":{"type":"string"}},"required":["firstname","middlename","lastname"],"type":"object"},"age":{"minimum":1,"maximum":100,"type":"integer"}},"$schema":"http:\/\/json-schema.org\/draft-04\/schema#","required":["name","age","gender"],"type":"object"},"rejected_code":400}},"id":"3","host":"example.com","create_time":1677889764,"upstream_id":"3","priority":0,"update_time":1677893058}}

Step8: Re-test the API Service

The “postdata.json” is good enough and should not break our request as shown below.

[admin@fedser flask]$ curl -X POST -H "Content-type: application/json" -H "Host: example.com" 'http://192.168.29.117:9080/postjson' -d @postdata.json
172.20.0.3 - - [04/Mar/2023 06:07:24] "POST /postjson HTTP/1.1" 200 -
{"age":20,"gender":"Male","name":{"firstname":"Alice","lastname":"land","middlename":"Wonder"}}

Now let’s create a “baddata.json” as shown below in which we will update the age value to be 120 which is out of bound from the constraint that we applied using the JSON schema.

[admin@fedser flask]$ cat baddata.json 
{
"name": {
	"firstname": "Alice",
  	"middlename": "Wonder",
	"lastname": "land"
},
"age": 120,
"gender": "male"
}

Now let’s try to re-test the API service by sending the following API request to the APISIX gateway service. If everything is correctly configured you should see that the API Gateway service will reject this request from being forwarded to the Upstream with a Validation Failed error as shown below.

[admin@fedser flask]$ curl -X POST -H "Content-type: application/json" -H "Host: example.com" 'http://192.168.29.117:9080/postjson' -d @baddata.json
property "age" validation failed: expected 120 to be at most 100

Hope you enjoyed reading this article. Thank you..