-
-
Notifications
You must be signed in to change notification settings - Fork 11.6k
fix(files): constrain cloud storage file paths (VERIA-45, VERIA-59) #27019
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
yuneng-berri
merged 7 commits into
BerriAI:litellm_internal_staging
from
stuxf:codex/cloud-storage-file-guard
May 4, 2026
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
ba11881
fix cloud storage file guards
stuxf f53e8d6
harden bedrock file bucket validation
stuxf 2776e01
harden cloud file compatibility path
stuxf e7c23fc
harden cloud file compatibility path
stuxf bb8f6fd
thread trusted params through vertex file content
stuxf 8791f63
trust only server legacy file flag
stuxf 7c94149
Merge remote-tracking branch 'origin/litellm_internal_staging' into c…
stuxf File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| import posixpath | ||
| import re | ||
| from types import MappingProxyType | ||
| from typing import Any, Mapping, Optional, Sequence, Tuple, cast | ||
| from urllib.parse import quote, unquote | ||
|
|
||
| from litellm._uuid import uuid | ||
|
|
||
| VERTEX_AI_MANAGED_GCS_PREFIX = "litellm-vertex-files/" | ||
| BEDROCK_MANAGED_S3_BATCH_PREFIX = "litellm-bedrock-files-" | ||
| BEDROCK_MANAGED_S3_UPLOAD_PREFIX = "litellm-bedrock-files/" | ||
| BEDROCK_MANAGED_S3_OUTPUT_PREFIX = "litellm-batch-outputs/" | ||
| BEDROCK_MANAGED_S3_PREFIXES = ( | ||
| BEDROCK_MANAGED_S3_BATCH_PREFIX, | ||
| BEDROCK_MANAGED_S3_UPLOAD_PREFIX, | ||
| BEDROCK_MANAGED_S3_OUTPUT_PREFIX, | ||
| ) | ||
| _MAPPING_PROXY_TYPE: type = type(MappingProxyType({})) | ||
|
|
||
| _SAFE_OBJECT_COMPONENT_PATTERN = re.compile(r"[^A-Za-z0-9._-]+") | ||
|
|
||
|
|
||
| def sanitize_cloud_object_component( | ||
| value: Optional[str], fallback: str = "file" | ||
| ) -> str: | ||
| if not isinstance(value, str): | ||
| return fallback | ||
|
|
||
| component = posixpath.basename(value.replace("\\", "/")).strip() | ||
| if component in {"", ".", ".."}: | ||
| return fallback | ||
|
|
||
| component = "".join( | ||
| "_" if ord(char) < 32 or ord(char) == 127 else char for char in component | ||
| ) | ||
| component = _SAFE_OBJECT_COMPONENT_PATTERN.sub("_", component) | ||
| component = component.strip("._") | ||
| if not component: | ||
| return fallback | ||
| return component[:255] | ||
|
|
||
|
|
||
| def sanitize_cloud_object_path(value: Optional[str], fallback: str = "file") -> str: | ||
| if not isinstance(value, str): | ||
| return fallback | ||
|
|
||
| segments = [] | ||
| for segment in value.replace("\\", "/").split("/"): | ||
| sanitized_segment = sanitize_cloud_object_component(segment, fallback="") | ||
| if sanitized_segment: | ||
| segments.append(sanitized_segment) | ||
|
|
||
| if not segments: | ||
| return fallback | ||
| return "/".join(segments) | ||
|
|
||
|
|
||
| def build_managed_cloud_object_name( | ||
| prefix: str, filename: Optional[str], fallback_filename: str = "file" | ||
| ) -> str: | ||
| safe_filename = sanitize_cloud_object_component( | ||
| filename, fallback=fallback_filename | ||
| ) | ||
| return f"{prefix}{uuid.uuid4().hex}-{safe_filename}" | ||
|
|
||
|
|
||
| def _validate_cloud_object_path(object_name: str) -> None: | ||
| if not object_name: | ||
| raise ValueError("Cloud storage object name is required") | ||
| if object_name.startswith("/"): | ||
| raise ValueError("Cloud storage object name must be relative") | ||
| if any(ord(char) < 32 or ord(char) == 127 for char in object_name): | ||
| raise ValueError("Cloud storage object name contains control characters") | ||
| segments = object_name.split("/") | ||
| if any(segment in {".", ".."} for segment in segments): | ||
| raise ValueError("Cloud storage object name contains an invalid path segment") | ||
| if "" in segments[:-1]: | ||
| raise ValueError("Cloud storage object name contains an invalid path segment") | ||
|
|
||
|
|
||
| def split_configured_cloud_bucket_name(bucket_name: str) -> Tuple[str, str]: | ||
| if not isinstance(bucket_name, str) or not bucket_name.strip(): | ||
| raise ValueError("Cloud storage bucket name is required") | ||
|
|
||
| bucket_name = bucket_name.strip() | ||
| if "://" in bucket_name or "?" in bucket_name or "#" in bucket_name: | ||
| raise ValueError( | ||
| "Cloud storage bucket name must not include a URI scheme or query" | ||
| ) | ||
| if any(ord(char) < 32 or ord(char) == 127 for char in bucket_name): | ||
| raise ValueError("Cloud storage bucket name contains control characters") | ||
|
|
||
| bucket, _, prefix = bucket_name.partition("/") | ||
| if not bucket: | ||
| raise ValueError("Cloud storage bucket name is required") | ||
| if "\\" in bucket: | ||
| raise ValueError("Cloud storage bucket name contains an invalid separator") | ||
|
|
||
| prefix = prefix.strip("/") | ||
| if prefix: | ||
| _validate_cloud_object_path(prefix) | ||
|
|
||
| return bucket, prefix | ||
|
|
||
|
|
||
| def encode_gcs_object_name_for_url(object_name: str) -> str: | ||
| return quote(unquote(object_name), safe="") | ||
|
|
||
|
|
||
| def encode_s3_object_key_for_url(object_key: str) -> str: | ||
| return quote(unquote(object_key), safe="/") | ||
|
|
||
|
|
||
| def should_allow_legacy_cloud_file_ids( | ||
| litellm_params: Optional[Mapping[str, Any]] = None, | ||
| ) -> bool: | ||
| value = None | ||
| if isinstance(litellm_params, Mapping): | ||
| trusted_model_credentials = litellm_params.get( | ||
| "_litellm_internal_model_credentials" | ||
| ) | ||
| if isinstance(trusted_model_credentials, _MAPPING_PROXY_TYPE): | ||
| value = cast(Mapping[str, Any], trusted_model_credentials).get( | ||
| "allow_legacy_cloud_file_ids" | ||
| ) | ||
|
|
||
| if isinstance(value, bool): | ||
| return value | ||
| if isinstance(value, str): | ||
| return value.strip().lower() in {"1", "true", "yes", "on"} | ||
| return False | ||
|
|
||
|
|
||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
| def validate_managed_cloud_file_id( | ||
| file_id: str, | ||
| scheme: str, | ||
| configured_bucket_name: str, | ||
| allowed_object_prefixes: Sequence[str], | ||
| allow_legacy_cloud_file_ids: bool = False, | ||
| ) -> Tuple[str, str]: | ||
| decoded_file_id = unquote(file_id) | ||
| if not decoded_file_id.startswith(scheme): | ||
| raise ValueError(f"file_id must be a {scheme} URI") | ||
|
|
||
| full_path = decoded_file_id[len(scheme) :] | ||
| if "/" not in full_path: | ||
| raise ValueError("file_id must include a cloud storage object name") | ||
|
|
||
| bucket_name, object_name = full_path.split("/", 1) | ||
| configured_bucket, configured_prefix = split_configured_cloud_bucket_name( | ||
| configured_bucket_name | ||
| ) | ||
| if bucket_name != configured_bucket: | ||
| raise ValueError("file_id bucket does not match the configured storage bucket") | ||
|
|
||
| _validate_cloud_object_path(object_name) | ||
| allowed_prefixes = tuple(allowed_object_prefixes) | ||
| if configured_prefix: | ||
| allowed_prefixes = tuple( | ||
| f"{configured_prefix.rstrip('/')}/{prefix}" for prefix in allowed_prefixes | ||
| ) | ||
|
|
||
| if object_name.startswith(allowed_prefixes): | ||
| return bucket_name, object_name | ||
|
|
||
| if allow_legacy_cloud_file_ids: | ||
| if configured_prefix and not object_name.startswith( | ||
| f"{configured_prefix.rstrip('/')}/" | ||
| ): | ||
| raise ValueError( | ||
| "file_id object does not match the configured storage prefix" | ||
| ) | ||
| return bucket_name, object_name | ||
|
|
||
| raise ValueError("file_id must reference a LiteLLM-managed storage object") | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gcs_log_idbehavior change without a feature flagPreviously
gcs_log_idin metadata was used as the exact GCS object path. This PR changes it to a sanitized hint incorporated into a new randomized path ({date}/custom-{uuid}-{safe_hint}). Any deployment that reads GCS log objects by a predictablegcs_log_id-derived path (e.g., external tooling, dashboards, or audit pipelines that construct GCS paths from knowngcs_log_idvalues) will silently stop finding those objects after this change.Per the project's backwards-compatibility rule, breaking behavior changes should be gated behind a server-side flag (e.g.
litellm.enforce_safe_gcs_log_pathsor an env var), so existing deployments can opt in on their own schedule. The analogous file retrieval path hasallow_legacy_cloud_file_idsfor exactly this reason.Rule Used: What: avoid backwards-incompatible changes without... (source)