Skip to content

fix(proxy): enforce require_managed_files on every route that accepts a raw provider id - #35551

Merged
mateo-berri merged 5 commits into
litellm_internal_stagingfrom
devin_ai_require_managed_files_read_paths_35530
Aug 8, 2026
Merged

fix(proxy): enforce require_managed_files on every route that accepts a raw provider id#35551
mateo-berri merged 5 commits into
litellm_internal_stagingfrom
devin_ai_require_managed_files_read_paths_35530

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • require_managed_files only guarded POST /v1/files
  • Raw provider ids still reached read, delete, batch and fine-tune routes
  • No ownership row exists for a raw id, so no tenant check runs
  • Any team could read, reuse or destroy another team's objects
  • Forged managed ids could embed another team's provider ids

How it solves it:

  • One guard rejects raw provider ids on every such route
  • Managed ids are verified against stored ownership before decoding
  • Covers files, batches, fine-tuning jobs and vector store files
  • Rejection happens before credential resolution and any provider call
  • Off by default; only fires when the setting is on

User Flow

Before: team A opts into managed files, and another team can still use raw or forged ids to read, run against, cancel and destroy its objects

  1. The proxy admin sets require_managed_files: true in litellm_settings and restarts the proxy
  2. Team A sends POST /v1/files with a JSONL batch input and gets back a long scrambled gateway id, bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9vY3RldC1zdHJlYW07dW5pZmll...
  3. Team A base64-decodes that id and reads OpenAI's own id out of the middle of it, file-MQfgEyYAXEtBzAyFxyEXg5. The same raw id is also visible in provider dashboards, in input_file_id on any batch object, and in support tickets
  4. Team A sends POST /v1/batches with the gateway id and gets a running batch back
  5. Team B, holding a key on a different team, sends GET /v1/files/file-MQfgEyYAXEtBzAyFxyEXg5 and gets 200 with team A's file metadata
  6. Team B sends GET /v1/files/file-MQfgEyYAXEtBzAyFxyEXg5/content and gets 200 with the file body in full, including the line team A confidential payroll row: alice 250000
  7. Team B sends POST /v1/batches with "input_file_id": "file-MQfgEyYAXEtBzAyFxyEXg5" and gets 200 and a real batch id, a job now running on team A's data and billed to the shared account
  8. Team B sends GET /v1/batches/batch_6a7706968acc8190aec13a7768d55aca for a batch it does not own and gets 200 with its status
  9. Team B sends POST /v1/fine_tuning/jobs with the same raw file as training_file, and POST /v1/fine_tuning/jobs/ftjob-abc123raw/cancel. Both come back with OpenAI's own error text, so the gateway relayed the raw ids upstream under the shared account instead of refusing them
  10. Team B wraps that raw file id in a forged gateway-shaped id and sends POST /v1/vector_stores/<its own vector store id>/files. It gets 200, so team A's file is attached to team B's vector store
  11. Team B replays team A's real gateway ids at GET /v1/files/<team A's gateway file id> and GET /v1/batches/<team A's gateway batch id> and gets 403 on both, so the only ids the gateway checks are the ones that look like its own
  12. Team B sends POST /v1/batches/batch_6a7706968acc8190aec13a7768d55aca/cancel and the status flips to cancelling
  13. Team B sends DELETE /v1/files/file-MQfgEyYAXEtBzAyFxyEXg5 and gets {"deleted": true}
  14. Team A goes back to GET /v1/files/<its own gateway file id>/content and now gets 404 No such File object. Its data is gone, destroyed by a tenant it cannot see and has no way to identify

After: raw and forged ids are refused at the gateway, and team A's objects are untouched

  1. The proxy admin sets require_managed_files: true in litellm_settings and restarts the proxy
  2. Team A sends POST /v1/files with a JSONL batch input and gets back a long scrambled gateway id, bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9vY3RldC1zdHJlYW07dW5pZmll...
  3. Team A base64-decodes that id and reads OpenAI's own id out of the middle of it, file-9szcvDSFsQpjhZUAFESRVg
  4. Team A sends POST /v1/batches with the gateway id and gets a running batch back
  5. Team B sends GET /v1/files/file-9szcvDSFsQpjhZUAFESRVg and gets 400 Raw provider file ids cannot be used when require_managed_files is enabled in litellm_settings. Use the LiteLLM managed file id returned when the file was created.
  6. Team B sends GET /v1/files/file-9szcvDSFsQpjhZUAFESRVg/content and gets the same 400. No file body is returned
  7. Team B sends POST /v1/batches with "input_file_id": "file-9szcvDSFsQpjhZUAFESRVg" and gets the same 400. No batch is created and nothing is billed
  8. Team B sends GET /v1/batches/batch_6a7709441a6c8190b8387188755ad369 and gets 400 Raw provider batch ids cannot be used when require_managed_files is enabled in litellm_settings...
  9. Team B sends the same two fine-tuning requests and gets 400 Raw provider file ids cannot be used... and 400 Raw provider fine-tuning job ids cannot be used.... Neither reaches OpenAI, so no upstream error text comes back
  10. Team B sends the forged gateway-shaped id to POST /v1/vector_stores/<its own vector store id>/files and gets 404 File not found. The file is not attached
  11. Team B replays team A's real gateway ids and gets 403 The caller does not have access to this managed file id. and 403 The caller does not have access to this managed batch id., now answered before the gateway picks credentials or contacts a provider
  12. Team B sends POST /v1/batches/batch_6a7709441a6c8190b8387188755ad369/cancel and gets the same batch 400. The batch keeps running
  13. Team B sends DELETE /v1/files/file-9szcvDSFsQpjhZUAFESRVg and gets the same file 400. Nothing is deleted
  14. Team A goes back to GET /v1/files/<its own gateway file id>/content and gets 200 with the payroll line intact, and GET /v1/batches/<its own gateway batch id> still returns its batch. Team B can no longer read, attach, run against, cancel or delete anything belonging to team A

