Skip to content

fix: harden CORS credentials, create_views exception handling, and spend log cleanup loop - #25559

Merged
yuneng-berri merged 5 commits into
BerriAI:litellm_yj_apr14from
shreyescodes:fix/cors-and-db-safety-bugs
Apr 14, 2026
Merged

fix: harden CORS credentials, create_views exception handling, and spend log cleanup loop#25559
yuneng-berri merged 5 commits into
BerriAI:litellm_yj_apr14from
shreyescodes:fix/cors-and-db-safety-bugs

Conversation

@shreyescodes

Copy link
Copy Markdown

Type

Bug Fix

Changes

1. CORS security fix (proxy_server.py)
allow_origins=["*"] + allow_credentials=True is a browser security misconfiguration — any origin can make credentialed cross-origin requests. Credentials are now automatically disabled when wildcard origins are used. Set LITELLM_CORS_ORIGINS=https://your-ui.com to re-enable credentials with specific origins.

2. create_views.py — exception masking
Broad except Exception: pass silently swallowed real DB errors (auth failures, connection errors) and blindly attempted CREATE VIEW. Now only genuine "view does not exist"/"undefined" errors proceed to view creation; all others are re-raised.

3. spend_log_cleanup.py — infinite loop guard
execute_raw() return type is now validated as int before use as a deletion count. An unexpected return type (e.g. None) previously made deleted_count == 0 evaluate False, causing an infinite deletion loop.

…nup loop

- proxy_server.py: disable allow_credentials when allow_origins=['*'] (wildcard
  + credentials is a browser security misconfiguration). Add LITELLM_CORS_ORIGINS
  env var to configure explicit allowed origins.
- create_views.py: narrow broad 'except Exception' to only catch genuine
  'view does not exist' errors; re-raise all other DB errors (auth, connection,
  etc.) that were previously silently swallowed.
- spend_log_cleanup.py: validate execute_raw() return type is int before using
  it as a deletion count; break loop safely on unexpected types to prevent
  infinite deletion loops.
@vercel

vercel Bot commented Apr 11, 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 Apr 11, 2026 5:39pm

Request Review

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@codspeed-hq

codspeed-hq Bot commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing shreyescodes:fix/cors-and-db-safety-bugs (75438ac) with main (01b9b50)

Open in CodSpeed

@codecov

codecov Bot commented Apr 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 37.50000% with 35 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/proxy/db/create_views.py 30.00% 35 Missing ⚠️

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR hardens three independent issues: CORS credentials are now auto-disabled when wildcard origins are in use (with a LITELLM_CORS_ALLOW_CREDENTIALS escape hatch for opt-in), create_missing_views re-raises real DB errors instead of silently swallowing them via a tightly-scoped _VIEW_NOT_FOUND_MARKERS tuple, and the _delete_old_logs batch loop now guards against non-int returns from execute_raw to prevent an infinite deletion loop. Each fix is accompanied by focused unit tests that import production code directly rather than mirroring it locally.

Confidence Score: 5/5

Safe to merge — all three fixes are correct, well-tested, and address real production issues.

All remaining findings are P2 style suggestions (missing return type annotation). No P0/P1 issues remain after the previous thread concerns were resolved in this implementation.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/proxy_server.py Introduces _get_cors_config() to compute origins/credentials from env vars; correctly disables credentials when wildcard origins are used. Missing return type annotation on the new helper.
litellm/proxy/db/create_views.py Replaces bare except Exception: pass with a module-level _VIEW_NOT_FOUND_MARKERS tuple; non-view-not-found errors are now re-raised correctly. Print statements replaced with verbose_logger.debug.
litellm/proxy/db/db_transaction_queue/spend_log_cleanup.py Adds isinstance(deleted_result, int) guard in _delete_old_logs; non-int return from execute_raw now breaks the loop instead of silently continuing, preventing an infinite deletion loop.
tests/test_litellm/proxy/test_cors_config.py Tests import _get_cors_config directly from proxy_server, exercising real production code. Covers wildcard/explicit origins, blank entries, credential overrides, and a module-level invariant check.
tests/test_litellm/proxy/db/test_create_views.py New test file covering re-raise on connection/permission errors and creation on does not exist/undefined table markers. The undefined function test correctly verifies bare undefined is no longer treated as a view-not-found signal.
tests/test_litellm/proxy/test_spend_log_cleanup.py New tests verify abort-on-non-int and multi-batch continuation; assertion formatting on existing test reformatted without weakening coverage.
docs/my-website/docs/proxy/config_settings.md Adds documentation for new LITELLM_CORS_ALLOW_CREDENTIALS and LITELLM_CORS_ORIGINS env vars in alphabetical order.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[proxy_server startup] --> B["_get_cors_config()"]
    B --> C{LITELLM_CORS_ORIGINS set?}
    C -- No / empty --> D["origins = ['*']"]
    C -- Yes --> E["origins = parsed list"]
    D --> F{LITELLM_CORS_ALLOW_CREDENTIALS set?}
    E --> F
    F -- Yes --> G["allow_credentials = env value (true/false)"]
    F -- No --> H{"'*' in origins?"}
    H -- Yes --> I["allow_credentials = False ✅ secure default"]
    H -- No --> J["allow_credentials = True"]
    G --> K[CORSMiddleware configured]
    I --> K
    J --> K

    subgraph create_views
        L["query_raw(SELECT 1 FROM view)"]
        L -- success --> M[View exists, skip]
        L -- exception --> N{error_msg in _VIEW_NOT_FOUND_MARKERS?}
        N -- Yes --> O["execute_raw(CREATE VIEW)"]
        N -- No --> P[re-raise - real DB error]
    end

    subgraph spend_log_cleanup
        Q["execute_raw(DELETE ...)"]
        Q --> R{isinstance result int?}
        R -- Yes --> S{result == 0?}
        S -- No --> T[total_deleted += result]
        T --> Q
        S -- Yes --> U[break - done]
        R -- No --> V[log error + break - prevents infinite loop]
    end
