Schema in a request allows additional properties

Issue ID: v3-schema-request-object-additionalproperties-true

Average severity: Medium

Description

The schema you have defined allows additional properties, either intentionally or unintentionally.

Unlike in the OpenAPI Specification (OAS) v2, in OAS v3 it is not enough to just state the type of the properties in the schema. By default, the property additionalProperties is true. Unless you specifically set additionalProperties to false, the schema continues to accept properties of any type.

However, sometimes you intentionally want to allow additional properties, especially when using the combining operations combining operations allOf, anyOf, or oneOf.

  • If you are using combining operations for objects with properties, additionalProperties must be true or the combining operations will not work.
  • If you are using combining operations for primitives with no properties, additionalProperties can be false and the combining operations still work.
  • If you have nested combining operations inside each other (for example, allOf with anyOf nested in the properties), the correct additionalProperties value depends on the combinations of the combining operations (see Remediation).

For more details, see the OpenAPI Specification.

Example

The following is an example of how this type of risk could look in your API definition. In this case, the schema does not use any combining operations (so there is no need to allow additional properties), but additionalProperties has not been set, so it defaults to true:

{
    "post": {
        "description": "Creates a new pet in the store",
        "operationId": "addPet",
        "parameters": [
            {
                "name": "pet",
                "in": "body",
                "description": "Pet to add to the system",
                "required": true,
                "schema": {
                    "$ref": "#components/schemas/NewPet"
                }
            }
        ]
    },
    // ...
    "NewPet": {
        "type": "object",
        "required": [
            "name"
        ],
        "properties": {
            "name": {
                "type": "string",
                "description": "Pet name"
            },
            "tag": {
                "type": "string",
                "description": "Pet tag"
            }
        }
    }
}

Possible exploit scenario

If you do not clearly define the schema and you leave properties of a JSON payload empty, you effectively allow attackers to pass in any data. This means that you are opening your backend to various attacks, such as SQL injection.

This also lets attackers to try various unexpected inputs. Unexpected inputs may cause the backend server to crash or behave in an unexpected way. This in turn may cause the server to potentially leak stack trace that can be used for further attacks, or even data.

If no restrictions to the set of properties in the JSON payload are enforced, the API might also accept more fields than expected. The received payloads could be blindly transformed into an object and stored, overwriting sensitive internal data.

Remediation

The best remediation option depends on what combining operations (if any) the schema uses and on how many levels as well as the type of the subchemas of the combining operations.

In general, the safest option is not to allow additional properties. If the schema does not use allOf, anyOf, or oneOf at all, make sure you define all properties of the accepted JSON payload in the schema itself, and set additionalProperties to false. This will enforce the limitations to what the schema accepts:

{
    "post": {
        "description": "Creates a new pet in the store",
        "operationId": "addPet",
        "parameters": [
            {
                "name": "pet",
                "in": "body",
                "description": "pet to add to the system",
                "required": true,
                "schema": {
                    "$ref": "#/components/schemas/NewPet"
                }
            }
        ]
    },
    // ...
    "NewPet": {
        "type": "object",
        "additionalProperties": false,
        "required": [
            "name"
        ],
        "properties": {
            "name": {
                "type": "string",
                "description": "Pet name"
            },
            "tag": {
                "type": "string",
                "description": "Pet tag"
            }
        }
    }
}

If the combining operation is anyOf or oneOf AND its schema is a primitive with no properties, set additionalProperties to false:

{
    "properties": {
        "name": {
            "oneOf": [
                {
                    "type": "string"
                },
                {
                    "type": "integer"
                }
            ],
            "additionalProperties": false
        }
    },
    "additionalProperties": false
}

If the combining operation is anyOf or oneOf AND its schema is an object with properties, set additionalProperties to true:

