Skip to content

Custom Code Guardrails UI Playground - #20377

Merged
8 commits merged into
litellm_oss_staging_02_03_2026from
litellm_dev_02_02_2026_p2
Feb 4, 2026
Merged

Custom Code Guardrails UI Playground#20377
8 commits merged into
litellm_oss_staging_02_03_2026from
litellm_dev_02_02_2026_p2

Conversation

@ghost

@ghost ghost commented Feb 4, 2026

Copy link
Copy Markdown

Relevant issues

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have Added testing in the tests/litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Type

🆕 New Feature
🐛 Bug Fix
🧹 Refactoring
📖 Documentation
🚄 Infrastructure
✅ Test

Changes

Krrish Dholakia added 4 commits February 2, 2026 18:22
first step in allowing teams to submit custom code for guardrails
support passing custom code for guardrails
allows users to write guardrails based on custom code
allows ui testing playground to sanity check if guardrail is working as expected
@vercel

vercel Bot commented Feb 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Feb 4, 2026 3:59am

Request Review

@greptile-apps

greptile-apps Bot commented Feb 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Overview

Greptile Summary

This PR adds a "Custom Code Guardrails" feature that allows users to write Python-like code to implement custom guardrail logic. The code runs in a sandboxed environment using exec() with restricted globals and a library of safe primitives.

Key Changes

  • Backend: New CustomCodeGuardrail class that executes user code via exec() with sandboxing through restricted global namespace
  • API Endpoint: /guardrails/test_custom_code for testing custom code with security validations (regex-based forbidden pattern checking, timeout protection)
  • UI Components: React-based playground and modal for writing, testing, and saving custom guardrails with code templates
  • Primitives Library: Safe functions for regex, JSON parsing, URL validation, code detection, and text utilities
  • Documentation: Comprehensive guide with examples

Critical Issues Found

  1. Security vulnerabilities in sandboxing approach: Using exec() with __builtins__ = {} is insufficient - Python objects can be introspected to escape the sandbox via dunder methods like __subclasses__, __bases__, etc.

  2. Inconsistent security validation: Forbidden pattern checks (import, exec, eval, file I/O) only apply to the test endpoint, not to production guardrails loaded from config.yaml

  3. Function signature mismatch: CustomCodePlayground.tsx calls testCustomCodeGuardrail() with 5 parameters but the function expects 2

  4. Type mismatch in test call: CustomCodeModal.tsx passes mode (pre_call/post_call) as input_type which expects request/response

  5. Blocking async operations: Custom code runs synchronously in async context, can block event loop

  6. Unrelated breaking change: Removed presidio_filter_scope field from PresidioConfigModel

Recommendations

  • Consider using a more secure sandboxing approach (RestrictedPython, separate process, or a safer language like Starlark/CEL)
  • Apply security validations consistently to both test endpoint and production code paths
  • Add resource limits (CPU, memory) beyond just timeout
  • Fix the function call signatures in the UI components

Confidence Score: 2/5

  • This PR has significant security concerns and runtime bugs that need to be addressed before merging
  • Score reflects critical security vulnerabilities in the sandboxing implementation (exec() with weak restrictions), function signature bugs that will cause runtime errors, inconsistent security validation between test and production paths, and async/blocking issues. The feature itself is well-designed with good UI/UX, but the execution has serious flaws.
  • Pay close attention to litellm/proxy/guardrails/guardrail_endpoints.py (security validation gaps), litellm/proxy/guardrails/guardrail_hooks/custom_code/custom_code_guardrail.py (exec() sandboxing), and ui/litellm-dashboard/src/components/guardrails/custom_code/CustomCodePlayground.tsx (function signature bug)

Important Files Changed

