PoC Archive PoC Archive
High CVE-2026-32255 (GHSA-qrx8-9hc6-jvqg) patched

Kan SSRF via Attachment Download Endpoint — CVE-2026-32255

by kOaDT (koadt@proton.me) · 2026-07-05

CVSS 8.6/10
Severity
High
CVE
CVE-2026-32255 (GHSA-qrx8-9hc6-jvqg)
Category
web
Affected product
Kan (kanbn/kan) open-source project management tool
Affected versions
Kan <= 0.5.4 (fixed in 0.5.5)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-03
Author / ResearcherkOaDT (koadt@proton.me)
CVE / AdvisoryCVE-2026-32255 (GHSA-qrx8-9hc6-jvqg)
Categoryweb
SeverityHigh
CVSS Score8.6 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N)
StatusPoC
Tagsssrf, kan, project-management, cwe-918, cloud-metadata, unauthenticated, full-read-ssrf
RelatedN/A

Affected Target

FieldValue
Software / SystemKan (kanbn/kan) open-source project management tool
Versions AffectedKan <= 0.5.4 (fixed in 0.5.5)
Language / PlatformTypeScript/Next.js target; Bash + Python 3 PoC tooling
Authentication RequiredNo
Network Access RequiredYes

Summary

Kan’s attachment download proxy endpoint, GET /api/download/attatchment, is intended to stream S3-hosted attachments to clients but instead takes a fully attacker-controlled url query parameter and passes it directly to fetch() on the server with no validation, authentication, or hostname allowlisting. Because the full response body is relayed back to the caller (not blind), an unauthenticated attacker can use this endpoint to make the server issue arbitrary HTTP requests and read back the complete response — reaching internal-only services, cloud metadata endpoints, and other network-restricted resources. The included PoC spins up a mock “internal” credential-serving API and demonstrates the server fetching and leaking its contents through the vulnerable endpoint.


Vulnerability Details

Root Cause

apps/web/src/pages/api/download/attatchment.ts executes const upstream = await fetch(url) using a client-supplied url query parameter with no validation against the configured S3 endpoint or any other allowlist (CWE-918).

Attack Vector

  1. Attacker identifies the unauthenticated GET /api/download/attatchment?url=... endpoint on a Kan instance.
  2. Attacker supplies a url value pointing to an internal-only address (e.g. an internal API, database admin panel, or 169.254.169.254 cloud metadata IP) instead of a legitimate S3 attachment URL.
  3. The Kan server performs the fetch() server-side and streams the full response body back to the attacker in the HTTP response.
  4. Attacker reads sensitive internal data (credentials, config, IAM metadata) directly from the response, or repeats the request against a range of internal hosts/ports to map the internal network.

Impact

Full-read, unauthenticated SSRF enabling access to internal services and cloud metadata endpoints, network reconnaissance/port scanning, and potential exfiltration of credentials or other sensitive internal data.


Environment / Lab Setup

Target:   Kan v0.5.4 (docker-compose stack: Kan, Postgres, Redis, S3-compatible storage) on http://localhost:3000
Attacker: Docker & Docker Compose, Bash, curl, Python 3 (for the mock internal service)

Proof of Concept

PoC Script

See exploit.sh and internal-service.py in this folder.

1
2
3
python3 internal-service.py
chmod +x exploit.sh
./exploit.sh http://localhost:3000

internal-service.py starts a mock internal API on port 8888 that returns fake credentials, simulating a network-isolated service. exploit.sh checks that the target’s attachment-download endpoint is reachable, then requests it with url pointing at the mock internal service (auto-detecting the Docker bridge gateway), printing the leaked internal response body if the SSRF succeeds.


Detection & Indicators of Compromise

GET /api/download/attatchment?url=http://169.254.169.254/latest/meta-data/... HTTP/1.1

Signs of compromise:

  • Requests to /api/download/attatchment with a url parameter resolving to internal/private IP ranges or the cloud metadata address.
  • Outbound connections from the Kan web server process to hosts other than the configured S3/storage endpoint.
  • Full response bodies from internal-only services appearing in HTTP responses served to external clients.

Remediation

ActionDetail
Primary fixUpgrade to Kan v0.5.5 or later, which validates the url parameter against the configured S3 endpoint hostname before proxying
Interim mitigationBlock external access to the download endpoint at the reverse proxy, or restrict egress from the application server to only the legitimate storage endpoint.

References


Notes

Mirrored from https://github.com/kOaDT/poc-cve-2026-32255 on 2026-07-05.

internal-service.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/usr/bin/env python3
"""
Simulated internal service for CVE-2026-32255 PoC.

This script mimics an internal API that exposes sensitive configuration data.
In a real scenario, this would be a service only accessible from within the
internal network (e.g., a config server, metadata endpoint, or admin API).

Usage: python3 internal-service.py [port]
"""

import json
import sys
from http.server import HTTPServer, BaseHTTPRequestHandler

PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8888

SENSITIVE_DATA = {
    "service": "internal-config-api",
    "credentials": {
        "db_host": "10.0.0.5",
        "db_user": "admin",
        "db_password": "s3cret_passw0rd!",
        "api_key": "sk-internal-4f8a2b1c9d3e7f6a5b0c8d2e1f4a7b3c",
    },
}


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps(SENSITIVE_DATA, indent=2).encode())

    def log_message(self, format, *args):
        print(f"[internal-service] {args[0]}")


if __name__ == "__main__":
    server = HTTPServer(("0.0.0.0", PORT), Handler)
    print(f"[*] Internal service listening on http://0.0.0.0:{PORT}")
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\n[*] Shutting down")
        server.shutdown()