Skip to content

[Security] Webhook Trigger Authentication Bypass - CWE-287/CWE-306 #5213

@icysun

Description

@icysun

MaxKB Webhook Trigger Authentication Bypass

Vulnerability Type: Improper Authentication / Missing Authorization
Severity: High
CVSS v3.1: 7.5 (High) — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N
Affected Component: Webhook event trigger endpoint
Affected Versions: MaxKB <= latest (as of 2026-05-04, commit-based analysis)
Discoverer: icysun icysun@qq.com
Status: Pending Disclosure


Overview

MaxKB's webhook trigger endpoint at /api/trigger/v1/webhook/{trigger_id} is accessible without any authentication. The WebhookAuth authentication class unconditionally returns (None, {}), effectively disabling all authentication for this endpoint. While the application includes an optional token-based verification mechanism in EventTrigger.execute(), this mechanism is optional — it only activates when the trigger_setting.token field is present and non-empty.

The backend serializer (TriggerValidationMixin._validate_event_setting()) does not enforce that a token must be present when creating an EVENT trigger. The token field is not part of the validation schema for event triggers. Although the Vue frontend auto-generates a UUIDv4-based token when creating event triggers via the UI, the API allows creating event triggers without any token.

An attacker who knows or guesses a trigger ID can invoke any untokened webhook trigger to execute its associated tasks — including running custom Python tool code via ToolExecutor.exec_code() which calls exec() in a subprocess.


Root Cause Analysis

1. WebhookAuth — No Authentication Performed

File: apps/common/auth/authenticate.py:152-157

class WebhookAuth(TokenAuthentication):
    keyword = "Bearer"

    def authenticate(self, request):
        return None, {}

WebhookAuth.authenticate() unconditionally returns (None, {}) without checking any credentials. In Django REST Framework, returning a tuple from authenticate() signals successful authentication. This means every request to an endpoint using WebhookAuth is treated as authenticated.

2. Token Verification is Optional

File: apps/trigger/handler/impl/trigger/event_trigger.py:108-112

@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'))

The token check is gated by if trigger_setting.get('token'). If token is None, missing, or empty string (""), the entire verification block is skipped. The trigger proceeds to execute its tasks without any authorization check.

3. Backend Does Not Enforce Token Requirement

File: apps/trigger/serializers/trigger.py:241-247

@staticmethod
def _validate_event_setting(setting):
    body = setting.get('body')
    if body is not None and not isinstance(body, list):
        raise serializers.ValidationError({
            'trigger_setting': _('body must be an array')
        })

_validate_event_setting() only validates that body (if present) is an array. There is no validation requiring the token field for EVENT triggers. The token field is completely absent from this validation method.

4. Model Default Has No Token

File: apps/trigger/models/trigger.py:36

trigger_setting = models.JSONField(default=dict)

The default value for trigger_setting is an empty dictionary {}, which contains no token key.

5. Webhook Endpoint Uses WebhookAuth

File: apps/trigger/urls.py:27

path('trigger/v1/webhook/<str:trigger_id>', EventTriggerView.as_view(), name='trigger_webhook'),

File: apps/trigger/handler/impl/trigger/event_trigger.py:78

class EventTriggerView(APIView):
    authentication_classes = [WebhookAuth]

The webhook endpoint is publicly accessible (no workspace_id in URL, no permission decorators).


Attack Chain

graph TD
    A[Attacker discovers trigger ID] --> B[POST /api/trigger/v1/webhook/trigger_id]
    B --> C[WebhookAuth.authenticate returns None, empty dict]
    C --> D[trigger_setting.get token returns None]
    D --> E[Token check skipped]
    E --> F{Trigger active?}
    F -->|Yes| G[EventTrigger.execute runs tasks]
    G --> H{Task type?}
    H -->|TOOL| I[ToolTask.execute called]
    I --> J[ToolExecutor.exec_code executes user Python code]
    J --> K[exec in subprocess - code execution]
    H -->|APPLICATION| L[ApplicationTask.execute runs workflow]
    L --> M[Workflow may contain tool nodes with exec]
    F -->|No| N[Returns 404 - no impact]
Loading

Detailed Data Flow

  1. Attacker sends POST /api/trigger/v1/webhook/{trigger_id} with arbitrary JSON body
  2. Django REST Framework calls WebhookAuth.authenticate() → returns (None, {}) → request is "authenticated"
  3. EventTriggerView.post() queries the trigger by ID from the database (no workspace filtering)
  4. EventTrigger.execute() checks trigger_setting.get('token')None → skips token verification
  5. get_parameters() processes the request body according to the trigger's body configuration
  6. simple_tools.execute() iterates over active trigger tasks and dispatches them
  7. For TOOL tasks: ToolTask.execute()ToolExecutor.exec_code()exec() in a subprocess
  8. For APPLICATION tasks: ApplicationTask.execute() → runs the application workflow, which may contain tool nodes that call exec()