Filename Overview
litellm/proxy/guardrails/guardrail_endpoints.py New endpoint for testing custom code guardrails with security validations, includes regex-based forbidden pattern checking and timeout protection
litellm/proxy/guardrails/guardrail_hooks/custom_code/custom_code_guardrail.py Core custom code guardrail implementation using exec() with restricted global namespace, sandboxing through primitives-only environment
ui/litellm-dashboard/src/components/guardrails/custom_code/CustomCodePlayground.tsx React UI for testing custom code guardrails with test runner and primitives reference
ui/litellm-dashboard/src/components/guardrails/custom_code/CustomCodeModal.tsx Modal UI for creating custom code guardrails with code templates and inline testing

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as LiteLLM Dashboard
    participant API as Guardrail Endpoints
    participant Guardrail as CustomCodeGuardrail
    participant Sandbox as exec() Sandbox
    participant Primitives as Safe Primitives

    User->>UI: Create Custom Code Guardrail
    UI->>UI: Edit code in CustomCodeModal
    User->>UI: Click "Test"
    UI->>API: POST /guardrails/test_custom_code
    API->>API: Validate forbidden patterns (regex)
    API->>API: Set __builtins__ = {}
    API->>Sandbox: exec(custom_code, restricted_globals)
    Sandbox->>Sandbox: Compile apply_guardrail function
    API->>Sandbox: call apply_guardrail(inputs, request_data, input_type)
    Sandbox->>Primitives: Use regex_match(), block(), etc.
    Primitives-->>Sandbox: Return results
    Sandbox-->>API: Return action (allow/block/modify)
    API-->>UI: TestCustomCodeGuardrailResponse
    UI-->>User: Display test result

    User->>UI: Click "Save Guardrail"
    UI->>API: POST /guardrails
    API->>API: Store guardrail in database
    API->>Guardrail: Initialize CustomCodeGuardrail
    Guardrail->>Sandbox: Compile custom code on init
    API-->>UI: Success response

    Note over User,Primitives: Production Request Flow
    User->>API: POST /chat/completions (with guardrail)
    API->>Guardrail: apply_guardrail (pre_call)
    Guardrail->>Sandbox: Execute apply_guardrail function
    Sandbox->>Primitives: Call primitives
    Primitives-->>Sandbox: Results
    Sandbox-->>Guardrail: allow/block/modify
    alt Block
        Guardrail-->>API: Raise HTTPException
        API-->>User: 400 Error with reason
    else Allow/Modify
        Guardrail-->>API: Modified inputs
        API->>API: Continue to LLM
    end
Loading

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 files reviewed, 10 comments

Edit Code Review Agent Settings | Greptile

exec_globals = get_custom_code_primitives().copy()

