VYPR
Medium severityNVD Advisory· Published May 26, 2026

CVE-2026-45412

CVE-2026-45412

Description

MaxKB is an open-source AI assistant for enterprise. Prior to 2.9.1, SSRF via work_flow_template Import. Authenticated users can supply arbitrary URLs in work_flow_template.downloadUrl which are fetched server-side without any URL validation or internal IP filtering. This vulnerability is fixed in 2.9.1.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Authenticated SSRF in MaxKB <2.9.1 allows server-side fetch of arbitrary URLs via work_flow_template import, enabling internal network reconnaissance.

Vulnerability

MaxKB versions prior to 2.9.1 contain a server-side request forgery (SSRF) vulnerability in the work_flow_template import functionality. The endpoint at apps/application/serializers/application.py (lines 524-526) directly uses the user-supplied downloadUrl parameter from the request payload without any validation: download_url = work_flow_template.get('downloadUrl'); res = requests.get(download_url, timeout=5). No blacklist exists for private IP ranges (RFC1918, link-local, cloud metadata). This affects all deployments where authenticated users can create or update applications. [1]

Exploitation

An authenticated user crafts a request to create or update an application, setting the downloadUrl field in the work_flow_template to an arbitrary URL, such as a cloud metadata endpoint (http://169.254.169.254/latest/meta-data/) or any internal network address. The server then fetches this URL and processes the response, allowing the attacker to probe internal services or retrieve sensitive metadata. No special network position or user interaction beyond authentication is required. [1]

Impact

Successful exploitation enables the attacker to perform internal network reconnaissance, potentially accessing cloud instance metadata (e.g., AWS, GCP), internal APIs, or other services behind the firewall. The CVSS v4.0 score is 6.9 (Medium), with the vector: AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:N/VA:N/SC:H/SI:N/SA:N, indicating low confidentiality impact to the primary system but high confidentiality impact to the scope (internal network). No direct code execution or file write is described. [1]

Mitigation

The vulnerability is fixed in MaxKB version 2.9.1, released on or before the publication date of 2026-05-26. All users should upgrade to 2.9.1 or later. For deployments that cannot immediately upgrade, no official workaround is documented. The vendor has no EOL notice for older versions. This CVE is not listed in CISA's Known Exploited Vulnerabilities catalog. [1]

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
2344623ca4f4

fix: validate and replace hostname with resolved IP in request URL

https://github.com/1Panel-dev/MaxKBwxg0103May 14, 2026Fixed in 2.9.1via llm-release-walk
1 file changed · +16 8
  • apps/oss/serializers/file.py+16 8 modified
    @@ -176,14 +176,22 @@ class SafeHTTPAdapter(HTTPAdapter):
         """
     
         def send(self, request, **kwargs):
    -        # 解析 URL 获取主机名
    -        parsed_url = urlparse(request.url)
    -        host = parsed_url.hostname
    -
    -        if host:
    -            # 验证目标 IP 是否安全
    -            self._validate_host_ip(host)
    -
    +        parsed = urlparse(request.url)
    +        host = parsed.hostname
    +        port = parsed.port or (443 if parsed.scheme == 'https' else 80)
    +
    +        # Resolve ONCE
    +        addr_infos = socket.getaddrinfo(host, port, socket.AF_INET, socket.SOCK_STREAM)
    +        validated_ip = None
    +        for info in addr_infos:
    +            ip = info[4][0]
    +            if self._is_unsafe_ip(ip):
    +                raise ValueError(f"Blocked: {ip}")
    +            validated_ip = ip
    +
    +        # PIN: replace hostname with validated IP in the URL
    +        request.url = request.url.replace(f"//{host}", f"//{validated_ip}", 1)
    +        request.headers['Host'] = host  # Preserve Host header for virtual hosting
             return super().send(request, **kwargs)
     
         def _validate_host_ip(self, host: str):
    
b9013dcef817

fix: validate download URLs to ensure they start with the correct domain

https://github.com/1Panel-dev/MaxKBCaptainBMay 11, 2026Fixed in 2.9.1via llm-release-walk
4 files changed · +14 0
  • apps/application/serializers/application.py+4 0 modified
    @@ -522,6 +522,8 @@ def insert_template_workflow(self, instance: Dict):
             self.is_valid(raise_exception=True)
             work_flow_template = instance.get('work_flow_template')
             download_url = work_flow_template.get('downloadUrl')
    +        if not download_url.startswith('https://apps-assets.fit2cloud.com/'):
    +            raise AppApiException(500, _("Illegal download url"))
             # 查找匹配的版本名称
             res = requests.get(download_url, timeout=5)
             app = ApplicationSerializer(
    @@ -1142,6 +1144,8 @@ def update_template_workflow(self, instance: Dict, app: Application):
             self.is_valid(raise_exception=True)
             work_flow_template = instance.get('work_flow_template')
             download_url = work_flow_template.get('downloadUrl')
    +        if not download_url.startswith('https://apps-assets.fit2cloud.com/'):
    +            raise AppApiException(500, _("Illegal download url"))
             # 查找匹配的版本名称
             res = requests.get(download_url, timeout=5)
             try:
    
  • apps/knowledge/serializers/knowledge_workflow.py+4 0 modified
    @@ -295,6 +295,8 @@ def save_workflow(self, instance: Dict):
                 if instance.get('work_flow_template') is not None:
                     template_instance = instance.get('work_flow_template')
                     download_url = template_instance.get('downloadUrl')
    +                if not download_url.startswith('https://apps-assets.fit2cloud.com/'):
    +                    raise AppApiException(500, _("Illegal download url"))
                     # 查找匹配的版本名称
                     res = requests.get(download_url, timeout=5)
                     KnowledgeWorkflowSerializer.Import(data={
    @@ -521,6 +523,8 @@ def edit(self, instance: Dict):
                 if instance.get("work_flow_template"):
                     template_instance = instance.get('work_flow_template')
                     download_url = template_instance.get('downloadUrl')
    +                if not download_url.startswith('https://apps-assets.fit2cloud.com/'):
    +                    raise AppApiException(500, _("Illegal download url"))
                     # 查找匹配的版本名称
                     res = requests.get(download_url, timeout=5)
                     KnowledgeWorkflowSerializer.Import(data={
    
  • apps/tools/serializers/tool.py+4 0 modified
    @@ -420,6 +420,8 @@ def insert(self, instance, with_valid=True):
                 if instance.get('work_flow_template') is not None:
                     template_instance = instance.get('work_flow_template')
                     download_url = template_instance.get('downloadUrl')
    +                if not download_url.startswith('https://apps-assets.fit2cloud.com/'):
    +                    raise AppApiException(500, _("Illegal download url"))
                     # 查找匹配的版本名称
                     res = requests.get(download_url, timeout=5)
                     tool = ToolSerializer.Import(data={
    @@ -1190,6 +1192,8 @@ def add(self, instance: Dict, with_valid=True):
     
                 versions = instance.get('versions', [])
                 download_url = instance.get('download_url')
    +            if not download_url.startswith('https://apps-assets.fit2cloud.com/'):
    +                raise AppApiException(500, _("Illegal download url"))
                 # 查找匹配的版本名称
                 version_name = next(
                     (version.get('name') for version in versions if version.get('downloadUrl') == download_url),
    
  • apps/tools/serializers/tool_workflow.py+2 0 modified
    @@ -323,6 +323,8 @@ def edit(self, instance: Dict):
                 if instance.get("work_flow_template"):
                     template_instance = instance.get('work_flow_template')
                     download_url = template_instance.get('downloadUrl')
    +                if not download_url.startswith('https://apps-assets.fit2cloud.com/'):
    +                    raise AppApiException(500, _("Illegal download url"))
                     # 查找匹配的版本名称
                     res = requests.get(download_url, timeout=5)
                     tool = QuerySet(Tool).filter(id=self.data.get("tool_id")).first()
    
587f5b6cde9c
https://github.com/1Panel-dev/MaxKBFixed in 2.9.1via llm-release-walk
587f5b6cde9c

Revert "fix: validate and replace hostname with resolved IP in request URL"

https://github.com/1Panel-dev/MaxKBwxg0103May 14, 2026Fixed in 2.9.1via llm-release-walk
1 file changed · +8 16
  • apps/oss/serializers/file.py+8 16 modified
    @@ -176,22 +176,14 @@ class SafeHTTPAdapter(HTTPAdapter):
         """
     
         def send(self, request, **kwargs):
    -        parsed = urlparse(request.url)
    -        host = parsed.hostname
    -        port = parsed.port or (443 if parsed.scheme == 'https' else 80)
    -
    -        # Resolve ONCE
    -        addr_infos = socket.getaddrinfo(host, port, socket.AF_INET, socket.SOCK_STREAM)
    -        validated_ip = None
    -        for info in addr_infos:
    -            ip = info[4][0]
    -            if self._is_unsafe_ip(ip):
    -                raise ValueError(f"Blocked: {ip}")
    -            validated_ip = ip
    -
    -        # PIN: replace hostname with validated IP in the URL
    -        request.url = request.url.replace(f"//{host}", f"//{validated_ip}", 1)
    -        request.headers['Host'] = host  # Preserve Host header for virtual hosting
    +        # 解析 URL 获取主机名
    +        parsed_url = urlparse(request.url)
    +        host = parsed_url.hostname
    +
    +        if host:
    +            # 验证目标 IP 是否安全
    +            self._validate_host_ip(host)
    +
             return super().send(request, **kwargs)
     
         def _validate_host_ip(self, host: str):
    
587f5b6cde9c
https://github.com/1Panel-dev/MaxKBFixed in 2.9.1via llm-release-walk

Vulnerability mechanics

Root cause

"Missing URL validation in work_flow_template.downloadUrl allows server-side requests to arbitrary URLs without internal IP filtering."

Attack vector

An authenticated attacker supplies an arbitrary URL in the `work_flow_template.downloadUrl` field when creating or updating an application, workflow, or tool. The server-side `requests.get()` call fetches that URL without any validation or internal IP filtering [ref_id=1]. This allows the attacker to target internal network addresses, including RFC1918 private ranges, link-local addresses, and cloud metadata endpoints such as `http://169.254.169.254/latest/meta-data/` [ref_id=1]. The response content is then parsed by the import logic, enabling internal network reconnaissance [ref_id=1].

Affected code

The vulnerability exists in multiple serializer files where `downloadUrl` from user-supplied `work_flow_template` data is passed directly to `requests.get()` without validation. The affected code paths are in `apps/application/serializers/application.py` (lines 524-526 and 1144-1146), `apps/knowledge/serializers/knowledge_workflow.py` (lines 295-297 and 523-525), `apps/tools/serializers/tool.py` (lines 420-422 and 1192-1194), and `apps/tools/serializers/tool_workflow.py` (lines 323-325) [ref_id=1].

What the fix does

Patch [patch_id=2590861] adds a `startswith('https://apps-assets.fit2cloud.com/')` check before every `requests.get(download_url)` call across all affected serializer files, rejecting any URL that does not begin with the expected domain. A prior attempt [patch_id=2590859] tried to resolve the hostname to an IP and validate it via `SafeHTTPAdapter`, but that approach was reverted in [patch_id=2590840] in favor of the simpler domain-prefix check. The domain whitelist approach closes SSRF by ensuring only the trusted asset server can be reached.

Preconditions

  • authAttacker must be authenticated to the MaxKB application
  • inputAttacker must supply a work_flow_template.downloadUrl field in the request payload
  • networkNo network-level restrictions on outbound requests from the server

Generated on May 26, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

1

News mentions

0

No linked articles in our index yet.