2

I'm using OpenApi 3. A tool I use, Owasp Zap looks at the OpenAPI doc and creates fake requests. When it gets a 404, it complains that it doesn't have the media type that the OpenAPI promises.

But I didn't write anything in the OpenAPI doc about how 404s are handled. Obviously I can't write an infinite number of bad end points & document that they return 404s.

What is the right way to record this in the OpenAPI yaml or json?

Here is a minimal yaml file... I know for sure that this file does say anything about 404, ie. 404s aren't in the contract so tools are complaining that 404s are valid responses, but 404 is what a site should return when a resource is missing

---
"openapi": "3.0.0"

paths:
    /Foo/:
        get:
            responses:
                "200":
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/Foo"
                default:
                    description: Errors
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/Error"
components:
    schemas:
        Foo:
            type: object
            required:
                - name
            properties:
                name:
                    type: string
        Error:
            type: object
            required:
                - error
            properties:
                error:
                    type: string
                message:
                    type: string
                data:
                    type: object
MatthewMartin
  • 32,326
  • 33
  • 105
  • 164
  • Can this be a problem with the webserver where you host your API? – Sasha Nov 20 '21 at 07:10
  • Problem could be anything. For what it's worth, it is a python app-- connexion on top of flask. – MatthewMartin Nov 20 '21 at 21:33
  • 1
    The wording seemed a bit unclear: is ZAP generating requests following the schema (i.e. using the paths specified) or random paths as well? The latter case would seem more like a problem with the tool — you could add a `/` path with a 404 response but that seems kludgey, especially since OpenAPI only allows one level of pattern matching (https://github.com/OAI/OpenAPI-Specification/issues/892) – Chris Adams Dec 09 '21 at 15:02
  • @ChrisAdams - It also generates endpoints like admin.php and then complains that it got a 404. I'll try that, I guess it is the equivalent of "/{Param}" where {Param} is nonexistent endpoints. – MatthewMartin Dec 09 '21 at 18:12
  • 1
    It would definitely be nice when the enhancement to OpenAPI lands since you could document e.g. a common error format but I would class that as as defect in ZAP: if it does something the API contract does not say it can do and gets the standard status code in return, that's not a problem. – Chris Adams Dec 11 '21 at 00:04

1 Answers1

3

This has been proposed already but not implemented: https://github.com/OAI/OpenAPI-Specification/issues/521

In the comments someone gave a suggestion: https://github.com/OAI/OpenAPI-Specification/issues/521#issuecomment-513055351, which reduces a little your code, but you would still have to insert N*M entries for N paths * M methods.

Since we don't have the ability to make the specification change to our needs, all that remains is we adapting ourselves.

From your profile, you seem to be a windows user. You can for example, create a new explorer context menu to your .yaml files (Add menu item to windows context menu only for specific filetype, Adding a context menu item in Windows for a specific file extension), and make it run a script that auto-fills your file.

Here, an example python script called yamlfill404.py that would be used in the context call in a way like path/to/pythonexecutable/python.exe path/to/python/script/yamlfill404.py %1, where %1 is the path to the file being right clicked.

Python file:

import yaml
from sys import argv
import re

order = ['openapi','paths','components']
level0re = re.compile('(?<=\n)[^ ][^:]+')

def _propfill(rootnode, nodes, value):
    if len(nodes) == 1:
        rootnode[nodes[0]] = value
    if len(nodes) > 1:
        nextnode = rootnode.get(nodes[0]) 
        if rootnode.get(nodes[0]) is None:
            nextnode = {}
            rootnode[nodes[0]] = nextnode
        _propfill(nextnode, nodes[1:], value)

def propfill(rootnode, nodepath, value):
    _propfill(rootnode, [n.replace('__slash__','/') for n in nodepath.replace('\/','__slash__').split('/')], value)

def yamlfill(filepath):
    with open(filepath, 'r') as file:
        yamltree = yaml.safe_load(file)
    #propfill(yamltree, 'components/schemas/notFoundResponse/...', '')
    propfill(yamltree, 'components/responses/notFound/description', 'Not found response')
    propfill(yamltree, 'components/responses/notFound/content/application\/json/schema/$ref', '#/components/schemas/notFoundResponse')
    responses = [mv['responses'] if 'responses' in mv else [] for pk,pv in (yamltree['paths'].items() if 'paths' in yamltree else []) for mk,mv in pv.items()]
    for response in responses:
        propfill(response, '404/$ref', '#/components/responses/notFound')
    yamlstring = yaml.dump(yamltree)
    offsets = [i[1] for i in sorted([(order.index(f.group(0)) if f.group(0) in order else len(order),f.start()-1) for f in [f for f in level0re.finditer('\n'+yamlstring)]])]
    offsets = [(offset,(sorted([o for o in offsets if o > offset]+[len(yamlstring)-1])[0])) for offset in offsets]
    with open(filepath[:-5]+'_404.yaml', 'w') as file:
        file.write(''.join(['\n'+yamlstring[o[0]:o[1]] for o in offsets]).strip())

yamlfill(argv[-1])

It processes the %1, which would be path/to/original.yaml and saves it as path/to/original_404.yaml (but you can change it to overwrite the original).

This example script changes the yaml formating (quotes type, spacing, ordering etc), because of the library used pyyaml. I had to reorder the file with the order = ['openapi','paths','components'], because it loses ordering. For less instrusion, maybe a more manual insertion would be better suited. Maybe one that uses only regex. Maye using awk, there are plenty of ways.

Unfortunately it is just a hack not not a solution.

MatthewMartin
  • 32,326
  • 33
  • 105
  • 164
brunoff
  • 4,161
  • 9
  • 10