dify - ✅(Solved) Fix plugin inner API can resolve EndUser across tenants when user_id is provided [2 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
langgenius/dify#35323Fetched 2026-04-17 08:56:06
View on GitHub
Comments
1
Participants
2
Timeline
6
Reactions
1
Author
Assignees
Timeline (top)
cross-referenced ×2assigned ×1closed ×1commented ×1

Root Cause

get_user() can resolve an EndUser from another tenant when a concrete user_id is provided, because the non-anonymous lookup path does not include tenant_id.

Fix Action

Fixed

PR fix notes

PR #35325: fix: scope plugin inner API end-user lookup by tenant

Description (problem / solution / changelog)

Summary

Restore tenant-scoped lookup semantics in controllers/inner_api/plugin/wraps.py for explicit end-user IDs.

Today the plugin inner API already documents that user_id is untrusted input, but the non-anonymous path uses session.get(EndUser, user_id), which can resolve a row from another tenant before the request context is updated.

This PR keeps the existing anonymous behavior unchanged and only narrows the non-anonymous lookup to:

  • EndUser.id == user_id
  • EndUser.tenant_id == tenant_id

If no matching row exists in the current tenant, the existing fallback path still creates a new end user for the request.

Test plan

  • Added a regression test in api/tests/unit_tests/controllers/inner_api/plugin/test_plugin_wraps.py
  • Reproduced the pre-fix behavior locally with a lightweight harness that returned a foreign-tenant EndUser
  • Verified the same harness no longer resolves the foreign row after the code change

Fixes #35323

Changed files

  • api/controllers/inner_api/plugin/wraps.py (modified, +13/-3)
  • api/tests/unit_tests/controllers/inner_api/plugin/test_plugin_wraps.py (modified, +52/-9)

PR #35336: fix(inner_api): ensure EndUser lookup is tenant-scoped for non-anonymous users

Description (problem / solution / changelog)

Fixes #35323

This PR ensures that when a user_id is provided to the plugin inner API, the lookup for the EndUser is scoped to the provided tenant_id.

Previously, non-anonymous users were looked up using session.get(EndUser, user_id), which ignores tenant boundaries. This could allow resolving an EndUser from a different tenant if their ID was known.

The fix changes the lookup to use a filtered query:

user_model = session.scalar(
    select(EndUser)
    .where(
        EndUser.id == user_id,
        EndUser.tenant_id == tenant_id,
    )
    .limit(1)
)

This aligns the non-anonymous path with the existing anonymous path and maintains strict tenant isolation.

Changed files

  • api/controllers/inner_api/plugin/wraps.py (modified, +8/-1)
RAW_BUFFERClick to expand / collapse

Self Checks

  • I have read the Contributing Guide and Language Policy.
  • This is only for bug report, if you would like to ask a question, please head to Discussions.
  • I have searched for existing issues, including closed ones.
  • I confirm that I am using English to submit this report, otherwise it will be closed.
  • Please do not modify this template :) and fill in all the required fields.

Dify version

main, also reproducible from code shipped in 1.13.3

Cloud or Self Hosted

Self Hosted (Source)

Steps to reproduce

  1. Call the plugin inner API path that goes through controllers/inner_api/plugin/wraps.py.
  2. Provide a valid tenant_id for tenant A.
  3. Provide a non-anonymous user_id that belongs to an EndUser row in tenant B.
  4. Observe how get_user() resolves the foreign row before the request context is updated.

Minimal code-path reproduction:

  • anonymous users are looked up with session_id + tenant_id
  • non-anonymous users are currently looked up with session.get(EndUser, user_id)
  • get_user_tenant() then injects that resolved user into the request context

✔️ Expected Behavior

A supplied user_id should stay tenant-scoped. If the user does not exist in the current tenant, Dify should not resolve an EndUser row from another tenant.

❌ Actual Behavior

get_user() can resolve an EndUser from another tenant when a concrete user_id is provided, because the non-anonymous lookup path does not include tenant_id.

That makes the tenant-scoping invariant inconsistent with the anonymous path and with other recent ownership hardening fixes in the codebase.

extent analysis

TL;DR

The most likely fix is to modify the get_user() function to include tenant_id in the lookup path for non-anonymous users.

Guidance

  • Verify that the issue is indeed caused by the missing tenant_id in the non-anonymous lookup path by checking the database queries executed during the reproduction steps.
  • Update the get_user() function to use session.get(EndUser, user_id, tenant_id=tenant_id) to ensure tenant-scoped lookup for non-anonymous users.
  • Review other parts of the codebase that perform user lookups to ensure consistency in tenant-scoping.
  • Test the updated get_user() function with different scenarios, including anonymous and non-anonymous users, to ensure the fix does not introduce any regressions.

Example

# updated get_user() function
def get_user(user_id, tenant_id):
    return session.get(EndUser, user_id, tenant_id=tenant_id)

Notes

The provided code snippet is a minimal example and may require adjustments to fit the actual implementation. Additionally, this fix assumes that the tenant_id is available in the context where get_user() is called.

Recommendation

Apply workaround: Update the get_user() function to include tenant_id in the lookup path for non-anonymous users, as this fix directly addresses the identified issue and ensures consistency in tenant-scoping.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

dify - ✅(Solved) Fix plugin inner API can resolve EndUser across tenants when user_id is provided [2 pull requests, 1 comments, 2 participants]