Schema in a response defines combining operations when 'additionalProperties' is set to 'false'

Issue ID: v3-schema-response-xof-additionalproperties-false

Average severity: Medium

Description

The schema defines combining operations allOf, anyOf, or oneOf while additionalProperties is set to false. This may result in all messages being blocked.

  • 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).

Note that schemas listed in an allOf, anyOf, or oneOf array know nothing of the other schemas on the list. You cannot use combining operations to add more details from one schema to another to “extend” it in the sense of object-oriented inheritance.

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. A reusable schema in the #/component/schemas section has been extended with an enum from allOf, but the properties have also been restricted in additionalProperties. The schema will reject everything, because properties refers to the entire reusable schema:

{
    "components": {
        "schemas": {
            "Pet": {
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string"
                    },
                    "petType": {
                        "type": "string"
                    }
                },
                "required": [
                    "name",
                    "petType"
                ]
            },
            "Cat": {
                "type": "object",
                "allOf": [
                    {
                        "$ref": "#/components/schemas/Pet"
                    },
                    {
                        "type": "object",
                        "properties": {
                            "furType": {
                                "type": "enum",
                                "enum": [
                                    "short-haired",
                                    "long-haired",
                                    "curly",
                                    "naked"
                                ],
                                "default": "short-haired"
                            }
                        }
                    }
                ],
                "additionalProperties": false
            }
        }
    }
}

Possible exploit scenario

Attackers strive to make your APIs behave in an unexpected way to learn more about your system or to cause a data breach. Good data definition quality in the schemas used in API responses allows reliably validating that the contents of outgoing API responses are as expected.

While filtering API responses does not block a specific kind of attack, it is there as a damage control mechanism in the unfortunate event that a successful attack has been conducted: it allows blocking the response and prevent attackers from retrieving data they should not access.

In the vast majority of cases (with the notable exception of Denial of Service (DoS and DDoS) attacks) attacks are conducted because attackers want to access data or resources they should not have access to. Often, this means that the structure or the size of the API response changes as a result of a successful attack, compared to a normal API response.

Validating that API responses are as expected can be achieved through proper schema validation of the API responses. The accuracy of this depends on the quality of the response schemas: the better defined your schemas are, the easier it is to detect when something is not right.

Remediation

Do not use combining operations like inheritance in an object-oriented language. When using combining operations in your schemas, pay attention when and how you restrict additional properties. The best remediation option depends on what combining operations the schema uses, on how many levels, and the type of the subchemas of the combining operations.

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.