CVE-2026-44847
Description
MaxKB is an open-source AI assistant for enterprise. Prior to 2.9.0, MaxKB's webhook trigger endpoint (/api/trigger/v1/webhook/{trigger_id}) is accessible without authentication. The WebhookAuth class unconditionally returns (None, {}), which Django REST Framework interprets as successful authentication. Combined with optional per-trigger token verification and no backend enforcement of token requirements, any unauthenticated attacker who knows a valid trigger ID can invoke webhook triggers to execute their bound tasks. This vulnerability is fixed in 2.9.0.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
MaxKB webhook trigger endpoint lacks authentication, allowing unauthenticated attackers to invoke triggers and potentially execute arbitrary code.
Vulnerability
MaxKB prior to version 2.9.0 contains an authentication bypass in its webhook trigger endpoint at /api/trigger/v1/webhook/{trigger_id}. The WebhookAuth authentication class unconditionally returns (None, {}), which Django REST Framework interprets as successful authentication [1][2]. Token verification is optional and only enforced when the trigger_setting.token field is present and non-empty. The backend serializer does not require a token for EVENT triggers, allowing creation of triggers without any token [1]. Affected versions: all MaxKB releases before 2.9.0.
Exploitation
An unauthenticated attacker who knows or guesses a valid trigger ID can send a request to the webhook endpoint to invoke the trigger. No authentication or prior access is required. The attacker can enumerate trigger IDs or obtain them through other means. Once a trigger ID is known, the attacker can trigger execution of any bound tasks, including custom Python tool code via ToolExecutor.exec_code() which calls exec() in a subprocess [1][2].
Impact
Successful exploitation allows unauthorized invocation of webhook triggers. If the trigger is bound to a custom Python tool, the attacker can achieve remote code execution (RCE) because the sandbox is disabled by default (SANDBOX=0) [2]. Additionally, repeated invocation can cause denial of service by exhausting API credits or degrading availability. Information disclosure is possible through unauthenticated interaction with the knowledge base [2].
Mitigation
The vulnerability is fixed in MaxKB version 2.9.0 [2]. The fix enforces token as a required field for all EVENT triggers at the serializer level, corrects WebhookAuth.authenticate() to return None instead of (None, {}), and includes a data migration to auto-generate tokens for existing untokened triggers [2]. Users should upgrade to v2.9.0 or later. If immediate upgrade is not possible, workarounds include adding a token to all existing EVENT triggers via the admin UI or database, and enabling the sandbox by setting SANDBOX=1 in the environment configuration [2].
AI Insight generated on May 26, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1- Range: <2.9.0
Patches
54f842256adc2fix: Trigger create allow no token
5 files changed · +21 −6
apps/locales/en_US/LC_MESSAGES/django.po+3 −0 modified@@ -9249,4 +9249,7 @@ msgid "The label field is required for the {index}th item in model_params_form" msgstr "" msgid "Publish" +msgstr "" + +msgid "token is required for EVENT triggers" msgstr "" \ No newline at end of file
apps/locales/zh_CN/LC_MESSAGES/django.po+4 −1 modified@@ -9372,4 +9372,7 @@ msgid "The label field is required for the {index}th item in model_params_form" msgstr "model_params_form 中的第 {index} 项的 label 字段是必填项" msgid "Publish" -msgstr "发布" \ No newline at end of file +msgstr "发布" + +msgid "token is required for EVENT triggers" +msgstr "事件触发器必须设置 token" \ No newline at end of file
apps/locales/zh_Hant/LC_MESSAGES/django.po+4 −1 modified@@ -9369,4 +9369,7 @@ msgid "The label field is required for the {index}th item in model_params_form" msgstr "model_params_form 中的第 {index} 项的 label 字段是必填项" msgid "Publish" -msgstr "發布" \ No newline at end of file +msgstr "發布" + +msgid "token is required for EVENT triggers" +msgstr "事件觸發器必須設定 token" \ No newline at end of file
apps/trigger/handler/impl/trigger/event_trigger.py+6 −4 modified@@ -117,10 +117,12 @@ class EventTrigger(BaseTrigger): @staticmethod def execute(trigger, request=None, **kwargs): trigger_setting = trigger.get('trigger_setting') - if trigger_setting.get('token'): - token = request.META.get('HTTP_AUTHORIZATION') - if not token or trigger_setting.get('token') != token.replace('Bearer ', ''): - raise AppAuthenticationFailed(1002, _('Authentication information is incorrect')) + token = trigger_setting.get('token') + if not token: + raise AppAuthenticationFailed(1002, _('Authentication information is incorrect')) + request_token = request.META.get('HTTP_AUTHORIZATION') + if not request_token or token != request_token.replace('Bearer ', ''): + raise AppAuthenticationFailed(1002, _('Authentication information is incorrect')) is_active = trigger.get('is_active') if not is_active: return Result(code=404, message="404", response_status=404)
apps/trigger/serializers/trigger.py+4 −0 modified@@ -243,6 +243,10 @@ def _validate_event_setting(setting): raise serializers.ValidationError({ 'trigger_setting': _('body must be an array') }) + if not setting.get('token'): + raise serializers.ValidationError({ + 'trigger_setting': _('token is required for EVENT triggers') + }) class TriggerTaskCreateRequest(serializers.Serializer):
ed501c3534b5fix: improve Redis lock handling in ScheduledTrigger class
1 file changed · +3 −2
apps/trigger/handler/impl/trigger/scheduled_trigger.py+3 −2 modified@@ -242,8 +242,9 @@ def execute(trigger, **kwargs): return source_type = trigger_task["source_type"] rlock = RedisLock() + trigger_id = str(trigger_task.get('trigger')) source_id = str(trigger_task["source_id"]) - if rlock.try_lock(source_id, 30 * 30): + if rlock.try_lock(f'{trigger_id}:{source_id}', 30 * 30): try: if source_type == "APPLICATION": from trigger.handler.impl.task.application_task import ApplicationTask @@ -256,7 +257,7 @@ def execute(trigger, **kwargs): else: maxkb_logger.warning(f"unsupported source_type={source_type}, task_id={trigger_task['id']}") finally: - rlock.un_lock(source_id) + rlock.un_lock(f'{trigger_id}:{source_id}') def support(self, trigger, **kwargs): return trigger.get("trigger_type") == "SCHEDULED"
6543094400abfix: implement RedisLock for task execution to prevent concurrent processing
1 file changed · +18 −11
apps/trigger/handler/impl/trigger/scheduled_trigger.py+18 −11 modified@@ -1,6 +1,8 @@ # coding=utf-8 +from celery_once import QueueOnce from django.db.models import QuerySet +from common.utils.lock import RedisLock from common.utils.logger import maxkb_logger from ops import celery_app from trigger.handler.base_trigger import BaseTrigger @@ -239,17 +241,22 @@ def execute(trigger, **kwargs): maxkb_logger.warning(f"unsupported task={trigger_task}") return source_type = trigger_task["source_type"] - - if source_type == "APPLICATION": - from trigger.handler.impl.task.application_task import ApplicationTask - - ApplicationTask().execute(trigger_task, **kwargs) - elif source_type == "TOOL": - from trigger.handler.impl.task.tool_task import ToolTask - - ToolTask().execute(trigger_task, **kwargs) - else: - maxkb_logger.warning(f"unsupported source_type={source_type}, task_id={trigger_task['id']}") + rlock = RedisLock() + source_id = str(trigger_task["source_id"]) + if rlock.try_lock(source_id, 30 * 30): + try: + if source_type == "APPLICATION": + from trigger.handler.impl.task.application_task import ApplicationTask + + ApplicationTask().execute(trigger_task, **kwargs) + elif source_type == "TOOL": + from trigger.handler.impl.task.tool_task import ToolTask + + ToolTask().execute(trigger_task, **kwargs) + else: + maxkb_logger.warning(f"unsupported source_type={source_type}, task_id={trigger_task['id']}") + finally: + rlock.un_lock(source_id) def support(self, trigger, **kwargs): return trigger.get("trigger_type") == "SCHEDULED"
d5f7b3f7ffd0fix: refactor trigger task execution to use instance methods
1 file changed · +3 −3
apps/trigger/handler/impl/trigger/scheduled_trigger.py+3 −3 modified@@ -234,7 +234,7 @@ class ScheduledTrigger(BaseTrigger): @staticmethod def execute(trigger, **kwargs): - trigger_task = kwargs.get("trigger_task") + trigger_task = kwargs.pop("trigger_task", None) if not trigger_task: maxkb_logger.warning(f"unsupported task={trigger_task}") return @@ -243,11 +243,11 @@ def execute(trigger, **kwargs): if source_type == "APPLICATION": from trigger.handler.impl.task.application_task import ApplicationTask - ApplicationTask.execute(trigger_task, **kwargs) + ApplicationTask().execute(trigger_task, **kwargs) elif source_type == "TOOL": from trigger.handler.impl.task.tool_task import ToolTask - ToolTask.execute(trigger_task, **kwargs) + ToolTask().execute(trigger_task, **kwargs) else: maxkb_logger.warning(f"unsupported source_type={source_type}, task_id={trigger_task['id']}")
e0fe160afb4ffix: update API base URL for AliyunBaiLianReranker
2 files changed · +2 −2
apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/reranker.py+1 −1 modified@@ -19,7 +19,7 @@ class AliyunBaiLianRerankerCredential(BaseForm, BaseModelCredential): Provides validation and encryption for the model credentials. """ api_base = forms.TextInputField(_('API URL'), required=True, - default_value='https://dashscope.aliyuncs.com/compatible-mode/v1') + default_value='https://dashscope.aliyuncs.com/api/v1') dashscope_api_key = PasswordInputField('API Key', required=True) def is_valid(
apps/models_provider/impl/aliyun_bai_lian_model_provider/model/reranker.py+1 −1 modified@@ -32,7 +32,7 @@ def is_cache_model(): def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): return AliyunBaiLianReranker(model=model_name, api_key=model_credential.get('dashscope_api_key'), - base_url=model_credential.get('api_base') or 'https://dashscope.aliyuncs.com/compatible-mode/v1', + base_url=model_credential.get('api_base') or 'https://dashscope.aliyuncs.com/api/v1', top_n=model_kwargs.get('top_n', 3)) def compress_documents(self, documents: Sequence[Document], query: str, callbacks: Optional[Callbacks] = None) -> \
Vulnerability mechanics
Root cause
"The `WebhookAuth` authentication class unconditionally returns `(None, {})` (interpreted as successful authentication by DRF), and the backend does not enforce that a token must be present for EVENT triggers, making token verification optional and bypassable."
Attack vector
An unauthenticated attacker sends a POST request to `/api/trigger/v1/webhook/{trigger_id}` with an arbitrary JSON body [ref_id=1]. `WebhookAuth.authenticate()` returns `(None, {})`, which DRF treats as successful authentication, so no credentials are checked [ref_id=1]. The `EventTrigger.execute()` method reads `trigger_setting.get('token')` — if the trigger was created without a token (possible via the API), this returns a falsy value and the entire token-verification block is skipped [ref_id=1]. The trigger then executes its bound tasks, which can include running custom Python tool code via `ToolExecutor.exec_code()` (which calls `exec()` in a subprocess) or running application workflows [ref_id=1]. The only precondition is knowledge of a valid, active EVENT trigger ID [ref_id=1].
Affected code
The webhook endpoint at `/api/trigger/v1/webhook/{trigger_id}` uses `WebhookAuth` in `apps/common/auth/authenticate.py`, which unconditionally returns `(None, {})` — Django REST Framework interprets this as successful authentication [ref_id=1]. The `EventTrigger.execute()` method in `apps/trigger/handler/impl/trigger/event_trigger.py` gates token verification on `if trigger_setting.get('token')`, so if the token field is absent or empty the check is skipped entirely [ref_id=1]. The backend serializer `_validate_event_setting()` in `apps/trigger/serializers/trigger.py` does not require a token field when creating EVENT triggers [ref_id=1].
What the fix does
Patch `2590849` makes token mandatory for EVENT triggers at two enforcement points: in the serializer `_validate_event_setting()` it now raises a validation error if `setting.get('token')` is falsy, and in `EventTrigger.execute()` it inverts the logic — if `token` is empty the method immediately raises an authentication error before reaching the comparison with the request's `Authorization` header [patch_id=2590849]. This closes the bypass by ensuring that every EVENT trigger must have a non-empty token and that the token is always verified at execution time. Patches `2590850` and `2590851` add Redis-lock-based concurrency protection to `ScheduledTrigger.execute()`, preventing duplicate task execution in clustered deployments but do not address the authentication bypass [patch_id=2590850][patch_id=2590851].
Preconditions
- inputAttacker must know or discover a valid EVENT trigger ID (UUIDv7, ~122 bits entropy — not trivially brute-forceable but may be leaked in logs, API responses, or shared URLs)
- configThe trigger must be of type EVENT and active (is_active = True)
- configThe trigger's trigger_setting must lack a token field (possible when creating triggers via the REST API without including a token)
- authNo authentication required — the endpoint is accessible over the network without any credentials
- configNo rate limiting is applied to the webhook endpoint
Reproduction
The reference write-up includes a proof-of-concept script (`poc_maxkb_webhook_auth_bypass.py`) with the following usage [ref_id=1]:
``` # Check if a specific trigger is vulnerable python3 poc_maxkb_webhook_auth_bypass.py \ --target http://maxkb-host:8080 \ --trigger-id 01950a3a-7b2c-7000-8000-000000000001
# Invoke a trigger with custom payload python3 poc_maxkb_webhook_auth_bypass.py \ --target http://maxkb-host:8080 \ --trigger-id
Generated on May 26, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
2News mentions
0No linked articles in our index yet.