# Remove access to builtins to prevent escape
exec_globals["__builtins__"] = {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setting __builtins__ to an empty dict does not provide complete sandbox protection - users can still access builtins through introspection like ().__class__.__bases__[0].__subclasses__() or other dunder methods. the regex-based forbidden pattern check on lines 1357-1406 is good first line of defense, but Python sandboxing via exec() is notoriously difficult to secure

Suggested change
exec_globals["__builtins__"] = {}
# Remove most builtins but keep safe ones needed by primitives
exec_globals["__builtins__"] = {
"__build_class__": __builtins__["__build_class__"],
"__name__": __builtins__["__name__"],
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: litellm/proxy/guardrails/guardrail_endpoints.py
Line: 1412:1412

Comment:
setting `__builtins__` to an empty dict does not provide complete sandbox protection - users can still access builtins through introspection like `().__class__.__bases__[0].__subclasses__()` or other dunder methods. the regex-based forbidden pattern check on lines 1357-1406 is good first line of defense, but Python sandboxing via `exec()` is notoriously difficult to secure

```suggestion
        # Remove most builtins but keep safe ones needed by primitives
        exec_globals["__builtins__"] = {
            "__build_class__": __builtins__["__build_class__"],
            "__name__": __builtins__["__name__"],
        }
```

How can I resolve this? If you propose a fix, please make it concise.

exec_globals = get_custom_code_primitives().copy()

# Execute the user code in the restricted environment
exec(compile(self.custom_code, "<guardrail>", "exec"), exec_globals)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using exec() with user-provided code is inherently risky. while you've restricted the globals namespace, Python sandboxing is very difficult - attackers can escape through object introspection (e.g., accessing __subclasses__, __bases__, etc.). the test endpoint adds regex-based pattern blocking, but this implementation in production doesn't have those checks when loading from config.yaml

consider using a more secure sandboxing approach like:

  • running code in a separate process with resource limits
  • using a proper sandboxing library like RestrictedPython
  • or using a language designed for safe user code execution (like Lua, Starlark, or CEL)
Prompt To Fix With AI
This is a comment left during a code review.
Path: litellm/proxy/guardrails/guardrail_hooks/custom_code/custom_code_guardrail.py
Line: 136:136

Comment:
using `exec()` with user-provided code is inherently risky. while you've restricted the globals namespace, Python sandboxing is very difficult - attackers can escape through object introspection (e.g., accessing `__subclasses__`, `__bases__`, etc.). the test endpoint adds regex-based pattern blocking, but this implementation in production doesn't have those checks when loading from config.yaml

consider using a more secure sandboxing approach like:
- running code in a separate process with resource limits
- using a proper sandboxing library like RestrictedPython
- or using a language designed for safe user code execution (like Lua, Starlark, or CEL)

How can I resolve this? If you propose a fix, please make it concise.

(r"\bsubprocess\.", "subprocess module access is not allowed"),
]

EXECUTION_TIMEOUT_SECONDS = 5

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the timeout is applied via ThreadPoolExecutor, but malicious code could still consume excessive CPU/memory within the 5-second window. threads also share memory space, so a memory exhaustion attack could impact the entire proxy process

consider adding resource limits (CPU, memory) using OS-level controls or running in a separate process

Prompt To Fix With AI
This is a comment left during a code review.
Path: litellm/proxy/guardrails/guardrail_endpoints.py
Line: 1395:1395

Comment:
the timeout is applied via `ThreadPoolExecutor`, but malicious code could still consume excessive CPU/memory within the 5-second window. threads also share memory space, so a memory exhaustion attack could impact the entire proxy process

consider adding resource limits (CPU, memory) using OS-level controls or running in a separate process

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +1357 to +1393
# Security validation patterns
FORBIDDEN_PATTERNS = [
# Import statements
(r"\bimport\s+", "import statements are not allowed"),
(r"\bfrom\s+\w+\s+import\b", "from...import statements are not allowed"),
(r"__import__\s*\(", "__import__() is not allowed"),
# Dangerous builtins
(r"\bexec\s*\(", "exec() is not allowed"),
(r"\beval\s*\(", "eval() is not allowed"),
(r"\bcompile\s*\(", "compile() is not allowed"),
(r"\bopen\s*\(", "open() is not allowed"),
(r"\bgetattr\s*\(", "getattr() is not allowed"),
(r"\bsetattr\s*\(", "setattr() is not allowed"),
(r"\bdelattr\s*\(", "delattr() is not allowed"),
(r"\bglobals\s*\(", "globals() is not allowed"),
(r"\blocals\s*\(", "locals() is not allowed"),
(r"\bvars\s*\(", "vars() is not allowed"),
(r"\bdir\s*\(", "dir() is not allowed"),
(r"\bbreakpoint\s*\(", "breakpoint() is not allowed"),
(r"\binput\s*\(", "input() is not allowed"),
# Dangerous dunder access
(r"__builtins__", "__builtins__ access is not allowed"),
(r"__globals__", "__globals__ access is not allowed"),
(r"__code__", "__code__ access is not allowed"),
(r"__subclasses__", "__subclasses__ access is not allowed"),
(r"__bases__", "__bases__ access is not allowed"),
(r"__mro__", "__mro__ access is not allowed"),
(r"__class__", "__class__ access is not allowed"),
(r"__dict__", "__dict__ access is not allowed"),
(r"__getattribute__", "__getattribute__ access is not allowed"),
(r"__reduce__", "__reduce__ access is not allowed"),
(r"__reduce_ex__", "__reduce_ex__ access is not allowed"),
# OS/system access
(r"\bos\.", "os module access is not allowed"),
(r"\bsys\.", "sys module access is not allowed"),
(r"\bsubprocess\.", "subprocess module access is not allowed"),
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security validation via regex patterns is good, but these patterns are only applied in the test endpoint. the production code path (when loading guardrails from config.yaml) in custom_code_guardrail.py:136 doesn't apply these checks

move this validation logic to a shared function that's called both by the test endpoint and during guardrail initialization

Prompt To Fix With AI
This is a comment left during a code review.
Path: litellm/proxy/guardrails/guardrail_endpoints.py
Line: 1357:1393

Comment:
security validation via regex patterns is good, but these patterns are only applied in the test endpoint. the production code path (when loading guardrails from config.yaml) in `custom_code_guardrail.py:136` doesn't apply these checks

move this validation logic to a shared function that's called both by the test endpoint and during guardrail initialization

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +255 to +263
const response = await testCustomCodeGuardrail(accessToken, {
custom_code: code,
test_input: parsedInput,
input_type: mode as "request" | "response",
request_data: {
model: "test-model",
metadata: {},
},
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the test endpoint is called with mode as input_type, but mode can be "pre_call", "post_call", "during_call", or "logging_only" while input_type expects "request" or "response"

Suggested change
const response = await testCustomCodeGuardrail(accessToken, {
custom_code: code,
test_input: parsedInput,
input_type: mode as "request" | "response",
request_data: {
model: "test-model",
metadata: {},
},
});
const response = await testCustomCodeGuardrail(accessToken, {
custom_code: code,
test_input: parsedInput,
input_type: mode === "post_call" ? "response" : "request",
request_data: {
model: "test-model",
metadata: {},
},
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/litellm-dashboard/src/components/guardrails/custom_code/CustomCodeModal.tsx
Line: 255:263

Comment:
the test endpoint is called with `mode` as `input_type`, but `mode` can be "pre_call", "post_call", "during_call", or "logging_only" while `input_type` expects "request" or "response"

```suggestion
      const response = await testCustomCodeGuardrail(accessToken, {
        custom_code: code,
        test_input: parsedInput,
        input_type: mode === "post_call" ? "response" : "request",
        request_data: {
          model: "test-model",
          metadata: {},
        },
      });
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +7645 to +7647
export const testCustomCodeGuardrail = async (
accessToken: string,
request: TestCustomCodeGuardrailRequest

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the function signature doesn't match the actual usage in CustomCodeModal.tsx:255 and CustomCodePlayground.tsx:113. in those files, the function is called with 5 separate parameters (accessToken, customCode, parsedInputs, parsedRequestData, inputType) but this signature expects 2 parameters (accessToken, request)

check which call sites are using the old signature and update them

Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/litellm-dashboard/src/components/networking.tsx
Line: 7645:7647

Comment:
the function signature doesn't match the actual usage in `CustomCodeModal.tsx:255` and `CustomCodePlayground.tsx:113`. in those files, the function is called with 5 separate parameters `(accessToken, customCode, parsedInputs, parsedRequestData, inputType)` but this signature expects 2 parameters `(accessToken, request)`

check which call sites are using the old signature and update them

How can I resolve this? If you propose a fix, please make it concise.

safe_request_data = self._prepare_safe_request_data(request_data)

# Execute the custom function
result = self._compiled_function(inputs, safe_request_data, input_type)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the custom function is called synchronously but apply_guardrail is an async method. if the custom code performs any long-running operations (loops, regex on large strings), it will block the async event loop and degrade proxy performance

consider running the custom code execution in a thread pool executor (like the test endpoint does) to avoid blocking

Prompt To Fix With AI
This is a comment left during a code review.
Path: litellm/proxy/guardrails/guardrail_hooks/custom_code/custom_code_guardrail.py
Line: 205:205

Comment:
the custom function is called synchronously but `apply_guardrail` is an async method. if the custom code performs any long-running operations (loops, regex on large strings), it will block the async event loop and degrade proxy performance

consider running the custom code execution in a thread pool executor (like the test endpoint does) to avoid blocking

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +576 to +587
# Python builtins (safe subset)
"len": len,
"str": str,
"int": int,
"float": float,
"bool": bool,
"list": list,
"dict": dict,
"True": True,
"False": False,
"None": None,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exposing Python builtins like list, dict, str, int, float, and bool is necessary for basic operations, but these objects have dunder methods that could potentially be used for sandbox escape (e.g., ().__class__.__bases__[0].__subclasses__())

this is a known limitation of Python sandboxing - consider documenting this security caveat in the code comments and documentation

Prompt To Fix With AI
This is a comment left during a code review.
Path: litellm/proxy/guardrails/guardrail_hooks/custom_code/primitives.py
Line: 576:587

Comment:
exposing Python builtins like `list`, `dict`, `str`, `int`, `float`, and `bool` is necessary for basic operations, but these objects have dunder methods that could potentially be used for sandbox escape (e.g., `().__class__.__bases__[0].__subclasses__()`)

this is a known limitation of Python sandboxing - consider documenting this security caveat in the code comments and documentation

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +113 to +119
const result = await testCustomCodeGuardrail(
accessToken,
customCode,
parsedInputs,
parsedRequestData,
inputType
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

incorrect function signature - testCustomCodeGuardrail expects 2 parameters (accessToken, request) but this is calling it with 5 separate parameters

Suggested change
const result = await testCustomCodeGuardrail(
accessToken,
customCode,
parsedInputs,
parsedRequestData,
inputType
);
const result = await testCustomCodeGuardrail(
accessToken,
{
custom_code: customCode,
test_input: parsedInputs,
request_data: parsedRequestData,
input_type: inputType
}
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/litellm-dashboard/src/components/guardrails/custom_code/CustomCodePlayground.tsx
Line: 113:119

Comment:
incorrect function signature - `testCustomCodeGuardrail` expects 2 parameters `(accessToken, request)` but this is calling it with 5 separate parameters

```suggestion
      const result = await testCustomCodeGuardrail(
        accessToken,
        {
          custom_code: customCode,
          test_input: parsedInputs,
          request_data: parsedRequestData,
          input_type: inputType
        }
      );
```

How can I resolve this? If you propose a fix, please make it concise.

"'output' runs on model → user traffic, and 'both' applies to both."
),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed presidio_filter_scope field - this appears to be an unrelated change that removes functionality. was this intentional? if users were relying on this field to control whether Presidio runs on input/output/both, this is a breaking change

verify this removal is intentional and not an accidental deletion during development

Prompt To Fix With AI
This is a comment left during a code review.
Path: litellm/types/guardrails.py
Line: 300:300

Comment:
removed `presidio_filter_scope` field - this appears to be an unrelated change that removes functionality. was this intentional? if users were relying on this field to control whether Presidio runs on input/output/both, this is a breaking change

verify this removal is intentional and not an accidental deletion during development

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PresidioPresidioConfigModelUserInterface already has this param

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confirmed - PresidioPresidioConfigModelUserInterface (litellm/types/guardrails.py:281-285) already defines this field with the same signature. removing the duplicate definition from the child class is correct - it now properly inherits the field from the parent.

@ghost
ghost changed the base branch from main to litellm_oss_staging_02_03_2026 February 4, 2026 03:56
@ghost
ghost merged commit 7056d99 into litellm_oss_staging_02_03_2026 Feb 4, 2026
3 of 4 checks passed
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
* feat(guardrails/): allow custom code execution for guardrails

first step in allowing teams to submit custom code for guardrails

* feat: custom_code_guardrail.md

support passing custom code for guardrails

* feat: initial commit adding ui for custom code guardrails

allows users to write guardrails based on custom code

* feat: expose new test custom code guardrail endpoint

allows ui testing playground to sanity check if guardrail is working as expected

* fix: fix linting errors

* fix: fix max recursion check

* fix: fix linting error
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants