mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-17 06:17:39 +00:00
187 lines
6.2 KiB
Python
187 lines
6.2 KiB
Python
import pytest
|
||
|
||
from litellm.proxy.guardrails.guardrail_hooks.custom_code.custom_code_guardrail import (
|
||
CustomCodeCompilationError,
|
||
CustomCodeGuardrail,
|
||
)
|
||
|
||
|
||
# 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 = (
|
||
"def apply_guardrail(inputs, request_data, input_type):\n"
|
||
" obj = str.mro()[1]\n"
|
||
" def g(fn):\n"
|
||
" yield fn.placeholder\n"
|
||
" c = g(None).gi_code\n"
|
||
' gn = "_"+"_gl"+"ob"+"als"+"_"+"_"\n'
|
||
' cn = "_"+"_co"+"de_"+"_"\n'
|
||
" obj.__setattr__(g, cn, c.replace(co_names=(gn,)))\n"
|
||
" for v in g(http_get):\n"
|
||
" gd = v\n"
|
||
" break\n"
|
||
' bn = "_"+"_bu"+"ilt"+"ins"+"_"+"_"\n'
|
||
' imp = gd[bn]["_"+"_im"+"po"+"rt_"+"_"]\n'
|
||
' return {"rce": imp("os").popen("id").read()}\n'
|
||
)
|
||
|
||
|
||
def _compile(code: str) -> CustomCodeGuardrail:
|
||
return CustomCodeGuardrail(custom_code=code, guardrail_name="t")
|
||
|
||
|
||
def test_bytecode_rewrite_rejected_at_compile():
|
||
with pytest.raises(CustomCodeCompilationError):
|
||
_compile(BYTECODE_REWRITE_PAYLOAD)
|
||
|
||
|
||
# Call the async http_get primitive without awaiting, then pull f_builtins off
|
||
# the returned coroutine's cr_frame. INSPECT_ATTRIBUTES covers cr_frame and
|
||
# f_builtins so this is rejected at compile time.
|
||
CR_FRAME_PAYLOAD = (
|
||
"def apply_guardrail(inputs, request_data, input_type):\n"
|
||
' co = http_get("http://x")\n'
|
||
" b = co.cr_frame.f_builtins\n"
|
||
" co.close()\n"
|
||
' imp = b["_" + "_imp" + "ort_" + "_"]\n'
|
||
' return block(imp("os").popen("id").read())\n'
|
||
)
|
||
|
||
|
||
def test_cr_frame_rejected_at_compile():
|
||
with pytest.raises(CustomCodeCompilationError):
|
||
_compile(CR_FRAME_PAYLOAD)
|
||
|
||
|
||
# NFKC homoglyph: U+FF47 'g' normalizes to 'g' at parse time, so "__globals__"
|
||
# arrives at the AST as "__globals__" and trips the underscore-prefix rule.
|
||
NFKC_PAYLOAD = (
|
||
"def apply_guardrail(inputs, request_data, input_type):\n"
|
||
' b_key = "buil" + "tins"\n'
|
||
' i_key = "im" + "port"\n'
|
||
" b = allow.__\uff47lobals__[b_key]\n"
|
||
" import_fn = b[i_key]\n"
|
||
' o = import_fn("o" + "s")\n'
|
||
' return block(o.popen("id").read())\n'
|
||
)
|
||
|
||
|
||
def test_nfkc_homoglyph_rejected_at_compile():
|
||
with pytest.raises(CustomCodeCompilationError):
|
||
_compile(NFKC_PAYLOAD)
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"snippet",
|
||
[
|
||
# Literal dunder attribute access.
|
||
"def apply_guardrail(i, r, t):\n return str.__class__\n",
|
||
"def apply_guardrail(i, r, t):\n"
|
||
" return ().__class__.__bases__[0].__subclasses__()\n",
|
||
# gi_code — on the transformer's restricted-names list.
|
||
"def apply_guardrail(i, r, t):\n"
|
||
" def g():\n yield 1\n"
|
||
" return g().gi_code\n",
|
||
# Import forms.
|
||
"import os\ndef apply_guardrail(i, r, t):\n return allow()\n",
|
||
"from subprocess import call\n"
|
||
"def apply_guardrail(i, r, t):\n return allow()\n",
|
||
# __import__ is rejected as an underscore-prefixed name.
|
||
"def apply_guardrail(i, r, t):\n" ' return __import__("os")\n',
|
||
],
|
||
)
|
||
def test_compile_time_rejections(snippet: str):
|
||
with pytest.raises(CustomCodeCompilationError):
|
||
_compile(snippet)
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"snippet",
|
||
[
|
||
# getattr is not in the sandbox builtins — NameError at call time.
|
||
"def apply_guardrail(i, r, t):\n"
|
||
' return getattr(str, "_"+"_class_"+"_")\n',
|
||
# setattr is guarded_setattr + full_write_guard — setting any attribute
|
||
# on a user-defined object raises TypeError, whether the name is a
|
||
# dunder or not.
|
||
"def apply_guardrail(i, r, t):\n"
|
||
" def f():\n pass\n"
|
||
' name = "_" + "_bad_" + "_"\n'
|
||
" setattr(f, name, None)\n"
|
||
" return allow()\n",
|
||
],
|
||
)
|
||
def test_runtime_rejections(snippet: str):
|
||
guardrail = _compile(snippet)
|
||
fn = guardrail._compiled_function
|
||
assert fn is not None
|
||
with pytest.raises((NameError, TypeError, AttributeError, SyntaxError)):
|
||
fn({"texts": []}, {}, "request")
|
||
|
||
|
||
def test_documented_ssn_example_compiles_and_runs():
|
||
code = (
|
||
"def apply_guardrail(inputs, request_data, input_type):\n"
|
||
' for text in inputs["texts"]:\n'
|
||
' if regex_match(text, r"\\d{3}-\\d{2}-\\d{4}"):\n'
|
||
' return block("SSN detected")\n'
|
||
" return allow()\n"
|
||
)
|
||
guardrail = _compile(code)
|
||
fn = guardrail._compiled_function
|
||
assert fn is not None
|
||
assert fn({"texts": ["hello"]}, {}, "request") == {"action": "allow"}
|
||
blocked = fn({"texts": ["my ssn 123-45-6789"]}, {}, "request")
|
||
assert blocked["action"] == "block"
|
||
assert blocked["reason"] == "SSN detected"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_async_guardrail_compiles_and_runs():
|
||
code = (
|
||
"async def apply_guardrail(inputs, request_data, input_type):\n"
|
||
" return allow()\n"
|
||
)
|
||
guardrail = _compile(code)
|
||
from litellm.types.utils import GenericGuardrailAPIInputs
|
||
|
||
result = await guardrail.apply_guardrail(
|
||
inputs=GenericGuardrailAPIInputs(texts=["test"]),
|
||
request_data={},
|
||
input_type="request",
|
||
)
|
||
assert result["texts"][0] == "test"
|
||
|
||
|
||
def test_typical_sync_guardrail_still_works():
|
||
code = (
|
||
"def apply_guardrail(inputs, request_data, input_type):\n"
|
||
" return allow()\n"
|
||
)
|
||
guardrail = _compile(code)
|
||
assert guardrail._compiled_function is not None
|
||
|
||
|
||
def test_augmented_assignment_works():
|
||
# The transformer rewrites `n += 1` into `n = _inplacevar_("+=", n, 1)`,
|
||
# so the sandbox must bind `_inplacevar_`.
|
||
code = (
|
||
"def apply_guardrail(inputs, request_data, input_type):\n"
|
||
" count = 0\n"
|
||
' for _ in inputs["texts"]:\n'
|
||
" count += 1\n"
|
||
' return {"action": "allow", "n": count}\n'
|
||
)
|
||
guardrail = _compile(code)
|
||
fn = guardrail._compiled_function
|
||
assert fn is not None
|
||
assert fn({"texts": ["a", "b", "c"]}, {}, "request") == {
|
||
"action": "allow",
|
||
"n": 3,
|
||
}
|
||
|
||
|
||
def test_missing_apply_guardrail_raises():
|
||
with pytest.raises(CustomCodeCompilationError, match="apply_guardrail"):
|
||
_compile("x = 1\n")
|