diff --git a/splunklib/searchcommands/environment.py b/splunklib/searchcommands/environment.py index 83ee939f4..474dacfeb 100644 --- a/splunklib/searchcommands/environment.py +++ b/splunklib/searchcommands/environment.py @@ -97,6 +97,12 @@ def configure_logging(logger_name, filename=None): global _current_logging_configuration_file filename = path.realpath(filename) + app_root_real = path.realpath(app_root) + if path.commonpath([filename, app_root_real]) != app_root_real: # pyright: ignore[reportUnknownArgumentType] + raise ValueError( + f'Logging configuration file "{filename}" is outside the app directory' + ) + if filename != _current_logging_configuration_file: working_directory = getcwd() chdir(app_root) @@ -114,9 +120,22 @@ def configure_logging(logger_name, filename=None): _current_logging_configuration_file = None + +def _find_app_root(app_file: str) -> str: + """Return the app root directory for a search command script.""" + directory: str = path.abspath(path.dirname(app_file)) + while True: + parent, name = path.split(directory) + if name == "bin": + return parent + if parent == directory: + return path.dirname(path.abspath(path.dirname(app_file))) + directory = parent + + splunk_home = path.abspath(path.join(getcwd(), environ.get("SPLUNK_HOME", ""))) app_file = getattr(sys.modules["__main__"], "__file__", sys.executable) -app_root = path.dirname(path.abspath(path.dirname(app_file))) +app_root = _find_app_root(app_file) splunklib_logger, logging_configuration = configure_logging("splunklib") diff --git a/tests/unit/searchcommands/test_builtin_options.py b/tests/unit/searchcommands/test_builtin_options.py index 911321251..f2827c5ef 100644 --- a/tests/unit/searchcommands/test_builtin_options.py +++ b/tests/unit/searchcommands/test_builtin_options.py @@ -137,6 +137,39 @@ def test_logging_configuration(self): f"Expected ValueError, but logging_configuration={command.logging_configuration}" ) + inside_app_root_logging_configuration = os.path.join( + environment.app_root, "default", "logging.conf" + ) + command.logging_configuration = inside_app_root_logging_configuration + assert command.logging_configuration == inside_app_root_logging_configuration, ( + "logging_configuration should accept an absolute path inside the app directory" + ) + + try: + command.logging_configuration = os.path.realpath(__file__) + except ValueError: + pass + except BaseException as e: + pytest.fail( + f"Expected ValueError for a path outside the app directory, but {type(e)} was raised" + ) + else: + pytest.fail( + f"Expected ValueError for a path outside the app directory, but {command.logging_configuration=}" + ) + + # logging_configuration raises a value error when a relative path traverses outside the app directory (RCE guard) + try: + command.logging_configuration = os.path.join("..", "..", "..", "__init__.py") + except ValueError: + pass + except BaseException as e: + pytest.fail(f"Expected ValueError, but {type(e)} was raised") + else: + pytest.fail( + f"Expected ValueError, but logging_configuration={command.logging_configuration}" + ) + def test_logging_level(self): rebase_environment("app_without_logging_configuration") command = StubbedSearchCommand() diff --git a/tests/unit/searchcommands/test_environment.py b/tests/unit/searchcommands/test_environment.py new file mode 100644 index 000000000..e2eb48ff5 --- /dev/null +++ b/tests/unit/searchcommands/test_environment.py @@ -0,0 +1,37 @@ +# Copyright © 2011-2026 Splunk, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"): you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os + +import pytest + +from splunklib.searchcommands.environment import ( + _find_app_root, # pyright: ignore[reportPrivateUsage] +) + + +@pytest.mark.parametrize( + ("app_file_parts", "expected_app_root_parts"), + [ + (("apps", "my_app", "bin", "command.py"), ("apps", "my_app")), + (("apps", "my_app", "bin", "linux_x86_64", "command.py"), ("apps", "my_app")), + (("apps", "my_app", "bin", "foo", "bar", "command.py"), ("apps", "my_app")), + (("some", "other", "layout", "command.py"), ("some", "other")), + ], +) +def test_find_app_root( + app_file_parts: tuple[str, ...], expected_app_root_parts: tuple[str, ...] +) -> None: + app_file = os.path.join(*app_file_parts) + assert _find_app_root(app_file) == os.path.abspath(os.path.join(*expected_app_root_parts))