Relevant issues

Fixes #35530

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Two teams on one proxy, one shared OpenAI account, hitting the real api.openai.com (real files, real batch jobs, real
spend). Team B replays team A's raw provider ids, a forged gateway-shaped id, and team A's real gateway ids at every
route that takes a caller-supplied id, then team A re-reads its own objects.

Both legs run the same script and differ only in the commit the proxy booted from: the before leg on the merge base
dee667edb7, the after leg on this PR's head 8c0556abf6. Each leg had its own worktree, its own venv, its own
postgres database and its own port. Response bodies are truncated at 600 characters, and long gateway ids are shown as
the shell variables holding them.

Probe, team B against team A's object Before dee667edb7 After 8c0556abf6
GET /v1/files/{raw id} 200, metadata 400, refused
GET /v1/files/{raw id}/content 200, payroll line leaked 400, refused
POST /v1/batches with raw input_file_id 200, job created 400, refused
GET /v1/batches/{raw id} 200, status read 400, refused
POST /v1/fine_tuning/jobs with raw file relayed to OpenAI 400, refused
POST /v1/fine_tuning/jobs/{raw id}/cancel relayed to OpenAI 400, refused
POST /v1/vector_stores/{id}/files, forged id 200, file attached 404, refused
GET /v1/files/{team A gateway id} 403 403
GET /v1/batches/{team A gateway id} 403 403
POST /v1/batches/{raw id}/cancel 200, cancelling 400, refused
DELETE /v1/files/{raw id} 200, deleted: true 400, refused
team A rereads its own file content 404, data destroyed 200, payroll line intact
team A rereads its own batch 200 200
team A submits its own file to fine-tuning 400 from OpenAI 400 from OpenAI

Config, identical for both runs:

model_list:
  - model_name: gpt-4o-mini
    litellm_params:
      model: openai/gpt-4o-mini
      api_key: os.environ/OPENAI_API_KEY

litellm_settings:
  require_managed_files: true

files_settings:
  - custom_llm_provider: openai
    api_key: os.environ/OPENAI_API_KEY

finetune_settings:
  - custom_llm_provider: openai
    api_key: os.environ/OPENAI_API_KEY

general_settings:
  master_key: sk-1234

Two tenants, one key each:

$ for T in a b; do
    TEAM=$(curl -s -X POST "$P/team/new" -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" \
      -d "{\"team_alias\":\"qa-$T-35551\",\"models\":[\"gpt-4o-mini\"]}" \
      | python3 -c 'import sys,json; print(json.load(sys.stdin)["team_id"])')
    curl -s -X POST "$P/key/generate" -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" \
      -d "{\"team_id\":\"$TEAM\",\"models\":[\"gpt-4o-mini\"]}" | python3 -c 'import sys,json; print(json.load(sys.stdin)["key"])'
  done

Team A's legitimate setup, plus one batch created straight on the shared OpenAI account so the proxy never minted it
(the realistic case for a shared key: objects the gateway has no record of), plus a vector store team B owns and a
forged id wrapping team A's raw file id:

$ curl -s -X POST "$P/v1/files" -H "Authorization: Bearer $KEY_A" \
    -F purpose="batch" -F target_model_names="gpt-4o-mini" -F file="@team_a_secret.jsonl"
$ curl -s -X POST "$P/v1/batches" -H "Authorization: Bearer $KEY_A" -H "Content-Type: application/json" \
    -d "{\"input_file_id\":\"$MANAGED_FILE\",\"endpoint\":\"/v1/chat/completions\",\"completion_window\":\"24h\"}"
$ curl -s -X POST https://api.openai.com/v1/batches -H "Authorization: Bearer $OPENAI_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"input_file_id\":\"$RAW_FILE\",\"endpoint\":\"/v1/chat/completions\",\"completion_window\":\"24h\"}"
$ curl -s -X POST https://api.openai.com/v1/vector_stores -H "Authorization: Bearer $OPENAI_API_KEY" \
    -H "Content-Type: application/json" -d '{"name":"qa-team-b-35551"}'
$ python3 -c 'import base64,uuid; print(base64.urlsafe_b64encode(f"litellm_proxy:application/octet-stream;unified_id,{uuid.uuid4()};target_model_names,gpt-4o-mini;llm_output_file_id,'"$RAW_FILE"';llm_output_file_model_id,forged-model-id".encode()).decode().rstrip("="))'

team_a_secret.jsonl is one batch line whose prompt is team A confidential payroll row: alice 250000

Before, at dee667edb7 (merge base, port 53417)

############ SETUP (before) ############
[team A] POST $P/v1/files -> managed file id: bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9vY3RldC1zdHJlYW07dW5pZmll...
[team A] raw provider file id decoded out of it: file-MQfgEyYAXEtBzAyFxyEXg5
[team A] POST $P/v1/batches -> managed batch id: bGl0ZWxsbV9wcm94eTttb2RlbF9pZDo2MWNmZWQzYjExNDZjNWFjMDgxMzc3...
[shared account, straight to api.openai.com] raw provider batch id this proxy never minted: batch_6a7706968acc8190aec13a7768d55aca
[team B] its own vector store: vs_6a77069718d08191b5c1857bbb52d9e7
[team B] forged gateway-shaped id wrapping team A's raw file id: bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9vY3RldC1zdHJlYW07dW5pZmll...

=== 1. [team B] GET $P/v1/files/$RAW_FILE ===
$ curl -s "$P/v1/files/file-MQfgEyYAXEtBzAyFxyEXg5" -H "Authorization: Bearer $KEY_B"
  -> HTTP 200  {"id":"file-MQfgEyYAXEtBzAyFxyEXg5","bytes":213,"created_at":1786185364,"filename":"modified_file.jsonl","object":"file","purpose":"batch","status":"processed","expires_at":1788777364,"status_details":null}

=== 2. [team B] GET $P/v1/files/$RAW_FILE/content ===
$ curl -s "$P/v1/files/file-MQfgEyYAXEtBzAyFxyEXg5/content" -H "Authorization: Bearer $KEY_B"
  -> HTTP 200  {"custom_id": "qa-1", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "team A confidential payroll row: alice 250000"}], "max_tokens": 5}}