Loading

Reviews (5): Last reviewed commit: "docs: add LITELLM_CORS_ORIGINS and LITEL..." | Re-trigger Greptile

Comment on lines +12 to +22
def _compute_cors_config(cors_origins_env):
"""
Mirror of the CORS config logic in proxy_server.py.
Kept here so tests remain isolated from module-level side-effects.
"""
if cors_origins_env is None or cors_origins_env.strip() == "":
origins = ["*"]
else:
origins = [o.strip() for o in cors_origins_env.split(",") if o.strip()]
allow_cors_credentials = "*" not in origins
return origins, allow_cors_credentials

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.

P2 CORS tests shadow the real implementation

_compute_cors_config is a local copy of the production logic rather than an import from proxy_server.py. If the origins / allow_cors_credentials logic in proxy_server.py diverges in the future, these tests will keep passing while the real code is broken. Consider importing and testing the actual values set at module load time, or at minimum add a structural test that proxy_server.allow_cors_credentials equals "*" not in proxy_server.origins so a drift will fail CI.

Comment thread litellm/proxy/proxy_server.py Outdated
Comment on lines +1143 to +1149
_cors_origins_env = os.getenv("LITELLM_CORS_ORIGINS")
if _cors_origins_env is None or _cors_origins_env.strip() == "":
origins = ["*"]
else:
origins = [o.strip() for o in _cors_origins_env.split(",") if o.strip()]

allow_cors_credentials = "*" not in origins

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.

P2 Backwards-incompatible default change for allow_credentials

Prior to this PR, allow_credentials was hardcoded True regardless of origin. Now it defaults to False whenever LITELLM_CORS_ORIGINS is unset. Per the team's rule on backwards-incompatible changes, this should be gated behind a user-controlled flag so existing deployments are not silently affected.

In practice, browsers already reject credentialed responses with Access-Control-Allow-Origin: * per the CORS spec, so no browser client would have been working with the old config. However, non-browser HTTP clients or tests that inspect the raw Access-Control-Allow-Credentials header would see a change in behaviour without any migration path.

Rule Used: What: avoid backwards-incompatible changes without... (source)

Comment on lines +27 to +29
error_msg = str(e).lower()
if "does not exist" not in error_msg and "undefined" not in error_msg:
raise

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.

P2 Overly broad "undefined" substring match

Checking "undefined" not in error_msg is wide enough to match unrelated error messages — e.g. "column 'undefined_col' referenced in query" or "undefined function". Any such error would be misclassified as a missing-view signal and trigger a CREATE VIEW call rather than surfacing the real problem.

A tighter pattern would reduce false matches:

VIEW_NOT_FOUND_MSGS = ("does not exist", "no such table", "undefined table")
if not any(marker in error_msg for marker in VIEW_NOT_FOUND_MSGS):
    raise

The same pattern is repeated across all eight view blocks, so a single shared tuple keeps it DRY and easier to extend.

…w feedback)

Add test_proxy_server_cors_invariant which directly imports and checks the
module-level origins and allow_cors_credentials variables in proxy_server.py.
This catches any future drift between the mirror helper and the real code.
@krrish-berri-2

Copy link
Copy Markdown
Contributor

@yuneng-berri please review and merge based on your analysis of PR impact + approach

@yuneng-berri
yuneng-berri changed the base branch from main to litellm_yj_apr14 April 14, 2026 04:16
@yuneng-berri
yuneng-berri merged commit b0a40fd into BerriAI:litellm_yj_apr14 Apr 14, 2026
50 of 51 checks passed
@yuneng-berri

Copy link
Copy Markdown
Contributor

Merging into a staging branch for full CI to run first. Thanks for the contribution!

fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…fety-bugs

fix: harden CORS credentials, create_views exception handling, and spend log cleanup loop
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.

5 participants