From 9f5e337d17706d15f4208ec9e39b06effda705f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20J=C4=99drecki?= Date: Fri, 21 Aug 2026 13:19:12 +0200 Subject: [PATCH 1/2] Disallow logging.conf from elsewhere than the app folder --- splunklib/searchcommands/environment.py | 20 +++++++- .../searchcommands/test_builtin_options.py | 37 +++++++++++++- tests/unit/searchcommands/test_environment.py | 48 +++++++++++++++++++ 3 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 tests/unit/searchcommands/test_environment.py diff --git a/splunklib/searchcommands/environment.py b/splunklib/searchcommands/environment.py index 83ee939f4..a5acb82d8 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,21 @@ def configure_logging(logger_name, filename=None): _current_logging_configuration_file = None + +def _find_app_root(app_file: str, splunk_home: str) -> str: + """Return the app root directory for a search command script.""" + splunk_apps_dir = path.join(splunk_home, "etc", "apps") + app_relpath = path.relpath(path.abspath(app_file), splunk_apps_dir) + app_dir = app_relpath.split(path.sep, 1)[0] + if app_dir == path.pardir: # app_file not in $SPLUNK_HOME/etc/apps + return path.dirname(path.abspath(path.dirname(app_file))) + + return path.join(splunk_apps_dir, app_dir) + + 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, splunk_home) 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..b7bc5d00b 100644 --- a/tests/unit/searchcommands/test_builtin_options.py +++ b/tests/unit/searchcommands/test_builtin_options.py @@ -24,7 +24,7 @@ from splunklib.searchcommands import environment from splunklib.searchcommands.decorators import Configuration from splunklib.searchcommands.search_command import SearchCommand -from tests.unit.searchcommands import package_directory, rebase_environment +from tests.unit.searchcommands import rebase_environment # portable log level names @@ -126,7 +126,7 @@ def test_logging_configuration(self): try: command.logging_configuration = os.path.join( - package_directory, "non-existent.logging.conf" + os.path.dirname(os.path.realpath(__file__)), "non-existent.logging.conf" ) except ValueError: pass @@ -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..57a06ba4e --- /dev/null +++ b/tests/unit/searchcommands/test_environment.py @@ -0,0 +1,48 @@ +# 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] +) + +_SPLUNK_HOME = os.path.join(os.sep, "opt", "splunk") +_APPS_DIRECTORY = os.path.join(_SPLUNK_HOME, "etc", "apps") + + +@pytest.mark.parametrize( + ("app_file_parts", "expected_app_root_parts"), + [ + # A script located directly in the app's bin directory + (("my_app", "bin", "command.py"), ("my_app",)), + # A script located in a subdirectory of bin, including one that happens to be + # named "bin" itself; the app root is still $SPLUNK_HOME/etc/apps/my_app + (("my_app", "bin", "foo", "bin", "command.py"), ("my_app",)), + ], +) +def test_find_app_root( + app_file_parts: tuple[str, ...], expected_app_root_parts: tuple[str, ...] +) -> None: + app_file = os.path.join(_APPS_DIRECTORY, *app_file_parts) + expected_app_root = os.path.join(_APPS_DIRECTORY, *expected_app_root_parts) + assert _find_app_root(app_file, _SPLUNK_HOME) == expected_app_root + + +def test_find_app_root_falls_back_on_nonstandard_splunk_home() -> None: + app_file = os.path.join("some", "other", "layout", "command.py") + expected_app_root = os.path.dirname(os.path.abspath(os.path.dirname(app_file))) + assert _find_app_root(app_file, _SPLUNK_HOME) == expected_app_root From 78827843329bad1f769dda1832866a43b389ce18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20J=C4=99drecki?= Date: Fri, 21 Aug 2026 14:15:18 +0200 Subject: [PATCH 2/2] Fix lint --- .basedpyright/baseline.json | 42 --------------------------- tests/unit/searchcommands/__init__.py | 2 +- 2 files changed, 1 insertion(+), 43 deletions(-) diff --git a/.basedpyright/baseline.json b/.basedpyright/baseline.json index 20155ed3a..3173168e0 100644 --- a/.basedpyright/baseline.json +++ b/.basedpyright/baseline.json @@ -42226,32 +42226,6 @@ } } ], - "./tests/unit/searchcommands/__init__.py": [ - { - "code": "reportUnknownParameterType", - "range": { - "startColumn": 23, - "endColumn": 27, - "lineCount": 1 - } - }, - { - "code": "reportMissingParameterType", - "range": { - "startColumn": 23, - "endColumn": 27, - "lineCount": 1 - } - }, - { - "code": "reportUnknownArgumentType", - "range": { - "startColumn": 64, - "endColumn": 68, - "lineCount": 1 - } - } - ], "./tests/unit/searchcommands/chunked_data_stream.py": [ { "code": "reportUnknownParameterType", @@ -42751,14 +42725,6 @@ } ], "./tests/unit/searchcommands/test_builtin_options.py": [ - { - "code": "reportUnknownVariableType", - "range": { - "startColumn": 57, - "endColumn": 75, - "lineCount": 1 - } - }, { "code": "reportDeprecated", "range": { @@ -42963,14 +42929,6 @@ "lineCount": 1 } }, - { - "code": "reportUnknownVariableType", - "range": { - "startColumn": 38, - "endColumn": 56, - "lineCount": 1 - } - }, { "code": "reportUnannotatedClassAttribute", "range": { diff --git a/tests/unit/searchcommands/__init__.py b/tests/unit/searchcommands/__init__.py index deda3b557..ee25e02c2 100644 --- a/tests/unit/searchcommands/__init__.py +++ b/tests/unit/searchcommands/__init__.py @@ -22,7 +22,7 @@ project_root = path.dirname(path.dirname(package_directory)) -def rebase_environment(name): +def rebase_environment(name: str) -> None: environment.app_root = path.join(package_directory, "apps", name) logging.Logger.manager.loggerDict.clear() del logging.root.handlers[:]