=== 3. [team B] POST $P/v1/batches  input_file_id=$RAW_FILE ===
$ curl -s -X POST "$P/v1/batches" -H "Authorization: Bearer $KEY_B" -H "Content-Type: application/json" -d '{"input_file_id":"file-MQfgEyYAXEtBzAyFxyEXg5","endpoint":"/v1/chat/completions","completion_window":"24h"}'
  -> HTTP 200  {"id":"batch_6a770699359c8190b3faed8751728df9","completion_window":"24h","created_at":1786185369,"endpoint":"/v1/chat/completions","input_file_id":"file-MQfgEyYAXEtBzAyFxyEXg5","object":"batch","status":"validating","cancelled_at":null,"cancelling_at":null,"completed_at":null,"error_file_id":null,"errors":null,"expired_at":null,"expires_at":1786271769,...}

=== 4. [team B] GET $P/v1/batches/$RAW_BATCH ===
$ curl -s "$P/v1/batches/batch_6a7706968acc8190aec13a7768d55aca" -H "Authorization: Bearer $KEY_B"
  -> HTTP 200  {"id":"batch_6a7706968acc8190aec13a7768d55aca","completion_window":"24h","created_at":1786185366,"endpoint":"/v1/chat/completions","input_file_id":"file-MQfgEyYAXEtBzAyFxyEXg5","object":"batch","status":"in_progress","cancelled_at":null,"cancelling_at":null,...,"model":"gpt-4o-mini-2024-07-18","request_counts":{"completed":0,"failed":0,"total":1},...}

=== 5. [team B] POST $P/v1/fine_tuning/jobs  training_file=$RAW_FILE ===
$ curl -s -X POST "$P/v1/fine_tuning/jobs" -H "Authorization: Bearer $KEY_B" -H "Content-Type: application/json" -d '{"model":"gpt-4o-mini","training_file":"file-MQfgEyYAXEtBzAyFxyEXg5","custom_llm_provider":"openai"}'
  -> HTTP 400  {"error":{"message":"Error code: 400 - {'error': {'message': 'Model gpt-4o-mini is not available for fine-tuning or does not exist.', 'type': 'invalid_request_error', 'param': None, 'code': 'model_not_available'}}","type":"internal_server_error","param":null,"code":"400"}}

=== 6. [team B] POST $P/v1/fine_tuning/jobs/ftjob-abc123raw/cancel ===
$ curl -s -X POST "$P/v1/fine_tuning/jobs/ftjob-abc123raw/cancel" -H "Authorization: Bearer $KEY_B" -H "Content-Type: application/json" -d '{"custom_llm_provider":"openai"}'
  -> HTTP 404  {"error":{"message":"Error code: 404 - {'error': {'message': 'Could not find fine tune: ftjob-abc123raw', 'type': 'invalid_request_error', 'param': 'fine_tune_id', 'code': 'fine_tune_not_found'}}","type":"internal_server_error","param":"fine_tune_id","code":"404"}}

=== 7. [team B] POST $P/v1/vector_stores/$VS_ID/files  file_id=<forged gateway-shaped id> ===
$ curl -s -X POST "$P/v1/vector_stores/vs_6a77069718d08191b5c1857bbb52d9e7/files" -H "Authorization: Bearer $KEY_B" -H "Content-Type: application/json" -d "{\"file_id\":\"$FORGED\"}"
  -> HTTP 200  {"id":"bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9vY3RldC1zdHJlYW07dW5pZmllZF9pZCw0YzY3NDZlNS0wODMwLTQyZTItYmQzYy1mZDdiMjM5NGNkYzA7...","object":"vector_store.file","usage_bytes":0,"created_at":1786185371,"vector_store_id":"vs_6a77069718d08191b5c1857bbb52d9e7","status":"in_progress","last_error":null,...}