Key Enabling Factors

Factor Status
Webhook endpoint has no authentication Confirmed
Token verification is optional in trigger execution Confirmed
Backend does not require token for EVENT triggers Confirmed
Tool code execution via exec() is reachable from triggers Confirmed
No rate limiting on webhook endpoint Confirmed
Trigger IDs are UUIDv7 (time-ordered, not cryptographically random) Confirmed
Webhook URL contains no workspace identifier Confirmed

Impact Assessment

Direct Impact

  1. Unauthorized Trigger Invocation: Any unauthenticated user can invoke webhook triggers, causing them to execute their configured tasks (tool executions, application workflows).

  2. Denial of Service: Repeated invocation of triggers with expensive operations (LLM calls, tool executions) can exhaust API credits, consume server resources, and degrade service availability.

  3. Information Disclosure: If a trigger task involves an application workflow, the attacker can interact with the knowledge base and obtain AI-generated responses that may contain sensitive information.

  4. Code Execution (conditional): If a trigger is bound to a custom Python tool, the attacker can trigger execution of that tool's code via ToolExecutor.exec_code(). While this executes in a subprocess with optional sandbox restrictions, the code runs with the server's user privileges (sandbox is disabled by defaultSANDBOX config defaults to 0).

Preconditions

  • The attacker must know or discover a valid trigger ID (UUIDv7, ~122 bits entropy — not trivially brute-forceable)
  • The trigger must be of type EVENT
  • The trigger must be active (is_active = True)
  • The trigger's trigger_setting must lack a token field (possible via API-only creation or migration)

Attack Scenarios

Scenario A: API-Only Trigger Creation (Most Likely)

  • A user or integration creates a trigger via the REST API without including a token in trigger_setting
  • The trigger is active and bound to a tool or application
  • Anyone who obtains the trigger ID can invoke it

Scenario B: Trigger ID Leakage

  • Trigger IDs may be exposed in logs, error messages, API responses, or shared documentation
  • Once an ID is known, the webhook can be invoked without authentication
  • UUIDv7 is time-ordered; knowing the approximate creation time narrows the search space

Scenario C: Legacy/Migrated Triggers

  • Triggers created before the token feature was introduced may not have a token
  • Migrated systems may have active triggers without token protection

Proof of Concept

See: poc_maxkb_webhook_auth_bypass.py

Usage

# 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

# Enumerate triggers (requires leaked IDs or significant time)
python3 poc_maxkb_webhook_auth_bypass.py \
  --target http://maxkb-host:8080 \
  --enumerate --count 100

# Invoke a trigger with custom payload
python3 poc_maxkb_webhook_auth_bypass.py \
  --target http://maxkb-host:8080 \
  --trigger-id <uuid> \
  --send-payload '{"question": "What is the admin password?"}'

Expected Vulnerable Response

HTTP 200
{
  "code": 200,
  "data": true,
  "message": "success"
}

Expected Protected Response (Token Present)

HTTP 200
{
  "code": 1002,
  "data": null,
  "message": "Authentication information is incorrect"
}

CVSS v3.1 Scoring

Metric Value Rationale
Attack Vector Network (N) Exploitable over the network via HTTP POST
Attack Complexity Low (L) No special conditions; simple HTTP request
Privileges Required None (N) No authentication required
User Interaction None (N) No victim interaction needed
Scope Unchanged (U) Impact contained within the MaxKB application
Confidentiality None (N) No direct data exfiltration (indirect via LLM responses)
Integrity High (H) Can execute arbitrary tool code, modify application state
Availability None (N) DoS is possible but not the primary impact

Base Score: 7.5 (High)CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N

Note on Integrity: If the sandbox is disabled (default), ToolExecutor.exec_code() runs user Python code with server-level privileges, enabling file manipulation, network access, and lateral movement within the infrastructure. Even with sandbox enabled, the tool can still perform actions within its permitted scope (e.g., LLM API calls, database operations via the application workflow).

Adjusted Score (with sandbox disabled): 9.8 (Critical) — If code execution is reachable, this becomes a full RCE. However, we rate conservatively at 7.5 because: (a) trigger ID discovery is non-trivial, (b) the default frontend generates tokens, and (c) sandbox may be enabled in production deployments.


Fix Recommendations

1. Make Token Mandatory for EVENT Triggers (Critical)

File: apps/trigger/serializers/trigger.py_validate_event_setting()

@staticmethod
def _validate_event_setting(setting):
    body = setting.get('body')
    if body is not None and not isinstance(body, list):
        raise serializers.ValidationError({
            'trigger_setting': _('body must be an array')
        })
    # NEW: Require token for EVENT triggers
    if not setting.get('token'):
        raise serializers.ValidationError({
            'trigger_setting': _('token is required for EVENT triggers')
        })
    if len(str(setting.get('token', ''))) < 16:
        raise serializers.ValidationError({
            'trigger_setting': _('token must be at least 16 characters')
        })

