From 0ace1c44332743e60629536b2679ceda6483fcf4 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 19 May 2026 06:39:34 +0000 Subject: [PATCH] fix(oci): validate oci_region before URL interpolation to prevent SSRF Anchor oci_region to ^[a-z][a-z0-9-]{0,30}[a-z0-9]$ inside get_oci_base_url so user-supplied regions that would redirect the signed request to an attacker-controlled host (e.g. 'evil.com/#') fail with HTTP 400 before the URL or signature is built. Empty string still falls back to the us-ashburn-1 default, so existing callers are unaffected. --- litellm/llms/oci/common_utils.py | 12 ++++++ .../llms/oci/test_oci_common_utils.py | 42 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/litellm/llms/oci/common_utils.py b/litellm/llms/oci/common_utils.py index 9e60c24c32a..50fcf6f4194 100644 --- a/litellm/llms/oci/common_utils.py +++ b/litellm/llms/oci/common_utils.py @@ -3,6 +3,7 @@ import datetime import hashlib import json import os +import re from dataclasses import dataclass from typing import Any, Dict, Optional, Protocol, Tuple from urllib.parse import urlparse @@ -185,12 +186,23 @@ def resolve_oci_credentials(optional_params: dict) -> dict: } +_OCI_REGION_RE = re.compile(r"^[a-z][a-z0-9-]{0,30}[a-z0-9]$") + + def get_oci_base_url(optional_params: dict, api_base: Optional[str] = None) -> str: """Return the OCI inference base URL, respecting any explicit api_base override.""" if api_base: return api_base.rstrip("/") creds = resolve_oci_credentials(optional_params) region = creds["oci_region"] + if not isinstance(region, str) or not _OCI_REGION_RE.match(region): + raise OCIError( + status_code=400, + message=( + f"Invalid OCI region {region!r}: must match " + "^[a-z][a-z0-9-]{0,30}[a-z0-9]$ (e.g. 'us-ashburn-1')." + ), + ) return f"https://inference.generativeai.{region}.oci.oraclecloud.com" diff --git a/tests/test_litellm/llms/oci/test_oci_common_utils.py b/tests/test_litellm/llms/oci/test_oci_common_utils.py index bb3171d8fbc..90fea6fe2aa 100644 --- a/tests/test_litellm/llms/oci/test_oci_common_utils.py +++ b/tests/test_litellm/llms/oci/test_oci_common_utils.py @@ -157,6 +157,48 @@ def test_get_oci_base_url_from_region(): assert url == "https://inference.generativeai.eu-frankfurt-1.oci.oraclecloud.com" +@pytest.mark.parametrize( + "region", + [ + "evil.com/#", + "evil.com", + "us-ashburn-1/../attacker", + "ATTACKER", + "-leading-hyphen", + "trailing-hyphen-", + "a", + "a" * 33, + "us ashburn 1", + "us_ashburn_1", + ], +) +def test_get_oci_base_url_rejects_unsafe_region(region): + with pytest.raises(OCIError, match="Invalid OCI region"): + get_oci_base_url({"oci_region": region}) + + +def test_get_oci_base_url_empty_region_falls_back_to_default(monkeypatch): + monkeypatch.delenv("OCI_REGION", raising=False) + url = get_oci_base_url({"oci_region": ""}) + assert url == "https://inference.generativeai.us-ashburn-1.oci.oraclecloud.com" + + +@pytest.mark.parametrize( + "region", + [ + "us-ashburn-1", + "eu-frankfurt-1", + "ap-tokyo-1", + "us-chicago-1", + "us-phoenix-1", + "ap", + ], +) +def test_get_oci_base_url_accepts_valid_region(region): + url = get_oci_base_url({"oci_region": region}) + assert url == f"https://inference.generativeai.{region}.oci.oraclecloud.com" + + # --------------------------------------------------------------------------- # validate_oci_environment # ---------------------------------------------------------------------------