=== 8. [team B] GET $P/v1/files/$MANAGED_FILE  (team A's real gateway id) ===
$ curl -s "$P/v1/files/$MANAGED_FILE" -H "Authorization: Bearer $KEY_B"
  -> HTTP 403  {"error":{"message":"User None does not have access to the file bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9vY3RldC1zdHJlYW07dW5pZmllZF9pZCxmMjY5MDYyNi0yMTI0LTQ0MTUtOGZjNy04ZTAzMTI0MjI4YjY7...","type":"None","param":"None","code":"403"}}

=== 9. [team B] GET $P/v1/batches/$MANAGED_BATCH  (team A's real gateway id) ===
$ curl -s "$P/v1/batches/$MANAGED_BATCH" -H "Authorization: Bearer $KEY_B"
  -> HTTP 403  {"error":{"message":"User None does not have access to the object bGl0ZWxsbV9wcm94eTttb2RlbF9pZDo2MWNmZWQzYjExNDZjNWFjMDgxMzc3NzdjOTgyOWRkZDNiOWM0NTk0NDQ0N2E0N2UzOWEzYWZlMTcyODc0YjMzO2xsbV9iYXRjaF9pZDpiYXRjaF82YTc3MDY5NWM0ZjQ4MTkwODY2Nzc1OTBlOTA1YjI0OA","type":"internal_server_error","param":"None","code":"403"}}

=== 10. [team B] POST $P/v1/batches/$RAW_BATCH/cancel  (destructive) ===
$ curl -s -X POST "$P/v1/batches/batch_6a7706968acc8190aec13a7768d55aca/cancel" -H "Authorization: Bearer $KEY_B"
  -> HTTP 200  {"id":"batch_6a7706968acc8190aec13a7768d55aca","completion_window":"24h","created_at":1786185366,"endpoint":"/v1/chat/completions","input_file_id":"file-MQfgEyYAXEtBzAyFxyEXg5","object":"batch","status":"cancelling","cancelled_at":null,"cancelling_at":1786185371,...}

=== 11. [team B] DELETE $P/v1/files/$RAW_FILE  (destructive) ===
$ curl -s -X DELETE "$P/v1/files/file-MQfgEyYAXEtBzAyFxyEXg5" -H "Authorization: Bearer $KEY_B"
  -> HTTP 200  {"id":"file-MQfgEyYAXEtBzAyFxyEXg5","deleted":true,"object":"file"}

############ VICTIM RE-CHECK (team A, own gateway ids) (before) ############

=== 12. [team A] GET $P/v1/files/$MANAGED_FILE/content  (is their data still there?) ===
$ curl -s "$P/v1/files/$MANAGED_FILE/content" -H "Authorization: Bearer $KEY_A"
  -> HTTP 404  {"error":{"message":"Error code: 404 - {'error': {'message': 'No such File object: file-MQfgEyYAXEtBzAyFxyEXg5', 'type': 'invalid_request_error', 'param': 'id', 'code': None}, 'detail': {'message': 'No such File object: file-MQfgEyYAXEtBzAyFxyEXg5', 'code': None}}...","type":"invalid_request_error","param":"id","code":"404"}}

=== 13. [team A] GET $P/v1/batches/$MANAGED_BATCH  (is their batch still running?) ===
$ curl -s "$P/v1/batches/$MANAGED_BATCH" -H "Authorization: Bearer $KEY_A"
  -> HTTP 200  {"id":"bGl0ZWxsbV9wcm94eTttb2RlbF9pZDo2MWNmZWQzYjExNDZjNWFjMDgxMzc3...","completion_window":"24h","created_at":1786185365,"endpoint":"/v1/chat/completions","input_file_id":"bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9vY3RldC1zdHJlYW07dW5pZmllZF9pZCxmMjY5MDYyNi0yMTI0LTQ0MTUtOGZjNy04ZTAzMTI0MjI4YjY7...",...}

=== 14. [team A] POST $P/v1/fine_tuning/jobs  training_file=$MANAGED_FILE  (own gateway id still reaches OpenAI) ===
$ curl -s -X POST "$P/v1/fine_tuning/jobs" -H "Authorization: Bearer $KEY_A" -H "Content-Type: application/json" -d "{\"model\":\"gpt-4o-mini\",\"training_file\":\"$MANAGED_FILE\",\"custom_llm_provider\":\"openai\"}"
  -> HTTP 400  {"error":{"message":"Error code: 400 - {'error': {'message': 'invalid training_file: bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9vY3RldC1zdHJlYW07dW5pZmllZF9pZCxmMjY5MDYyNi0yMTI0LTQ0MTUtOGZjNy04ZTAzMTI0MjI4YjY7...', 'type': 'invalid_request_error', 'param': 'training_file', 'code': None}}","type":"internal_server_error","param":"training_file","code":"400"}}

Steps 5 and 6 answer with OpenAI's own error text, which is the tell: the raw ids were forwarded upstream under the
shared account rather than refused. Any raw id belonging to a real fine-tunable model or a real job would have gone
through the same way. Step 7 shows the gateway trusts the shape of a managed id rather than verifying it, so a
hand-forged one wrapping team A's raw file id attaches that file to team B's vector store

After, at 8c0556abf6 (this PR's head, port 53219)

############ SETUP (after) ############
[team A] POST $P/v1/files -> managed file id: bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9vY3RldC1zdHJlYW07dW5pZmll...
[team A] raw provider file id decoded out of it: file-9szcvDSFsQpjhZUAFESRVg
[team A] POST $P/v1/batches -> managed batch id: bGl0ZWxsbV9wcm94eTttb2RlbF9pZDo2MWNmZWQzYjExNDZjNWFjMDgxMzc3...
[shared account, straight to api.openai.com] raw provider batch id this proxy never minted: batch_6a7709441a6c8190b8387188755ad369
[team B] its own vector store: vs_6a7709445c34819194250d8c8e19cc81
[team B] forged gateway-shaped id wrapping team A's raw file id: bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9vY3RldC1zdHJlYW07dW5pZmll...

=== 1. [team B] GET $P/v1/files/$RAW_FILE ===
$ curl -s "$P/v1/files/file-9szcvDSFsQpjhZUAFESRVg" -H "Authorization: Bearer $KEY_B"
  -> HTTP 400  {"error":{"message":"Raw provider file ids cannot be used when require_managed_files is enabled in litellm_settings. Use the LiteLLM managed file id returned when the file was created.","type":"None","param":"None","code":"400"}}

=== 2. [team B] GET $P/v1/files/$RAW_FILE/content ===
$ curl -s "$P/v1/files/file-9szcvDSFsQpjhZUAFESRVg/content" -H "Authorization: Bearer $KEY_B"
  -> HTTP 400  {"error":{"message":"Raw provider file ids cannot be used when require_managed_files is enabled in litellm_settings. Use the LiteLLM managed file id returned when the file was created.","type":"None","param":"None","code":"400"}}

=== 3. [team B] POST $P/v1/batches  input_file_id=$RAW_FILE ===
$ curl -s -X POST "$P/v1/batches" -H "Authorization: Bearer $KEY_B" -H "Content-Type: application/json" -d '{"input_file_id":"file-9szcvDSFsQpjhZUAFESRVg","endpoint":"/v1/chat/completions","completion_window":"24h"}'
  -> HTTP 400  {"error":{"message":"Raw provider file ids cannot be used when require_managed_files is enabled in litellm_settings. Use the LiteLLM managed file id returned when the file was created.","type":"internal_server_error","param":"None","code":"400"}}

=== 4. [team B] GET $P/v1/batches/$RAW_BATCH ===
$ curl -s "$P/v1/batches/batch_6a7709441a6c8190b8387188755ad369" -H "Authorization: Bearer $KEY_B"
  -> HTTP 400  {"error":{"message":"Raw provider batch ids cannot be used when require_managed_files is enabled in litellm_settings. Use the LiteLLM managed batch id returned when the batch was created.","type":"internal_server_error","param":"None","code":"400"}}

=== 5. [team B] POST $P/v1/fine_tuning/jobs  training_file=$RAW_FILE ===
$ curl -s -X POST "$P/v1/fine_tuning/jobs" -H "Authorization: Bearer $KEY_B" -H "Content-Type: application/json" -d '{"model":"gpt-4o-mini","training_file":"file-9szcvDSFsQpjhZUAFESRVg","custom_llm_provider":"openai"}'
  -> HTTP 400  {"error":{"message":"Raw provider file ids cannot be used when require_managed_files is enabled in litellm_settings. Use the LiteLLM managed file id returned when the file was created.","type":"internal_server_error","param":"None","code":"400"}}

=== 6. [team B] POST $P/v1/fine_tuning/jobs/ftjob-abc123raw/cancel ===
$ curl -s -X POST "$P/v1/fine_tuning/jobs/ftjob-abc123raw/cancel" -H "Authorization: Bearer $KEY_B" -H "Content-Type: application/json" -d '{"custom_llm_provider":"openai"}'
  -> HTTP 400  {"error":{"message":"Raw provider fine-tuning job ids cannot be used when require_managed_files is enabled in litellm_settings. Use the LiteLLM managed fine-tuning job id returned when the fine-tuning job was created.","type":"internal_server_error","param":"None","code":"400"}}

=== 7. [team B] POST $P/v1/vector_stores/$VS_ID/files  file_id=<forged gateway-shaped id> ===
$ curl -s -X POST "$P/v1/vector_stores/vs_6a7709445c34819194250d8c8e19cc81/files" -H "Authorization: Bearer $KEY_B" -H "Content-Type: application/json" -d "{\"file_id\":\"$FORGED\"}"
  -> HTTP 404  {"detail":"File not found: bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9vY3RldC1zdHJlYW07dW5pZmllZF9pZCxjOTFjNTRjZC04YmViLTQ4Y2QtYmZmNC02NDQ1NjZkZTMzNmI7..."}

=== 8. [team B] GET $P/v1/files/$MANAGED_FILE  (team A's real gateway id) ===
$ curl -s "$P/v1/files/$MANAGED_FILE" -H "Authorization: Bearer $KEY_B"
  -> HTTP 403  {"error":{"message":"The caller does not have access to this managed file id.","type":"None","param":"None","code":"403"}}

=== 9. [team B] GET $P/v1/batches/$MANAGED_BATCH  (team A's real gateway id) ===
$ curl -s "$P/v1/batches/$MANAGED_BATCH" -H "Authorization: Bearer $KEY_B"
  -> HTTP 403  {"error":{"message":"The caller does not have access to this managed batch id.","type":"internal_server_error","param":"None","code":"403"}}

=== 10. [team B] POST $P/v1/batches/$RAW_BATCH/cancel  (destructive) ===
$ curl -s -X POST "$P/v1/batches/batch_6a7709441a6c8190b8387188755ad369/cancel" -H "Authorization: Bearer $KEY_B"
  -> HTTP 400  {"error":{"message":"Raw provider batch ids cannot be used when require_managed_files is enabled in litellm_settings. Use the LiteLLM managed batch id returned when the batch was created.","type":"internal_server_error","param":"None","code":"400"}}

=== 11. [team B] DELETE $P/v1/files/$RAW_FILE  (destructive) ===
$ curl -s -X DELETE "$P/v1/files/file-9szcvDSFsQpjhZUAFESRVg" -H "Authorization: Bearer $KEY_B"
  -> HTTP 400  {"error":{"message":"Raw provider file ids cannot be used when require_managed_files is enabled in litellm_settings. Use the LiteLLM managed file id returned when the file was created.","type":"None","param":"None","code":"400"}}

############ VICTIM RE-CHECK (team A, own gateway ids) (after) ############

=== 12. [team A] GET $P/v1/files/$MANAGED_FILE/content  (is their data still there?) ===
$ curl -s "$P/v1/files/$MANAGED_FILE/content" -H "Authorization: Bearer $KEY_A"
  -> HTTP 200  {"custom_id": "qa-1", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "team A confidential payroll row: alice 250000"}], "max_tokens": 5}}

=== 13. [team A] GET $P/v1/batches/$MANAGED_BATCH  (is their batch still running?) ===
$ curl -s "$P/v1/batches/$MANAGED_BATCH" -H "Authorization: Bearer $KEY_A"
  -> HTTP 200  {"id":"bGl0ZWxsbV9wcm94eTttb2RlbF9pZDo2MWNmZWQzYjExNDZjNWFjMDgxMzc3...","completion_window":"24h","created_at":1786186051,"endpoint":"/v1/chat/completions","input_file_id":"bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9vY3RldC1zdHJlYW07dW5pZmllZF9pZCwxNGNlZmQ0NC0zMjExLTRlZjUtOGQzNy1lMTE0N2NjYmNhNWU7...",...}

=== 14. [team A] POST $P/v1/fine_tuning/jobs  training_file=$MANAGED_FILE  (own gateway id still reaches OpenAI) ===
$ curl -s -X POST "$P/v1/fine_tuning/jobs" -H "Authorization: Bearer $KEY_A" -H "Content-Type: application/json" -d "{\"model\":\"gpt-4o-mini\",\"training_file\":\"$MANAGED_FILE\",\"custom_llm_provider\":\"openai\"}"
  -> HTTP 400  {"error":{"message":"Error code: 400 - {'error': {'message': 'invalid training_file: bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9vY3RldC1zdHJlYW07dW5pZmllZF9pZCwxNGNlZmQ0NC0zMjExLTRlZjUtOGQzNy1lMTE0N2NjYmNhNWU7...', 'type': 'invalid_request_error', 'param': 'training_file', 'code': None}}","type":"internal_server_error","param":"training_file","code":"400"}}

No provider call is made for probes 1 to 6, 10 and 11, and the gateway's own resolution of team A's ids in 12, 13 and 14
is unaffected. Probe 14 is the guard letting team A's own id through: the 400 is OpenAI's, reached over the wire, and it
is byte-identical to the before leg's

The same sweep through the OpenAI Python SDK

The curl legs above are the routes; this is the client a customer actually holds. Same proxies, same objects,
OpenAI(api_key=<team key>, base_url="<proxy>/v1"):

### before, at dee667edb7 (against live objects, before the destructive probes ran)
=== sdk-1. [team B] client.files.retrieve('file-SbCNMzaQ1KZaxhn6aG41QX') ===
  -> HTTP 200  FileObject(id='file-SbCNMzaQ1KZaxhn6aG41QX', bytes=213, created_at=1786185432, filename='modified_file.jsonl', object='file', purpose='batch', status='processed', ...)
=== sdk-2. [team B] client.files.content('file-SbCNMzaQ1KZaxhn6aG41QX') ===
  -> HTTP 200  {"custom_id": "qa-1", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "team A confidential payroll row: alice 250000"}], "max_tokens": 5}}