{
    "type": "object",
    "anyOf": [
        {
            "type": "object",
            "required": ["age"],
            "properties": {
                "age": {
                    "minimum": 5,
                    "type": "integer"
                }

            },
            "additionalProperties": true
        },
        {
            "type": "object",
            "required": ["name"],
            "properties": {
                "name": {
                    "minLength": 3,
                    "type": "string"
                }
            },
            "additionalProperties": true
        }
    ],
    "additionalProperties": true
}

If the combining operation is allOf, additionalProperties must be true:

{
    "type": "object",
    "allOf": [
        {
            "type": "object",
            "required": ["age"],
            "properties": {
                "age": {
                    "minimum": 5,
                    "type": "integer"
                }
            },
            "additionalProperties": true
        },
        {
            "type": "object",
            "required": ["name"],
            "properties": {
                "name": {
                    "minLength": 3,
                    "type": "string"
                }

            },
            "additionalProperties": true
        }
    ],
    "additionalProperties": true
}

For nested combining operations, the basic principles above apply:

  • allOf must always have additionalProperties set to true, in both root schema and subschemas.
  • anyOf and oneOf for primitives can have additionalProperties set to false.
  • anyOf and oneOf for objects must in general have additionalProperties set to true.

However, the following clarifications should be noted:

  • If you have only oneOf in both root schema and subschemas, you can set additionalProperties to false even if the schema was for an object with properties.
  • If you have only anyOf in both root schema and subschemas, you can set additionalProperties to false in subschemas to impose stricter security, or to true for more relaxed security.
  • If you have oneOf but the root schema defines required properties, you must set additionalProperties to true.
{
    "type": "object",
    "required": ["age"],
    "properties": {
        "age": {
            "type": "integer",
            "minimum": 0
        }
    },
    "oneOf": [
        {
            "type": "object",
            "required": ["name"],
            "properties": {
                "name": {
                    "type": "string",
                    "minLength": 3 
                }
            },
            "additionalProperties": true
        }
    ],
    "additionalProperties": true
}

Sometimes compiling reusable schemas from #/components/schemas/ using combining operations may result in conflicting demands for additionalProperties.

For example, your reusable schemas could be primitives with no properties (like #/components/schemas/User and #/components/schemas/Usermail in the code example below), and you are only referencing them under oneOf (like in the path /users/search), so you decide to set additionalProperties to false to increase security.

However, then you add a path (here /users) to your API that references these reusable schemas under allOf. As stated above, allOf requires additionalProperties to be true. This creates a conflit between security and functionality of the value of additionalProperties:

  • If the reusable schemas allow additional properties, this is a security risk.
  • If the reusable schemas refenced as subschemas under allOf do not allow additional properties, the intersection of the allOf is null.
{
    "paths": {
        "/users": {
            "requestBody": {
                "content": {
                    "schema": {
                        "type": "object",
                        "allOf": [
                            {
                                "$ref": "#/components/schemas/User"
                            },
                            {
                                "$ref": "#/components/schemas/Usermail"
                            }         
                        ]
                    }
                }
            }
        },
        "/users/search": {
            "requestBody": {
                "content": {
                    "schema": {
                        "type": "object",
                        "oneOf": [
                            {   
                                "$ref": "#/components/schemas/User"
                            },
                            {
                                "$ref": "#/components/schemas/Usermail"
                            }         
                        ]
                    }
                }
            }
        }
    },
    // ...
    "components": {
        "schemas": {
            "User": {
                "type": "object",
                "properties": {
                    "firstname": {
                        "type": "string"
                    },
                    "lastname": {
                        "type": "string"
                    }
                },
                "additionalProperties": false
            },
            "Usermail": {
                "type": "object",
                "properties": {
                    "email": {
                        "type": "string"
                    }
                },
                "additionalProperties": false
            }
        }
    }
}

We recommend splitting the allOf schema (the path /users) into two separate objects to be able to enforce security while avoiding the null intersection in the allOf.

The combining operation allOf is a handy way to allow composition to re-use objects by reference, but as a drawback you must always allow additional properties, which is a security risk. Consider if you could compile the object into one schema, especially for sensitive APIs and API operations.