[Security] Validate cache salts before they reach LMCache (#51444)
Signed-off-by: Clinton Thomas <1033162+KernelClint@users.noreply.github.com> Co-authored-by: Lucas Bourtoule <35483370+dhalf@users.noreply.github.com> Co-authored-by: Cyrus Leung <tlleungac@connect.ust.hk>
This commit is contained in:
co-authored by
Lucas Bourtoule
Cyrus Leung
parent
4ee2595512
commit
e962733e08
@@ -11,6 +11,7 @@ import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
|
||||
from vllm.exceptions import VLLMValidationError
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -28,7 +29,7 @@ def test_chat_completion_request_rejects_non_object_body(payload):
|
||||
|
||||
|
||||
def test_chat_completion_request_cache_salt_still_validated_on_dict():
|
||||
with pytest.raises(ValidationError, match="cache_salt"):
|
||||
with pytest.raises(VLLMValidationError, match="cache_salt"):
|
||||
ChatCompletionRequest.model_validate(
|
||||
{
|
||||
"model": "qwen",
|
||||
|
||||
@@ -13,11 +13,13 @@ request models whose before-validators were missing the same guard.
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
|
||||
from vllm.entrypoints.openai.completion.protocol import CompletionRequest
|
||||
from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
|
||||
from vllm.entrypoints.pooling.classify.protocol import ClassificationChatRequest
|
||||
from vllm.entrypoints.pooling.embed.protocol import EmbeddingChatRequest
|
||||
from vllm.entrypoints.pooling.pooling.protocol import PoolingChatRequest
|
||||
from vllm.entrypoints.scale_out.token_in_token_out.protocol import GenerateRequest
|
||||
from vllm.entrypoints.serve.tokenize.protocol import TokenizeChatRequest
|
||||
from vllm.entrypoints.speech_to_text.transcription.protocol import TranscriptionRequest
|
||||
from vllm.entrypoints.speech_to_text.translation.protocol import TranslationRequest
|
||||
@@ -69,3 +71,33 @@ def test_tokenize_chat_request_still_validates_dict_bodies():
|
||||
"add_generation_prompt": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
CACHE_SALT_MODELS = [
|
||||
(CompletionRequest, {"prompt": "hello"}),
|
||||
(ChatCompletionRequest, {"messages": [{"role": "user", "content": "hello"}]}),
|
||||
(ResponsesRequest, {"input": "hello"}),
|
||||
(GenerateRequest, {"token_ids": [1], "sampling_params": {}}),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("request_model", "payload"),
|
||||
CACHE_SALT_MODELS,
|
||||
ids=[m.__name__ for m, _ in CACHE_SALT_MODELS],
|
||||
)
|
||||
def test_request_models_reject_forbidden_cache_salt(request_model, payload):
|
||||
with pytest.raises(VLLMValidationError, match="cache_salt"):
|
||||
request_model.model_validate({**payload, "cache_salt": "/"})
|
||||
|
||||
|
||||
def test_request_models_reject_overlong_cache_salt():
|
||||
with pytest.raises(VLLMValidationError, match="cache_salt"):
|
||||
CompletionRequest.model_validate({"prompt": "hello", "cache_salt": "a" * 129})
|
||||
|
||||
|
||||
def test_request_models_accept_safe_cache_salt():
|
||||
request = CompletionRequest.model_validate(
|
||||
{"prompt": "hello", "cache_salt": "safe-cache_salt-123"}
|
||||
)
|
||||
assert request.cache_salt == "safe-cache_salt-123"
|
||||
|
||||
@@ -29,6 +29,28 @@ StopParam: TypeAlias = (
|
||||
str | Annotated[list[str], Field(max_length=envs.VLLM_MAX_STOP_STRINGS)] | None
|
||||
)
|
||||
|
||||
_CACHE_SALT_FORBIDDEN_CHARS = frozenset("@/\\\x00")
|
||||
_MAX_CACHE_SALT_LENGTH = 128
|
||||
|
||||
|
||||
def validate_cache_salt(cache_salt: object) -> None:
|
||||
"""Validate cache salts before they reach downstream cache backends."""
|
||||
if cache_salt is None:
|
||||
return
|
||||
if not isinstance(cache_salt, str) or not cache_salt:
|
||||
raise VLLMValidationError(
|
||||
"Parameter 'cache_salt' must be a non-empty string if provided.",
|
||||
parameter="cache_salt",
|
||||
)
|
||||
if len(cache_salt) > _MAX_CACHE_SALT_LENGTH or any(
|
||||
char in _CACHE_SALT_FORBIDDEN_CHARS for char in cache_salt
|
||||
):
|
||||
raise VLLMValidationError(
|
||||
"Parameter 'cache_salt' must be at most 128 characters and must "
|
||||
"not contain '@', '/', '\\\\', or NUL.",
|
||||
parameter="cache_salt",
|
||||
)
|
||||
|
||||
|
||||
class SpeculativeDecodingMetrics(OpenAIBaseModel):
|
||||
"""Per-request speculative-decoding acceptance metrics.
|
||||
|
||||
@@ -33,6 +33,7 @@ from vllm.entrypoints.generate.base.protocol import (
|
||||
StreamOptions,
|
||||
ToolCall,
|
||||
structured_outputs_from_response_format,
|
||||
validate_cache_salt,
|
||||
validate_structural_tag_response_format,
|
||||
validate_structured_outputs_structural_tag,
|
||||
)
|
||||
@@ -519,6 +520,14 @@ class ChatCompletionRequest(OpenAIBaseModel):
|
||||
|
||||
# --8<-- [end:chat-completion-extra-params]
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def check_cache_salt_support(cls, data: Any) -> Any:
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
validate_cache_salt(data.get("cache_salt"))
|
||||
return data
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _normalize_messages_before(cls, data: Any) -> Any:
|
||||
|
||||
@@ -16,6 +16,7 @@ from vllm.entrypoints.generate.base.protocol import (
|
||||
StopParam,
|
||||
StreamOptions,
|
||||
structured_outputs_from_response_format,
|
||||
validate_cache_salt,
|
||||
validate_structural_tag_response_format,
|
||||
validate_structured_outputs_structural_tag,
|
||||
)
|
||||
@@ -402,6 +403,14 @@ class CompletionRequest(OpenAIBaseModel):
|
||||
routed_experts_prompt_start=self.routed_experts_prompt_start,
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def check_cache_salt_support(cls, data: Any) -> Any:
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
validate_cache_salt(data.get("cache_salt"))
|
||||
return data
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def normalize_null_max_tokens(cls, data):
|
||||
|
||||
@@ -60,7 +60,7 @@ from vllm.entrypoints.chat_utils import (
|
||||
ChatCompletionMessageParam,
|
||||
ChatTemplateContentFormatOption,
|
||||
)
|
||||
from vllm.entrypoints.generate.base.protocol import StopParam
|
||||
from vllm.entrypoints.generate.base.protocol import StopParam, validate_cache_salt
|
||||
from vllm.entrypoints.serve.engine.protocol import OpenAIBaseModel
|
||||
from vllm.exceptions import VLLMValidationError
|
||||
from vllm.logger import init_logger
|
||||
@@ -466,6 +466,14 @@ class ResponsesRequest(OpenAIBaseModel):
|
||||
and "message.output_text.logprobs" in self.include
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def check_cache_salt_support(cls, data: Any) -> Any:
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
validate_cache_salt(data.get("cache_salt"))
|
||||
return data
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def validate_background(cls, data):
|
||||
|
||||
@@ -11,6 +11,7 @@ from vllm.entrypoints.chat_utils import (
|
||||
ChatCompletionMessageParam,
|
||||
ChatTemplateContentFormatOption,
|
||||
)
|
||||
from vllm.entrypoints.generate.base.protocol import validate_cache_salt
|
||||
from vllm.entrypoints.serve.engine.protocol import OpenAIBaseModel
|
||||
from vllm.exceptions import VLLMValidationError
|
||||
from vllm.renderers import ChatParams, TokenizeParams, merge_kwargs
|
||||
@@ -95,6 +96,15 @@ class PoolingBasicRequestMixin(OpenAIBaseModel):
|
||||
)
|
||||
# --8<-- [end:pooling-common-extra-params]
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def check_cache_salt_support(cls, data: Any) -> Any:
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
validate_cache_salt(data.get("cache_salt"))
|
||||
return data
|
||||
|
||||
def _build_pooling_tok_params(
|
||||
self,
|
||||
model_config: ModelConfig,
|
||||
|
||||
@@ -11,7 +11,7 @@ from pydantic import (
|
||||
)
|
||||
|
||||
from vllm.config import ModelConfig
|
||||
from vllm.entrypoints.generate.base.protocol import StreamOptions
|
||||
from vllm.entrypoints.generate.base.protocol import StreamOptions, validate_cache_salt
|
||||
from vllm.entrypoints.openai.chat_completion.protocol import (
|
||||
ChatCompletionLogProbs,
|
||||
ChatCompletionRequest,
|
||||
@@ -177,6 +177,13 @@ class GenerateRequest(BaseModel):
|
||||
instance._sampling_params_provided_keys = provided
|
||||
return instance
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _validate_cache_salt(cls, data: Any) -> Any:
|
||||
if isinstance(data, dict):
|
||||
validate_cache_salt(data.get("cache_salt"))
|
||||
return data
|
||||
|
||||
def is_sampling_param_provided(self, name: str) -> bool:
|
||||
"""Whether the caller explicitly set ``sampling_params.<name>``.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user