=== sdk-3. [team B] client.batches.create(input_file_id='file-SbCNMzaQ1KZaxhn6aG41QX') ===
  -> HTTP 200  Batch(id='batch_6a7706dfa8c08190b1a9d802b7457934', ..., status='validating', ...)
=== sdk-4. [team B] client.batches.retrieve('batch_6a7706d9abf481909303bb0f639dcf36') ===
  -> HTTP 200  Batch(id='batch_6a7706d9abf481909303bb0f639dcf36', ..., status='in_progress', ...)
=== sdk-5. [team B] client.files.delete('file-SbCNMzaQ1KZaxhn6aG41QX') ===
  -> HTTP 200  FileDeleted(id='file-SbCNMzaQ1KZaxhn6aG41QX', deleted=True, object='file')
=== sdk-6. [team A] client.files.content(<own gateway id>) ===
  -> HTTP 404  Error code: 404 - {'error': {'message': "... 'No such File object: file-SbCNMzaQ1KZaxhn6aG41QX' ..."}}
=== sdk-7. [team A] client.batches.retrieve(<own gateway id>) ===
  -> HTTP 200  Batch(id='bGl0ZWxsbV9wcm94eTttb2RlbF9pZDo2MWNmZWQzYjExNDZjNWFjMDgxMzc3...', ...)