2. Migrate Existing Triggers Without Tokens

Add a data migration that generates tokens for all existing EVENT triggers that lack one:

# Generated token should be communicated to the trigger owner
from django.db import migrations
import uuid

def generate_tokens(apps, schema_editor):
    Trigger = apps.get_model('trigger', 'Trigger')
    for trigger in Trigger.objects.filter(trigger_type='EVENT'):
        setting = trigger.trigger_setting or {}
        if not setting.get('token'):
            setting['token'] = uuid.uuid4().hex
            trigger.trigger_setting = setting
            trigger.save(update_fields=['trigger_setting'])

3. Remove WebhookAuth or Make It Functional

Either remove WebhookAuth entirely and rely solely on per-trigger token verification, or implement proper authentication:

class WebhookAuth(TokenAuthentication):
    keyword = "Bearer"

    def authenticate(self, request):
        # Return None (not authenticated) by default
        # The view will still handle token verification per-trigger
        return None

Important: In DRF, returning None from authenticate() means "no authentication attempted" — the request proceeds with request.user = None and request.auth = None. The current code returns (None, {}) which is incorrect — it signals successful authentication. This should be changed to return None (not a tuple).

4. Add Rate Limiting

Add DRF throttle classes to the webhook endpoint:

from rest_framework.throttling import AnonRateThrottle

class EventTriggerView(APIView):
    authentication_classes = [WebhookAuth]
    throttle_classes = [AnonRateThrottle]

5. Add Workspace Isolation (Defense in Depth)

Consider adding workspace validation to the webhook trigger execution to prevent cross-workspace trigger invocation:

# In EventTrigger.execute():
if request and hasattr(request, 'workspace_id'):
    if trigger.get('workspace_id') != request.workspace_id:
        raise AppAuthenticationFailed(1002, 'Invalid trigger for this workspace')

Acknowledgments


CVE Request

This vulnerability is being submitted for CVE assignment through the relevant CNA. Key details for the request:

  • Product: MaxKB
  • Vendor: 1Panel-dev (FIT2CLOUD)
  • Component: Webhook event trigger authentication
  • Vulnerability Type: CWE-287 (Improper Authentication) / CWE-306 (Missing Authentication for Critical Function)
  • Attack Vector: Network
  • Impact: Unauthorized code execution (conditional), unauthorized application workflow invocation, denial of service

AI Cross-Review Notes

Verification Results

Question Finding
WebhookAuth does no authentication? Confirmed. Returns (None, {}) which DRF interprets as successful auth. No middleware supplements this.
Trigger ID predictable? Partially. UUIDv7 is time-ordered (~48 bits timestamp), but has ~122 bits total entropy. Not brute-forceable without leaked IDs.
Can exec() be reached from webhook? Confirmed. ToolTask.execute() → ToolExecutor.exec_code() → exec() in subprocess. Sandbox is disabled by default.
Token optional by default? Confirmed. Backend _validate_event_setting() does not require token. Model default is empty dict. Frontend auto-generates token but API does not enforce it.
Can normal users create triggers? Restricted. Requires WORKSPACE_MANAGE role (admin-level). But any workspace admin can create untokened triggers.
Rate limiting? None. No throttle classes on the webhook endpoint.
Design choice or vulnerability? Vulnerability. The intent is clearly optional auth (token in trigger_setting), but making it optional AND having the backend not enforce it AND having the auth class return "authenticated" creates a bypass.

Key Mitigating Factor

The primary mitigating factor is trigger ID discovery. UUIDv7 provides ~122 bits of entropy, making blind enumeration infeasible. However:

  1. Trigger IDs may be leaked via logs, shared URLs, error messages, or API responses
  2. The trigger_task API returns trigger IDs to authenticated users
  3. UUIDv7's time-ordering means knowing the creation time reduces entropy
  4. Third-party integrations may expose trigger IDs in their configuration

Developer Likely Pushback Points

  1. "The frontend always generates a token" — True for UI-created triggers, but the API does not enforce this
  2. "Trigger IDs are UUIDs, not guessable" — True for blind attacks, but IDs can be leaked
  3. "This is by design for webhook integrations" — Valid for the webhook pattern, but should require token by default
  4. "Sandbox limits code execution impact" — Sandbox is disabled by default (SANDBOX=0)

Three-Question Filter

  1. Can this be triggered by an unauthenticated remote attacker? Yes — WebhookAuth returns authenticated for all requests, and token verification is optional.
  2. Is there a complete attack chain from network input to impactful action? Yes — webhook → no auth → trigger execution → tool code exec (conditional on trigger configuration).
  3. Is this a known issue or configuration-dependent? This is a code-level vulnerability, not a configuration issue. The code explicitly makes authentication optional.

Verdict: Confirmed vulnerability. Recommended for disclosure with severity High.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions