diff --git a/tiled/_tests/test_slicer.py b/tiled/_tests/test_slicer.py new file mode 100644 index 000000000..fa79f7dbd --- /dev/null +++ b/tiled/_tests/test_slicer.py @@ -0,0 +1,106 @@ +import pytest +from fastapi import Query + +from ..server.dependencies import slice_ + +slice_test_data = [ + "", + ":", + "::" "0", + "0:", + "0::", + ":0", + "::0", + "5:", + ":10", + "::12", + "-1", + "-5:", + ":-5", + "::-45", + "3:5", + "5:3", + "123::4", + "5::678", + ":123:4", + ":5:678", + ",", + ",,", + ",:", + ":,::", + ",,:,::,,::,:,,::,", + "0,1,2", + "5:,:10,::-5", + "1:2:3,4:5:6,7:8:9", + "10::20,30::40,50::60", + "1 : 2", + "1:2, 3", + "1 ,2:3", + "1 , 2 , 3", +] + +slice_typo_data = [ + ":::", + "1:2:3:4", + "1:2,3:4:5:6", +] + +slice_malicious_data = [ + "1:(2+3)", + "1**2", + "print('oh so innocent')", + "; print('oh so innocent')", + ")\"; print('oh so innocent')", + "1:2)\"; print('oh so innocent')", + "1:2)\";print('oh_so_innocent')", + "import sys; sys.exit()", + "; import sys; sys.exit()", + "touch /tmp/x", + "rm -rf /tmp/*", +] + + +# this is the outgoing slice_ function from tiled.server.dependencies as is +def reference_slice_( + slice: str = Query(None, pattern="^[-0-9,:]*$"), +): + "Specify and parse a block index parameter." + import numpy + + # IMPORTANT We are eval-ing a user-provider string here so we need to be + # very careful about locking down what can be in it. The regex above + # excludes any letters or operators, so it is not possible to execute + # functions or expensive arithmetic. + return tuple( + [ + eval(f"numpy.s_[{dim!s}]", {"numpy": numpy}) + for dim in (slice or "").split(",") + if dim + ] + ) + + +@pytest.mark.parametrize("slice", slice_test_data) +def test_slicer(slice: str): + """ + Test the slicer function + """ + assert slice_(slice) == reference_slice_(slice) + + +@pytest.mark.parametrize("slice", slice_typo_data) +def test_slicer_typo_data(slice: str): + """ + Test the slicer function with invalid input + """ + with pytest.raises(TypeError): + _ = slice_(slice) + + +@pytest.mark.parametrize("slice", slice_malicious_data) +def test_slicer_malicious_exec(slice: str): + """ + Test the slicer function with 'malicious' input + """ + with pytest.raises(ValueError): + _ = slice_(slice) diff --git a/tiled/server/dependencies.py b/tiled/server/dependencies.py index ccca20785..a1e38f672 100644 --- a/tiled/server/dependencies.py +++ b/tiled/server/dependencies.py @@ -17,6 +17,9 @@ from .core import NoEntry from .utils import filter_for_access, record_timing +# saving slice() to rescue after using "slice" for FastAPI dependency injection of slice_(slice: str) +slice_func = slice + @lru_cache(1) def get_query_registry(): @@ -160,19 +163,16 @@ def expected_shape( def slice_( - slice: str = Query(None, pattern="^[-0-9,:]*$"), + slice: str | None = None, ): "Specify and parse a block index parameter." - import numpy - - # IMPORTANT We are eval-ing a user-provider string here so we need to be - # very careful about locking down what can be in it. The regex above - # excludes any letters or operators, so it is not possible to execute - # functions or expensive arithmetic. - return tuple( - [ - eval(f"numpy.s_[{dim!s}]", {"numpy": numpy}) - for dim in (slice or "").split(",") - if dim - ] - ) + + def np_style_slicer(indices: tuple): + return indices[0] if len(indices) == 1 else slice_func(*indices) + + def parse_slice_str(dim: str): + return np_style_slicer( + tuple(int(idx) if idx else None for idx in dim.split(":")) + ) + + return tuple(parse_slice_str(dim) for dim in (slice or "").split(",") if dim)