From 8b8347c9d563579d1fc6c35c44317923e62f842c Mon Sep 17 00:00:00 2001 From: akushonkamen Date: Sat, 10 Oct 2026 16:51:22 +0800 Subject: [PATCH] fix(server): disable code-executing plugins by default, add opt-in flag load_plugins() registered every module with a SLUG and a run() unconditionally, so the executecode plugin - which runs Python from user requests and LLM responses in a live Jupyter kernel with no sandboxing - was active in all deployments by default. With no optillm_api_key set, any unauthenticated request could reach full code execution on the host (issue #302). - Add UNSAFE_DEFAULT_DISABLED_PLUGINS and skip such plugins during load_plugins() with a warning that names the exact flag to re-enable - Opt back in via --enable-unsafe-plugins (CLI), OPTILLM_ENABLE_UNSAFE_PLUGINS (env) or server_config; comma-separated slugs, same three-part pattern as --readurls-allow-internal - Reload plugins after parse_args() in main() so the opt-in (and --plugins-dir) apply to the loaded set; the pre-parse load is kept for --approach choices. With the env var set even the first load includes the plugin, so --approach keeps working - README: mark the executecode row as disabled by default and add a code-execution security section mirroring the readurls one - No sandboxing in this change (nsjail/gVisor-style isolation is out of scope); pure stdlib, no new dependencies Note: this is an intentional default behavior change - deployments that rely on executecode must now set the flag or env var. Fixes #302 --- README.md | 17 +++++- optillm/server.py | 22 ++++++++ tests/test_plugin_gating.py | 104 ++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 tests/test_plugin_gating.py diff --git a/README.md b/README.md index 1706482..5812f2e 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,21 @@ optillm ⚠️ **Security Note**: Only enable this on trusted networks where internal URL fetching is actually required. +## 🔒 Code Execution Security (executecode plugin) + +The `executecode` plugin executes Python code (from user requests and LLM responses) in a live Jupyter kernel with **no sandboxing**. Because arbitrary code execution on the host cannot be safely exposed by default, this plugin is **disabled unless you explicitly opt in**: + +```bash +# Command line +optillm --enable-unsafe-plugins=executecode + +# Environment variable +export OPTILLM_ENABLE_UNSAFE_PLUGINS=executecode +optillm +``` + +⚠️ **Security Note**: Only enable this plugin in single-user, air-gapped environments where the person submitting requests is trusted with full access to the host running optillm. + ## Implemented techniques | Approach | Slug | Description | @@ -209,7 +224,7 @@ optillm | Memory | `memory` | Implements a short term memory layer, enables you to use unbounded context length with any LLM. Set `OPTILLM_MEMORY_FILE` to opt in to file-backed persistence so memories survive across requests | | Privacy | `privacy` | Anonymize PII data in request and deanonymize it back to original value in response | | Read URLs | `readurls` | Reads all URLs found in the request, fetches the content at the URL and adds it to the context | -| Execute Code | `executecode` | Enables use of code interpreter to execute python code in requests and LLM generated responses | +| Execute Code | `executecode` | Enables use of code interpreter to execute python code in requests and LLM generated responses. **Disabled by default** (executes arbitrary code); opt in with `--enable-unsafe-plugins=executecode` | | JSON | `json` | Enables structured outputs using the outlines library, supports pydantic types and JSON schema | | GenSelect | `genselect` | Generative Solution Selection - generates multiple candidates and selects the best based on quality criteria | | Web Search | `web_search` | Performs Google searches using Chrome automation (Selenium) to gather search results and URLs | diff --git a/optillm/server.py b/optillm/server.py index 77d1586..4a9dcd9 100644 --- a/optillm/server.py +++ b/optillm/server.py @@ -196,6 +196,7 @@ def count_reasoning_tokens(text: str, tokenizer=None) -> int: 'ssl_verify': True, 'ssl_cert_path': '', 'readurls_allow_internal': False, + 'enable_unsafe_plugins': '', } # List of known approaches @@ -285,10 +286,20 @@ def none_approach( logger.error(f"Error in none approach: {str(e)}") raise +# Plugins that execute arbitrary code are disabled by default and require an +# explicit opt-in via --enable-unsafe-plugins (env: OPTILLM_ENABLE_UNSAFE_PLUGINS). +UNSAFE_DEFAULT_DISABLED_PLUGINS = {"executecode"} + def load_plugins(): # Clear existing plugins first but modify the global dict in place plugin_approaches.clear() + # Slugs explicitly opted in via --enable-unsafe-plugins (env: + # OPTILLM_ENABLE_UNSAFE_PLUGINS). The env fallback covers the initial + # load in main(), which runs before parse_args() populates server_config. + enabled_arg = server_config.get('enable_unsafe_plugins', '') or os.environ.get('OPTILLM_ENABLE_UNSAFE_PLUGINS', '') + enabled_unsafe_plugins = {slug.strip() for slug in enabled_arg.split(',') if slug.strip()} + # Get installed package plugins directory import optillm package_plugin_dir = os.path.join(os.path.dirname(optillm.__file__), 'plugins') @@ -328,6 +339,12 @@ def load_plugins(): spec.loader.exec_module(module) if hasattr(module, 'SLUG') and hasattr(module, 'run'): + if module.SLUG in UNSAFE_DEFAULT_DISABLED_PLUGINS and module.SLUG not in enabled_unsafe_plugins: + logger.warning( + f"Plugin '{module.SLUG}' is disabled by default because it executes arbitrary code. " + f"Start optillm with --enable-unsafe-plugins={module.SLUG} " + f"(env: OPTILLM_ENABLE_UNSAFE_PLUGINS) to allow it.") + continue if module.SLUG in plugin_approaches: logger.info(f"Overriding {source} plugin: {module.SLUG}") plugin_approaches[module.SLUG] = module.run @@ -1096,6 +1113,7 @@ def parse_args(): ("--log-conversations", "OPTILLM_LOG_CONVERSATIONS", bool, False, "Enable conversation logging with full metadata"), ("--conversation-log-dir", "OPTILLM_CONVERSATION_LOG_DIR", str, str(Path.home() / ".optillm" / "conversations"), "Directory to save conversation logs"), ("--readurls-allow-internal", "OPTILLM_READURLS_ALLOW_INTERNAL", bool, False, "Allow the readurls plugin to fetch non-public/internal URLs (disables SSRF protection)"), + ("--enable-unsafe-plugins", "OPTILLM_ENABLE_UNSAFE_PLUGINS", str, "", "Comma-separated slugs of code-executing plugins (e.g. executecode) to enable; such plugins are disabled by default"), ] for arg, env, type_, default, help_text, *extra in args_env: @@ -1190,6 +1208,10 @@ def main(): # Update server_config with all argument values server_config.update(vars(args)) + # Reload plugins now that server_config reflects the CLI/env options, so + # --enable-unsafe-plugins (and --plugins-dir) take effect for the loaded set. + load_plugins() + port = server_config['port'] # Initialize request batcher if batch mode is enabled diff --git a/tests/test_plugin_gating.py b/tests/test_plugin_gating.py new file mode 100644 index 0000000..6450c8f --- /dev/null +++ b/tests/test_plugin_gating.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +""" +Tests for the plugin gating that disables code-executing plugins (executecode) +by default and requires an explicit opt-in (issue #302). + +All tests are offline: they exercise load_plugins() registration against the +packaged plugin directory and the pure helper functions of the executecode +plugin. No server, LLM client, or Jupyter kernel is started. +""" + +import sys +import os + +import pytest + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from optillm import plugin_approaches, load_plugins +from optillm.server import UNSAFE_DEFAULT_DISABLED_PLUGINS, server_config +from optillm.plugins.executecode_plugin import ( + extract_python_code, + should_execute_request_code, +) + +ENV_VAR = "OPTILLM_ENABLE_UNSAFE_PLUGINS" + + +@pytest.fixture(autouse=True) +def restore_default_plugins(monkeypatch): + """Make every test start (and leave behind) the default plugin registry.""" + monkeypatch.setitem(server_config, 'enable_unsafe_plugins', '') + monkeypatch.delenv(ENV_VAR, raising=False) + yield + load_plugins() + + +def test_unsafe_plugin_set_contains_executecode(): + assert "executecode" in UNSAFE_DEFAULT_DISABLED_PLUGINS + + +def test_executecode_disabled_by_default(): + load_plugins() + assert "executecode" not in plugin_approaches, \ + "executecode executes arbitrary code and must be opt-in" + # Gating must not disable unrelated plugins. + assert "memory" in plugin_approaches + assert "readurls" in plugin_approaches + + +def test_executecode_opt_in_via_server_config(monkeypatch): + monkeypatch.setitem(server_config, 'enable_unsafe_plugins', 'executecode') + load_plugins() + assert "executecode" in plugin_approaches + + +def test_executecode_opt_in_via_env_var(monkeypatch): + monkeypatch.setenv(ENV_VAR, "executecode") + load_plugins() + assert "executecode" in plugin_approaches + + +def test_opt_in_accepts_comma_separated_list(monkeypatch): + monkeypatch.setitem(server_config, 'enable_unsafe_plugins', 'memory, executecode ,') + load_plugins() + assert "executecode" in plugin_approaches + assert "memory" in plugin_approaches + + +def test_disabled_plugin_logs_how_to_enable(caplog): + import logging + with caplog.at_level(logging.WARNING, logger="optillm.server"): + load_plugins() + warnings = [r.message for r in caplog.records if "executecode" in r.message] + assert warnings, "expected a warning explaining why executecode was skipped" + assert "--enable-unsafe-plugins=executecode" in warnings[0] + assert ENV_VAR in warnings[0] + + +def test_extract_python_code_multiple_blocks(): + text = "Before\n```python\nprint(1)\n```\nmiddle\n```python\nprint(2)\n```\nafter" + assert extract_python_code(text) == ["print(1)", "print(2)"] + + +def test_extract_python_code_no_blocks(): + assert extract_python_code("no code here at all") == [] + + +def test_extract_python_code_requires_python_tag(): + # A bare fenced block without the `python` tag must not be extracted. + assert extract_python_code("```\nprint(1)\n```") == [] + + +def test_should_execute_request_code_keyword_hits(): + for query in ["please run this", "execute it", "show the output", "give me the result"]: + assert should_execute_request_code(query) is True, query + + +def test_should_execute_request_code_keyword_misses(): + assert should_execute_request_code("what is the capital of France?") is False + assert should_execute_request_code("") is False + + +if __name__ == "__main__": + sys.exit(pytest.main([__file__, "-v"]))