mirror of
https://github.com/langgenius/dify.git
synced 2026-01-08 07:14:14 +00:00
security(api): fix privilege escalation vulnerability in model config and chat message APIs (#25518)
The `ChatMessageApi` (`POST /console/api/apps/{app_id}/chat-messages`) and
`ModelConfigResource` (`POST /console/api/apps/{app_id}/model-config`)
endpoints do not properly validate user permissions, allowing users without `editor`
permission to access restricted functionality.
This PR addresses this issue by adding proper permission check.
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
"""Integration tests for ChatMessageApi permission verification."""
|
||||
|
||||
import uuid
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from flask.testing import FlaskClient
|
||||
|
||||
from controllers.console.app import completion as completion_api
|
||||
from controllers.console.app import wraps
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from models import Account, App, Tenant
|
||||
from models.account import TenantAccountRole
|
||||
from models.model import AppMode
|
||||
from services.app_generate_service import AppGenerateService
|
||||
|
||||
|
||||
class TestChatMessageApiPermissions:
|
||||
"""Test permission verification for ChatMessageApi endpoint."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_app_model(self):
|
||||
"""Create a mock App model for testing."""
|
||||
app = App()
|
||||
app.id = str(uuid.uuid4())
|
||||
app.mode = AppMode.CHAT.value
|
||||
app.tenant_id = str(uuid.uuid4())
|
||||
app.status = "normal"
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def mock_account(self):
|
||||
"""Create a mock Account for testing."""
|
||||
|
||||
account = Account()
|
||||
account.id = str(uuid.uuid4())
|
||||
account.name = "Test User"
|
||||
account.email = "test@example.com"
|
||||
account.last_active_at = naive_utc_now()
|
||||
account.created_at = naive_utc_now()
|
||||
account.updated_at = naive_utc_now()
|
||||
|
||||
# Create mock tenant
|
||||
tenant = Tenant()
|
||||
tenant.id = str(uuid.uuid4())
|
||||
tenant.name = "Test Tenant"
|
||||
|
||||
account._current_tenant = tenant
|
||||
return account
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("role", "status"),
|
||||
[
|
||||
(TenantAccountRole.OWNER, 200),
|
||||
(TenantAccountRole.ADMIN, 200),
|
||||
(TenantAccountRole.EDITOR, 200),
|
||||
(TenantAccountRole.NORMAL, 403),
|
||||
(TenantAccountRole.DATASET_OPERATOR, 403),
|
||||
],
|
||||
)
|
||||
def test_post_with_owner_role_succeeds(
|
||||
self,
|
||||
test_client: FlaskClient,
|
||||
auth_header,
|
||||
monkeypatch,
|
||||
mock_app_model,
|
||||
mock_account,
|
||||
role: TenantAccountRole,
|
||||
status: int,
|
||||
):
|
||||
"""Test that OWNER role can access chat-messages endpoint."""
|
||||
|
||||
"""Setup common mocks for testing."""
|
||||
# Mock app loading
|
||||
|
||||
mock_load_app_model = mock.Mock(return_value=mock_app_model)
|
||||
monkeypatch.setattr(wraps, "_load_app_model", mock_load_app_model)
|
||||
|
||||
# Mock current user
|
||||
monkeypatch.setattr(completion_api, "current_user", mock_account)
|
||||
|
||||
mock_generate = mock.Mock(return_value={"message": "Test response"})
|
||||
monkeypatch.setattr(AppGenerateService, "generate", mock_generate)
|
||||
|
||||
# Set user role to OWNER
|
||||
mock_account.role = role
|
||||
|
||||
response = test_client.post(
|
||||
f"/console/api/apps/{mock_app_model.id}/chat-messages",
|
||||
headers=auth_header,
|
||||
json={
|
||||
"inputs": {},
|
||||
"query": "Hello, world!",
|
||||
"model_config": {
|
||||
"model": {"provider": "openai", "name": "gpt-4", "mode": "chat", "completion_params": {}}
|
||||
},
|
||||
"response_mode": "blocking",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == status
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Integration tests for ModelConfigResource permission verification."""
|
||||
|
||||
import uuid
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from flask.testing import FlaskClient
|
||||
|
||||
from controllers.console.app import model_config as model_config_api
|
||||
from controllers.console.app import wraps
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from models import Account, App, Tenant
|
||||
from models.account import TenantAccountRole
|
||||
from models.model import AppMode
|
||||
from services.app_model_config_service import AppModelConfigService
|
||||
|
||||
|
||||
class TestModelConfigResourcePermissions:
|
||||
"""Test permission verification for ModelConfigResource endpoint."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_app_model(self):
|
||||
"""Create a mock App model for testing."""
|
||||
app = App()
|
||||
app.id = str(uuid.uuid4())
|
||||
app.mode = AppMode.CHAT.value
|
||||
app.tenant_id = str(uuid.uuid4())
|
||||
app.status = "normal"
|
||||
app.app_model_config_id = str(uuid.uuid4())
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def mock_account(self):
|
||||
"""Create a mock Account for testing."""
|
||||
|
||||
account = Account()
|
||||
account.id = str(uuid.uuid4())
|
||||
account.name = "Test User"
|
||||
account.email = "test@example.com"
|
||||
account.last_active_at = naive_utc_now()
|
||||
account.created_at = naive_utc_now()
|
||||
account.updated_at = naive_utc_now()
|
||||
|
||||
# Create mock tenant
|
||||
tenant = Tenant()
|
||||
tenant.id = str(uuid.uuid4())
|
||||
tenant.name = "Test Tenant"
|
||||
|
||||
account._current_tenant = tenant
|
||||
return account
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("role", "status"),
|
||||
[
|
||||
(TenantAccountRole.OWNER, 200),
|
||||
(TenantAccountRole.ADMIN, 200),
|
||||
(TenantAccountRole.EDITOR, 200),
|
||||
(TenantAccountRole.NORMAL, 403),
|
||||
(TenantAccountRole.DATASET_OPERATOR, 403),
|
||||
],
|
||||
)
|
||||
def test_post_with_owner_role_succeeds(
|
||||
self,
|
||||
test_client: FlaskClient,
|
||||
auth_header,
|
||||
monkeypatch,
|
||||
mock_app_model,
|
||||
mock_account,
|
||||
role: TenantAccountRole,
|
||||
status: int,
|
||||
):
|
||||
"""Test that OWNER role can access model-config endpoint."""
|
||||
# Set user role to OWNER
|
||||
mock_account.role = role
|
||||
|
||||
# Mock app loading
|
||||
mock_load_app_model = mock.Mock(return_value=mock_app_model)
|
||||
monkeypatch.setattr(wraps, "_load_app_model", mock_load_app_model)
|
||||
|
||||
# Mock current user
|
||||
monkeypatch.setattr(model_config_api, "current_user", mock_account)
|
||||
|
||||
# Mock AccountService.load_user to prevent authentication issues
|
||||
from services.account_service import AccountService
|
||||
|
||||
mock_load_user = mock.Mock(return_value=mock_account)
|
||||
monkeypatch.setattr(AccountService, "load_user", mock_load_user)
|
||||
|
||||
mock_validate_config = mock.Mock(
|
||||
return_value={
|
||||
"model": {"provider": "openai", "name": "gpt-4", "mode": "chat", "completion_params": {}},
|
||||
"pre_prompt": "You are a helpful assistant.",
|
||||
"user_input_form": [],
|
||||
"dataset_query_variable": "",
|
||||
"agent_mode": {"enabled": False, "tools": []},
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr(AppModelConfigService, "validate_configuration", mock_validate_config)
|
||||
|
||||
# Mock database operations
|
||||
mock_db_session = mock.Mock()
|
||||
mock_db_session.add = mock.Mock()
|
||||
mock_db_session.flush = mock.Mock()
|
||||
mock_db_session.commit = mock.Mock()
|
||||
monkeypatch.setattr(model_config_api.db, "session", mock_db_session)
|
||||
|
||||
# Mock app_model_config_was_updated event
|
||||
mock_event = mock.Mock()
|
||||
mock_event.send = mock.Mock()
|
||||
monkeypatch.setattr(model_config_api, "app_model_config_was_updated", mock_event)
|
||||
|
||||
response = test_client.post(
|
||||
f"/console/api/apps/{mock_app_model.id}/model-config",
|
||||
headers=auth_header,
|
||||
json={
|
||||
"model": {
|
||||
"provider": "openai",
|
||||
"name": "gpt-4",
|
||||
"mode": "chat",
|
||||
"completion_params": {"temperature": 0.7, "max_tokens": 1000},
|
||||
},
|
||||
"user_input_form": [],
|
||||
"dataset_query_variable": "",
|
||||
"pre_prompt": "You are a helpful assistant.",
|
||||
"agent_mode": {"enabled": False, "tools": []},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == status
|
||||
@@ -962,7 +962,8 @@ class TestAccountService:
|
||||
Test getting user through non-existent email.
|
||||
"""
|
||||
fake = Faker()
|
||||
non_existent_email = fake.email()
|
||||
domain = f"test-{fake.random_letters(10)}.com"
|
||||
non_existent_email = fake.email(domain=domain)
|
||||
found_user = AccountService.get_user_through_email(non_existent_email)
|
||||
assert found_user is None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user