### after, at 8c0556abf6
=== sdk-1. [team B] client.files.retrieve('file-9szcvDSFsQpjhZUAFESRVg') ===
  -> HTTP 400  Error code: 400 - {'error': {'message': 'Raw provider file ids cannot be used when require_managed_files is enabled in litellm_settings. Use the LiteLLM managed file id returned when the file was created.', 'type': 'None', 'param': 'None', 'code': '400'}}
=== sdk-2. [team B] client.files.content('file-9szcvDSFsQpjhZUAFESRVg') ===
  -> HTTP 400  Error code: 400 - {'error': {'message': 'Raw provider file ids cannot be used when require_managed_files is enabled in litellm_settings. ...', 'code': '400'}}
=== sdk-3. [team B] client.batches.create(input_file_id='file-9szcvDSFsQpjhZUAFESRVg') ===
  -> HTTP 400  Error code: 400 - {'error': {'message': 'Raw provider file ids cannot be used when require_managed_files is enabled in litellm_settings. ...', 'code': '400'}}
=== sdk-4. [team B] client.batches.retrieve('batch_6a7709441a6c8190b8387188755ad369') ===
  -> HTTP 400  Error code: 400 - {'error': {'message': 'Raw provider batch ids cannot be used when require_managed_files is enabled in litellm_settings. ...', 'code': '400'}}
