Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a way to specify combinations of OpenAPI parameters to fuzz via extension field #17735

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions w3af/core/data/parsers/doc/open_api/specification.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

from yaml import load
from bravado_core.spec import Spec
from bravado_core.operation import Operation

try:
from yaml import CLoader as Loader, CDumper as Dumper
Expand Down Expand Up @@ -98,13 +99,28 @@ def _set_operation_params(self, operation):
* One only containing values for the required fields
* One containing values for the required and optional fields
"""
parameter_handler = ParameterHandler(self.spec, operation)
has_optional = parameter_handler.operation_has_optional_params()

for optional in {False, has_optional}:
op = parameter_handler.set_operation_params(optional=optional)
if op is not None:
yield op
spec = operation.op_spec
templates = [{}]
if 'x-w3af-request-templates' in spec:
templates = spec['x-w3af-request-templates']

for template in templates:
spec_copy = dict(spec)
for k, v in template.items():
spec_copy[k] = v
templated_operation = Operation.from_spec(
self.spec,
operation.path_name,
operation.http_method,
spec_copy
)
parameter_handler = ParameterHandler(self.spec, templated_operation)
has_optional = parameter_handler.operation_has_optional_params()

for optional in {False, has_optional}:
op = parameter_handler.set_operation_params(optional=optional)
if op is not None:
yield op

def _parse_spec_from_dict(self, spec_dict, retry=True):
"""
Expand Down
103 changes: 103 additions & 0 deletions w3af/core/data/parsers/doc/open_api/tests/data/templated.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
{
"swagger": "2.0",
"info": {
"version": "1.0.0",
"title": "Swagger Petstore",
"description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"name": "Swagger API Team"
},
"license": {
"name": "MIT"
}
},
"host": "petstore.swagger.io",
"basePath": "/api",
"schemes": [
"http"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/pets": {
"get": {
"description": "Gets a pet by a criteria",
"operationId": "getPetBy",
"produces": [
"application/json"
],
"parameters": [
{
"name": "name",
"in": "query",
"description": "name of pet",
"required": false,
"type": "string"
},
{
"name": "owner",
"in": "query",
"description": "owner of pet",
"required": false,
"type": "string"
}
],
"responses": {
"200": {
"description": "pet response",
"schema": {
"$ref": "#/definitions/Pet"
}
}
},
"x-w3af-request-templates": [
{
"parameters": [
{
"name": "name",
"in": "query",
"description": "name of pet",
"required": true,
"type": "string",
"default": "Rover"
}
]
},
{
"parameters": [
{
"name": "owner",
"in": "query",
"description": "owner of pet",
"required": true,
"type": "string",
"default": "John Smith"
}
]
}
]
}
}
},
"definitions": {
"Pet": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
},
"tag": {
"type": "string"
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ def get_specification(self):
return file('%s/data/multiple_paths_and_headers.json' % CURRENT_PATH).read()


class Templated(object):
def get_specification(self):
return file('%s/data/templated.json' % CURRENT_PATH).read()


class PetstoreSimpleModel(object):

@staticmethod
Expand Down
44 changes: 43 additions & 1 deletion w3af/core/data/parsers/doc/open_api/tests/test_specification.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
NestedModel,
NestedLoopModel,
ArrayModelItems,
MultiplePathsAndHeaders)
MultiplePathsAndHeaders,
Templated)


class TestSpecification(unittest.TestCase):
Expand Down Expand Up @@ -586,6 +587,47 @@ def test_parameter_handler_multiple_paths_and_headers(self):
handler = SpecificationHandler(http_response)
self.check_parameter_setting(handler)

def test_templated_operation(self):
specification_as_string = Templated().get_specification()
http_response = self.generate_response(specification_as_string)
handler = SpecificationHandler(http_response)

data = [d for d in handler.get_api_information()]

# The specification contains two templates for the operation, each
# with a single required parameter
self.assertEqual(len(data), 2)

(spec, api_resource_name, resource,
operation_name, operation, parameters) = data[0]

self.assertEqual(api_resource_name, 'pets')
self.assertEqual(operation_name, 'getPetBy')
self.assertEqual(operation.consumes, [u'application/json'])
self.assertEqual(operation.produces, [u'application/json'])
self.assertEqual(operation.path_name, '/pets')

# Now we check the parameters for the operation
self.assertEqual(len(operation.params), 1)

param = operation.params.get('name')
self.assertEqual(param.param_spec['required'], True)
self.assertEqual(param.param_spec['in'], 'query')
self.assertEqual(param.param_spec['type'], 'string')
self.assertEqual(param.fill, 'Rover')

# And check the second one too
(spec, api_resource_name, resource,
operation_name, operation, parameters) = data[1]

self.assertEqual(len(operation.params), 1)

param = operation.params.get('owner')
self.assertEqual(param.param_spec['required'], True)
self.assertEqual(param.param_spec['in'], 'query')
self.assertEqual(param.param_spec['type'], 'string')
self.assertEqual(param.fill, 'John Smith')

def check_parameter_setting(self, spec_handler):
data = [d for d in spec_handler.get_api_information()]
self.assertIsNotNone(data)
Expand Down
29 changes: 29 additions & 0 deletions w3af/plugins/crawl/open_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,4 +458,33 @@ def get_long_desc(self):
During parsing an Open API specification, the plugin looks for parameters
which are passed to endpoints via HTTP headers, and enables them for further testing.
This behavior may be disabled by setting 'discover_fuzzable_headers' configuration parameter to False.

To allow for testing of complex REST APIs, this plugin supports an extension to the Open API
specification: If an extension field named `x-w3af-request-templates` exists on an operation,
this plugin will use the contents to create multiple requests for the operation, each configured
in a specific way. The `x-w3af-request-templates` field is structured as a list of dicts, each of
which contains overrides to the operation it appears on. For each item in the list, the plugin
will create a separate URL to test, configured according to the overrides in that item. For example,
the following fragment would produce two URLs for testing:

...
"x-w3af-request-templates": [
{
"parameters": [
{"name": "foo", "in": "query", "default": "value1"},
]
},
{
"parameters": [
{"name": "bar", "in": "query", "default": "value2"},
]
}
]
...

If the original operation was http://www.w3af.org/api/test?foo=&bar=,
then this configuration would produce for testing the URLs
http://www.w3af.org/api/test?foo=value1
http://www.w3af.org/api/test?bar=value2

"""