Skip to content

Commit

Permalink
Merge pull request #89 from Yelp/fix-recursion
Browse files Browse the repository at this point in the history
Fix RecursionError: Limit depth of nested Objects when generating fuzz parameters
  • Loading branch information
Chandra158 authored Feb 23, 2024
2 parents 0d5f1cd + 5df88d8 commit dc04123
Show file tree
Hide file tree
Showing 6 changed files with 47 additions and 6 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ optional arguments:
--ignore-exceptions Ignores all exceptions raised during fuzzing (aka.
only fails when vulnerabilities are found).
--disable-unicode Disable unicode characters in fuzzing, only use ASCII.
--depth DEPTH Maximum depth for generating nested fuzz parameters.
```

Expand Down
4 changes: 4 additions & 0 deletions fuzz_lightyear/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ENABLE_COLOR = True
MAX_FUZZ_DEPTH = 6
SEED_RANDOM_LENGTH = 128
UNICODE_ENABLED = True
24 changes: 21 additions & 3 deletions fuzz_lightyear/fuzzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def _fuzz_parameter(
parameter: Dict[str, Any],
operation_id: str = None,
required: bool = False,
depth: int = 0,
) -> SearchStrategy:
"""
:param required: for object types, the required parameter is in a
Expand Down Expand Up @@ -78,7 +79,12 @@ def _fuzz_parameter(
}
fuzz_fn = mapping[_type]
if fuzz_fn in (_fuzz_object, _fuzz_array):
strategy = fuzz_fn(parameter, operation_id, required=required) # type: ignore
strategy = fuzz_fn(
parameter,
operation_id,
depth + 1,
required=required,
) # type: ignore
else:
strategy = fuzz_fn(parameter, required=required) # type: ignore

Expand Down Expand Up @@ -168,19 +174,22 @@ def _fuzz_boolean(
def _fuzz_array(
parameter: Dict[str, Any],
operation_id: str = None,
depth: int = 0,
required: bool = False,
) -> SearchStrategy:
item = parameter['items']
required = parameter.get('required', required)

# TODO: Handle `oneOf`
strategy = st.lists(
elements=_fuzz_parameter(item, operation_id, required=required),
elements=_fuzz_parameter(item, operation_id, required=required, depth=depth + 1),
min_size=parameter.get(
'minItems',
0 if not required else 1,
),
max_size=parameter.get('maxItems', None),
max_size=(0
if depth > get_settings().max_fuzz_depth
else parameter.get('maxItems', None)),
)
if not required:
return st.one_of(st.none(), strategy)
Expand All @@ -191,10 +200,18 @@ def _fuzz_array(
def _fuzz_object(
parameter: Dict[str, Any],
operation_id: str = None,
depth: int = 0,
**kwargs: Any,
) -> SearchStrategy:
# TODO: Handle `additionalProperties`
output = {}

if (
depth > get_settings().max_fuzz_depth or
'properties' not in parameter
):
return st.none()

for name, specification in parameter['properties'].items():
try:
strategy = _get_strategy_from_factory(
Expand Down Expand Up @@ -224,6 +241,7 @@ def _fuzz_object(
specification,
operation_id,
bool(required),
depth + 1,
)

return st.fixed_dictionaries(output)
Expand Down
5 changes: 5 additions & 0 deletions fuzz_lightyear/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from hypothesis.errors import NonInteractiveExampleWarning
from swagger_spec_validator.common import SwaggerValidationError # type: ignore

from .config import MAX_FUZZ_DEPTH
from .datastore import get_excluded_operations
from .datastore import get_non_vulnerable_operations
from .datastore import get_setup_fixtures
Expand Down Expand Up @@ -56,6 +57,7 @@ def main(argv: Optional[List[str]] = None) -> int:
seed=args.seed,
ignore_exceptions=args.ignore_exceptions,
disable_unicode=args.disable_unicode,
max_fuzz_depth=args.depth,
)

outputter.show_results()
Expand Down Expand Up @@ -112,6 +114,7 @@ def run_tests(
seed: int = None,
ignore_exceptions: bool = False,
disable_unicode: bool = False,
max_fuzz_depth: int = MAX_FUZZ_DEPTH,
) -> ResultFormatter:
"""
:param tests: list of tests to run.
Expand All @@ -121,12 +124,14 @@ def run_tests(
:param seed: used for random generation of test input
:param ignore_exceptions: if True, ignores HTTP exceptions to requests.
:param disable_unicode: if True, only use ASCII characters to fuzz strings
:param max_fuzz_depth: used for preventing recursion Objects with self reference
"""
if seed is not None:
get_settings().seed = seed
if disable_unicode:
get_settings().unicode_enabled = False

get_settings().max_fuzz_depth = max_fuzz_depth
outputter = ResultFormatter()
for result in generate_sequences(
n=iterations,
Expand Down
12 changes: 9 additions & 3 deletions fuzz_lightyear/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@

from hypothesis import core

from fuzz_lightyear.config import ENABLE_COLOR
from fuzz_lightyear.config import MAX_FUZZ_DEPTH
from fuzz_lightyear.config import SEED_RANDOM_LENGTH
from fuzz_lightyear.config import UNICODE_ENABLED


class Settings:
def __init__(self) -> None:
self.seed = random.getrandbits(128) # type: int
self.unicode_enabled = True # type: bool
self.enable_color = True # type: bool
self.seed = random.getrandbits(SEED_RANDOM_LENGTH) # type: int
self.unicode_enabled = UNICODE_ENABLED # type: bool
self.enable_color = ENABLE_COLOR # type: bool
self.max_fuzz_depth = MAX_FUZZ_DEPTH # type: int

@property
def seed(self) -> int:
Expand Down
7 changes: 7 additions & 0 deletions fuzz_lightyear/usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ def parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace:
),
)

parser.add_argument(
'--depth',
default=6,
type=int,
help='Maximum depth for generating nested fuzz parameters.',
)

return parser.parse_args(argv)


Expand Down

0 comments on commit dc04123

Please sign in to comment.