=== sdk-5. [team B] client.files.delete('file-9szcvDSFsQpjhZUAFESRVg') ===
  -> HTTP 400  Error code: 400 - {'error': {'message': 'Raw provider file ids cannot be used when require_managed_files is enabled in litellm_settings. ...', 'code': '400'}}
=== sdk-6. [team A] client.files.content(<own gateway id>) ===
  -> HTTP 200  {"custom_id": "qa-1", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "team A confidential payroll row: alice 250000"}], "max_tokens": 5}}
=== sdk-7. [team A] client.batches.retrieve(<own gateway id>) ===
  -> HTTP 200  Batch(id='bGl0ZWxsbV9wcm94eTttb2RlbF9pZDo2MWNmZWQzYjExNDZjNWFjMDgxMzc3...', ...)

Default off, at 8c0556abf6 (same head commit, port 58907)

The gate is opt-in, so nobody who has not set require_managed_files: true sees any change. Same head commit, same
sweep, one line of config different:

$ diff qa_config.yaml qa_config_off.yaml
8c8
<   require_managed_files: true
---
>   require_managed_files: false
proxy_server.py:4857 - setting litellm.require_managed_files=False

Normalizing away the things that must differ per run (file, batch and vector-store ids, UUIDs, base64 gateway ids, model
hashes, unix timestamps, and the leg label), the sweep log from this run is line-for-line identical to the before leg's.
Status vectors for all three runs, probes 1 through 14 in order:

Run Statuses
before dee667edb7 200 200 200 200 400 404 200 403 403 200 200 404 200 400
after 8c0556abf6, off 200 200 200 200 400 404 200 403 403 200 200 404 200 400
after 8c0556abf6, on 400 400 400 400 400 400 404 403 403 400 400 200 200 400

The default-off sweep log contains zero occurrences of Raw provider, zero of cannot be used, and zero HTTP 500s, and
the proxy log carries no traceback other than the upstream 400s and 404s and the two pre-existing 403s that the before
leg also produced

No 500s from the ownership check

The new guard resolves the enterprise hook at request time, so a proxy where that hook is missing would answer 500 Managed resource ownership validation is unavailable rather than falling open. Across all three runs, 42 probes, the
proxy logs contain no 500 of any kind. Every refusal is a deliberate 400, 403 or 404

Type

🐛 Bug Fix

Changes

require_managed_files was enforced in exactly one place, on upload. Every other route that takes a caller-supplied
provider id accepted a raw one and forwarded it to the provider under whatever credentials the calling team resolves
to. Ownership is only ever recorded for LiteLLM managed ids, so a raw id carries no tenant to check against, and on a
provider account shared by several teams, knowing another tenant's id is enough to read, reuse or destroy the object
behind it.

validate_managed_file_id_requirement in openai_files_endpoints/common_utils.py is generalised to
validate_managed_id_requirement, which handles "file" | "batch" | "fine-tuning job". The validator checks each
managed id against the caller's stored ownership record before any route decodes or forwards it. Raw and model-encoded
ids raise 400, a forged managed-looking id without an ownership row raises 404, and another team's real id raises 403

Call sites invoke the ownership-aware guard before decoding managed ids, resolving credentials, or routing to a
provider:

  • openai_files_endpoints/files_endpoints.py: get_file, get_file_content, delete_file
  • batches_endpoints/endpoints.py: input_file_id on create, batch_id on retrieve and cancel
  • fine_tuning_endpoints/endpoints.py: training_file and validation_file on create, fine_tuning_job_id on
    retrieve and cancel
  • vector_store_files_endpoints/endpoints.py: once in the shared id resolver that all five file routes funnel through
    (create, retrieve, content, update, delete)

validation_file matters on its own: it is uploaded and readable exactly like the training file, so a managed
training_file must not be able to smuggle a raw validation_file past the guard. There is a test for that case.

Tests cover raw ids, forged managed-looking ids and another team's real managed ids. They also assert every downstream
provider seam stays at zero calls, so a guard that runs after routing still fails

Known gaps, out of scope here

