VYPR
High severity7.5NVD Advisory· Published May 26, 2026

CVE-2026-44847

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

Patches

5
4f842256adc2

fix: Trigger create allow no token

https://github.com/1Panel-dev/MaxKBzhangzhanweiMay 6, 2026Fixed in 2.9.0via llm-release-walk
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):
    
ed501c3534b5

fix: improve Redis lock handling in ScheduledTrigger class

https://github.com/1Panel-dev/MaxKBCaptainBApr 29, 2026Fixed in 2.9.0via llm-release-walk
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"
    
6543094400ab

fix: implement RedisLock for task execution to prevent concurrent processing

https://github.com/1Panel-dev/MaxKBCaptainBApr 29, 2026Fixed in 2.9.0via llm-release-walk
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"
    
d5f7b3f7ffd0

fix: refactor trigger task execution to use instance methods

https://github.com/1Panel-dev/MaxKBCaptainBApr 24, 2026Fixed in 2.9.0via llm-release-walk
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']}")
     
    
e0fe160afb4f

fix: update API base URL for AliyunBaiLianReranker

https://github.com/1Panel-dev/MaxKBwxg0103May 7, 2026Fixed in 2.9.0via release-tag
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

2

News mentions

0

No linked articles in our index yet.