security: gate custom code guardrail exec() behind LITELLM_ENABLE_CUSTOM_CODE_GUARDRAILS

CustomCodeGuardrail and the test endpoint both exec() user-supplied
code in the same process. RestrictedPython sandbox escapes are well-
documented. This makes custom code guardrails opt-in only by requiring
LITELLM_ENABLE_CUSTOM_CODE_GUARDRAILS=true to be set explicitly.

Also updates existing tests to enable the flag via monkeypatch.
This commit is contained in:
Jack Pippett 2026-04-28 12:25:14 -07:00
parent 0c81cd5a18
commit 67fb5adbfd
4 changed files with 30 additions and 0 deletions

View file

@ -2071,6 +2071,14 @@ async def test_custom_code_guardrail(
detail="Admin access required to test custom code guardrails",
)
if os.getenv("LITELLM_ENABLE_CUSTOM_CODE_GUARDRAILS", "").lower() != "true":
return TestCustomCodeGuardrailResponse(
success=False,
error="Custom code guardrails are disabled by default. "
"Set LITELLM_ENABLE_CUSTOM_CODE_GUARDRAILS=true to enable.",
error_type="compilation",
)
EXECUTION_TIMEOUT_SECONDS = 5
try:

View file

@ -35,6 +35,7 @@ Example: block when response rejects the user (input_type response only):
"""
import asyncio
import os
import threading
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Type, cast
@ -143,8 +144,17 @@ class CustomCodeGuardrail(CustomGuardrail):
"""Returns the config model for the UI."""
return CustomCodeGuardrailConfigModel
@staticmethod
def _require_custom_code_enabled() -> None:
if os.getenv("LITELLM_ENABLE_CUSTOM_CODE_GUARDRAILS", "").lower() != "true":
raise CustomCodeCompilationError(
"Custom code guardrails are disabled by default. "
"Set LITELLM_ENABLE_CUSTOM_CODE_GUARDRAILS=true to enable."
)
def _do_compile(self) -> None:
"""Internal compilation method without lock. Expected to run inside _compile_lock."""
self._require_custom_code_enabled()
exec_globals = build_sandbox_globals()
compiled = compile_sandboxed(self.custom_code)
exec(compiled, exec_globals) # noqa: S102

View file

@ -9,6 +9,11 @@ from litellm.proxy.guardrails.guardrail_hooks.custom_code import (
)
@pytest.fixture(autouse=True)
def enable_custom_code_guardrails(monkeypatch):
monkeypatch.setenv("LITELLM_ENABLE_CUSTOM_CODE_GUARDRAILS", "true")
@pytest.fixture
def response_rejection_guardrail():
"""Guardrail instance using the response-rejection custom code."""

View file

@ -1,3 +1,5 @@
import os
import pytest
from litellm.proxy.guardrails.guardrail_hooks.custom_code.custom_code_guardrail import (
@ -6,6 +8,11 @@ from litellm.proxy.guardrails.guardrail_hooks.custom_code.custom_code_guardrail
)
@pytest.fixture(autouse=True)
def enable_custom_code_guardrails(monkeypatch):
monkeypatch.setenv("LITELLM_ENABLE_CUSTOM_CODE_GUARDRAILS", "true")
# str.mro() + generator gi_code + code.replace(co_names=...) + __setattr__
# to swap a function's bytecode and read http_get's real builtins dict.
BYTECODE_REWRITE_PAYLOAD = (