Worth separate issues rather than widening this PR:

  • The passthrough routes (/openai/v1/files/{id} and friends) accept raw ids by design and never reach this guard
  • /v1/responses (response_id, previous_response_id), evals, videos, skills, threads, assistants and interactions
    take caller-supplied ids and are not covered by require_managed_files at all
  • A caller can select which upstream credentials to use with ?model= or x-litellm-model without a key model-access
    check
  • Raw ids stay accepted by default, which is still an IDOR risk on a shared provider account. Flipping that default, or
    adding a separate isolation switch, is a product decision worth taking on its own
  • With the setting on, a managed file id sent as training_file is still forwarded to the provider verbatim
    rather than translated back to the raw id, so a fine-tuning create fails upstream with invalid training_file.
    That predates this PR and is unchanged by it

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Note

High Risk
This is a security fix for cross-tenant IDOR on shared provider credentials; behavior changes when require_managed_files is true, and misconfigured ownership hooks return 500.

Overview
When require_managed_files is enabled, validate_managed_id_requirement now runs on every proxy route that accepts a caller-supplied file, batch, or fine-tuning job id—not only on file upload. Raw provider ids and model-encoded ids return 400; LiteLLM unified ids are checked against stored ownership (403 if another tenant’s id).

The guard is wired into files (retrieve, content, delete), batches (create input_file_id, retrieve, cancel), fine-tuning (create training/validation files, retrieve, cancel), and vector store files (via an async shared resolver used by all five file routes). A ManagedResourceAccessChecker protocol types the managed-files hook.

CI adds fine_tuning and vector_store_files test paths; new unit tests assert rejection happens before any provider/router call.

Reviewed by Cursor Bugbot for commit 8c0556a. Bugbot is set up for automated code reviews on this repo. Configure here.

…nd delete

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@CLAassistant

CLAassistant commented Aug 2, 2026

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 all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ mateo-berri
❌ devin-ai-integration[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds opt-in managed-resource ownership validation before covered file, batch, fine-tuning, and vector-store-file operations reach provider routing

  • Generalizes managed-ID validation across resource types
  • Applies ownership checks before decoding IDs or resolving provider credentials
  • Adds focused endpoint regression tests and includes the new test directories in CI

Confidence Score: 5/5

The PR appears safe to merge

The previously reported format-only acceptance path is closed by checking stored ownership before any managed ID is decoded or dispatched, and no blocking failure remains

Important Files Changed

Filename Overview
litellm/proxy/openai_files_endpoints/common_utils.py Replaces format-only managed-ID acceptance with production ownership checks through the managed-files hook
litellm/proxy/openai_files_endpoints/files_endpoints.py Validates caller-supplied file IDs before retrieval, content access, or deletion
litellm/proxy/batches_endpoints/endpoints.py Validates batch input files and batch IDs before decoding, database lookup, credential resolution, or provider calls
litellm/proxy/fine_tuning_endpoints/endpoints.py Validates training, validation, and fine-tuning job IDs before routing operations
litellm/proxy/vector_store_files_endpoints/endpoints.py Centralizes managed file ownership validation in the resolver shared by all vector-store-file operations
tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py Adds regression coverage for raw-ID rejection, owned managed IDs, and default-off behavior
tests/test_litellm/proxy/batches_endpoints/test_endpoints.py Adds coverage that blocked batch and input-file IDs do not reach provider seams
tests/test_litellm/proxy/fine_tuning_endpoints/test_endpoints.py Adds endpoint coverage for training, validation, and job ownership enforcement
tests/test_litellm/proxy/vector_store_files_endpoints/test_endpoints.py Adds shared-resolver coverage for raw, missing, and inaccessible managed file IDs

Reviews (3): Last reviewed commit: "fix(proxy): authenticate managed ids bef..." | Re-trigger Greptile

Comment thread litellm/proxy/openai_files_endpoints/common_utils.py Outdated
@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.60976% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...lm/proxy/vector_store_files_endpoints/endpoints.py 44.44% 5 Missing ⚠️
litellm/proxy/fine_tuning_endpoints/endpoints.py 57.14% 3 Missing ⚠️
...tellm/proxy/openai_files_endpoints/common_utils.py 89.47% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing devin_ai_require_managed_files_read_paths_35530 (8c0556a) with litellm_internal_staging (c28cbb8)1

Open in CodSpeed

Footnotes

  1. No successful run was found on litellm_internal_staging (e24a914) during the generation of this report, so c28cbb8 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@mateo-berri mateo-berri changed the title fix(files): enforce require_managed_files on file retrieve, content and delete fix(proxy): enforce require_managed_files on every route that accepts a raw provider id Aug 8, 2026
require_managed_files was only checked on upload, so raw provider ids still
reached the batch, fine-tuning and vector store file routes. Ownership rows
exist only for managed ids, so those requests were forwarded under shared
credentials with no tenant check: knowing another tenant's id was enough to
read, run against, cancel or delete their object.

Generalise the file-id guard to validate_managed_id_requirement(resource_id,
resource_kind) and call it on batch create/retrieve/cancel, fine-tuning
create/retrieve/cancel (training_file and validation_file both) and the shared
vector store file id resolver. Behaviour is unchanged when the setting is off.
@mateo-berri
mateo-berri requested a review from a team August 8, 2026 08:36
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 8c0556a. Configure here.

@mateo-berri mateo-berri left a comment

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.

LGTM

@mateo-berri
mateo-berri merged commit 1d0cba7 into litellm_internal_staging Aug 8, 2026
86 checks passed
@mateo-berri
mateo-berri deleted the devin_ai_require_managed_files_read_paths_35530 branch August 8, 2026 17:22
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.

[Security]: Raw provider file IDs bypass managed-file ownership checks

3 participants